switch from JSON to structural cloning
There are two reasons for this: 1. I've reconsidered my original plan to store the past games in localStorage, because that would make it difficult to display them in historical order, and would necessitate more complex logic for updating and removing sessions. 2. I've been unhappy with how I did the testing of the serialization and deserialization logic. So I redid it, and now I'm satisfied with it. I've noticed that the testing methodology for the invalid fromStruct method tests is not fully sound. If a check is accidentally removed that test would not detect that, as long as it is not the very last. That is because then the next error triggers. Therefore that will need to be revisited.
This commit is contained in:
parent
82082ef84e
commit
85b9c2459c
@ -53,29 +53,14 @@ export default class Game extends EventTarget {
|
||||
if (typeof value === "number")
|
||||
this.#goal = value;
|
||||
|
||||
if (this.#goal < 1)
|
||||
throw new RangeError("goal must be at least 1");
|
||||
|
||||
this.#currentRound = new Round(this.#goal, this.#goal);
|
||||
this.#currentRound.addEventListener(
|
||||
Round.winEvent, this.#boundRoundFinishedHandler);
|
||||
} else if (typeof value === "object") {
|
||||
if (!("goal" in value))
|
||||
throw new TypeError("missing goal in deserialization object");
|
||||
if (typeof value.goal !== "number")
|
||||
throw new TypeError("goal in deserialization object must be number");
|
||||
this.#goal = value.goal;
|
||||
|
||||
if (!("rounds" in value))
|
||||
throw new TypeError("missing rounds in deserialization object");
|
||||
if (!Array.isArray(value.rounds))
|
||||
throw new TypeError("rounds in deserialization object must be array");
|
||||
for (let r of value.rounds)
|
||||
this.#rounds.push(new RoundResult(r));
|
||||
|
||||
if (!("currentRound" in value))
|
||||
throw new TypeError("missing currentRound in deserialization object");
|
||||
if (this.result.winner === null)
|
||||
this.#currentRound = new Round(value.currentRound);
|
||||
else if (value.currentRound !== null)
|
||||
throw new TypeError("currentRound in finished game must be null");
|
||||
this.#fromStruct(value);
|
||||
} else {
|
||||
throw new TypeError("unknown form of Game constructor");
|
||||
}
|
||||
@ -151,12 +136,55 @@ export default class Game extends EventTarget {
|
||||
|
||||
#boundRoundFinishedHandler = this.#handleRoundFinished.bind(this);
|
||||
|
||||
/** Export needed data for JSON serialization. */
|
||||
toJSON() {
|
||||
/** Export the data of this `Game` as a plain JS object with fields.
|
||||
*
|
||||
* The internals of the returned object are not stabilized, even if they are
|
||||
* visible. It should be treated as opaque.
|
||||
*
|
||||
* There are only two stabile uses of the object:
|
||||
* 1. It can be passed to the `Game` constructor as a single argument. The
|
||||
* constructor will then create a behaviourally identical instance to the
|
||||
* one from which the object was created. This is guaranteed to be
|
||||
* backwards compatible, i.e. a revised version of this class can still
|
||||
* use the objects created by an older version.
|
||||
* 2. It can be stored using IndexedDB.
|
||||
*/
|
||||
toStruct() {
|
||||
return {
|
||||
goal: this.#goal,
|
||||
rounds: this.#rounds,
|
||||
currentRound: this.#currentRound,
|
||||
rounds: this.#rounds.map((r) => r.toStruct()),
|
||||
currentRound:
|
||||
this.#currentRound !== null ? this.#currentRound.toStruct() : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Read in an object created by `Game.toStruct` */
|
||||
#fromStruct(value) {
|
||||
if (typeof value !== "object")
|
||||
throw new TypeError("struct must be an object");
|
||||
|
||||
if (typeof value.goal !== "number")
|
||||
throw new TypeError("struct must contain goal as number");
|
||||
if (!Number.isInteger(value.goal) || value.goal < 1)
|
||||
throw new RangeError("struct must contain goal >= 1 as integer");
|
||||
this.#goal = value.goal;
|
||||
|
||||
if (!("rounds" in value))
|
||||
throw new TypeError("struct must contain rounds");
|
||||
if (!Array.isArray(value.rounds))
|
||||
throw new TypeError("struct must contain rounds as array");
|
||||
this.#rounds = value.rounds.map((r) => new RoundResult(r));
|
||||
|
||||
if (typeof value.currentRound !== "object")
|
||||
throw new TypeError("struct must contain currentRound as object");
|
||||
if (this.result.winner === null) {
|
||||
if (value.currentRound === null)
|
||||
throw new TypeError(
|
||||
"struct of ongoing game must contain current round");
|
||||
else
|
||||
this.#currentRound = new Round(value.currentRound);
|
||||
} else if (value.currentRound !== null)
|
||||
throw new TypeError(
|
||||
"struct of finished game must not contain current round");
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,10 @@ export default function() {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("low goal", function(assert) {
|
||||
assert.throws(function() { new Game(0); }, "goal must be 1 or higher");
|
||||
});
|
||||
|
||||
QUnit.test("higher goal", function(assert) {
|
||||
let game = new Game(15);
|
||||
assert.strictEqual(game.rounds.length, 0, "no past rounds");
|
||||
@ -45,10 +49,8 @@ export default function() {
|
||||
game.currentRound.winner = Team.We;
|
||||
|
||||
assert.equal(game.rounds.length, 1, "one round played");
|
||||
assert.deepEqual(
|
||||
game.rounds[0].toJSON(),
|
||||
{ points: 2, winner: Team.We},
|
||||
"first round correct");
|
||||
assert.strictEqual(game.rounds[0].points, 2, "first round points");
|
||||
assert.strictEqual(game.rounds[0].winner, Team.We, "first round winner");
|
||||
assert.notStrictEqual(game.currentRound, null, "current round there");
|
||||
assert.false(game.currentRound.decided, "current round is not decided");
|
||||
assert.deepEqual(
|
||||
@ -70,10 +72,9 @@ export default function() {
|
||||
game.currentRound.winner = Team.They;
|
||||
|
||||
assert.equal(game.rounds.length, 2, "two round played");
|
||||
assert.deepEqual(
|
||||
game.rounds[1].toJSON(),
|
||||
{ points: 3, winner: Team.They},
|
||||
"second round correct");
|
||||
assert.strictEqual(game.rounds[1].points, 3, "second round points");
|
||||
assert.strictEqual(
|
||||
game.rounds[1].winner, Team.They, "second round winner");
|
||||
assert.notStrictEqual(game.currentRound, null, "current round there");
|
||||
assert.false(game.currentRound.decided, "current round is not decided");
|
||||
assert.deepEqual(
|
||||
@ -183,21 +184,24 @@ export default function() {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("serialization - unfinished", function(assert) {
|
||||
QUnit.test("finished event", function(assert) {
|
||||
let game = new Game(2);
|
||||
game.addEventListener(Game.finishedEvent, function() {
|
||||
assert.step("event");
|
||||
});
|
||||
game.currentRound.winner = Team.They;
|
||||
assert.verifySteps(["event"], "event was triggered");
|
||||
});
|
||||
|
||||
QUnit.test("toStruct - unfinished", function(assert) {
|
||||
let game = new Game();
|
||||
game.currentRound.winner = Team.We;
|
||||
game.currentRound.raise(Team.They);
|
||||
game.currentRound.winner = Team.They;
|
||||
game.currentRound.raise(Team.We);
|
||||
let struct = game.toStruct();
|
||||
|
||||
let json = game.toJSON();
|
||||
json.currentRound = json.currentRound.toJSON();
|
||||
for (let i = 0; i < json.rounds.length; i++)
|
||||
json.rounds[i] = json.rounds[i].toJSON();
|
||||
|
||||
assert.deepEqual(
|
||||
json,
|
||||
{
|
||||
let expected = {
|
||||
goal: 11,
|
||||
rounds: [
|
||||
{ points: 2, winner: Team.We },
|
||||
@ -207,140 +211,147 @@ export default function() {
|
||||
points: 3,
|
||||
raisedLast: Team.We,
|
||||
winner: null,
|
||||
weLimit: 9,
|
||||
theyLimit: 8,
|
||||
ourLimit: 9,
|
||||
theirLimit: 8
|
||||
},
|
||||
},
|
||||
"serialized data"
|
||||
);
|
||||
};
|
||||
|
||||
assert.deepEqual(struct, expected, "successfull structurizing");
|
||||
});
|
||||
|
||||
QUnit.test("serialization - finished", function(assert) {
|
||||
QUnit.test("toStruct - finished", function(assert) {
|
||||
let game = new Game(3);
|
||||
game.currentRound.winner = Team.We;
|
||||
game.currentRound.raise(Team.They);
|
||||
game.currentRound.winner = Team.They;
|
||||
let struct = game.toStruct();
|
||||
|
||||
let json = game.toJSON();
|
||||
for (let i = 0; i < json.rounds.length; i++)
|
||||
json.rounds[i] = json.rounds[i].toJSON();
|
||||
|
||||
assert.deepEqual(
|
||||
json,
|
||||
{
|
||||
let expected = {
|
||||
goal: 3,
|
||||
rounds: [
|
||||
{ points: 2, winner: Team.We },
|
||||
{ points: 3, winner: Team.They },
|
||||
],
|
||||
currentRound: null,
|
||||
},
|
||||
"serialized data"
|
||||
);
|
||||
};
|
||||
|
||||
assert.deepEqual(struct, expected, "successfull structurizing");
|
||||
});
|
||||
|
||||
QUnit.test("deserialize - unfinished", function(assert) {
|
||||
let currentRound = new Round(2, 3);
|
||||
currentRound.raise(Team.They);
|
||||
QUnit.test("fromStruct - current", function(assert) {
|
||||
let orig = new Game(4);
|
||||
orig.currentRound.raise(Team.We);
|
||||
|
||||
let game = new Game({
|
||||
let copy = new Game(orig.toStruct());
|
||||
assert.strictEqual(copy.goal, orig.goal, "goals match");
|
||||
assert.strictEqual(
|
||||
copy.currentRound.points,
|
||||
orig.currentRound.points,
|
||||
"current points match");
|
||||
assert.strictEqual(
|
||||
copy.rounds.length, orig.rounds.length, "rounds match");
|
||||
|
||||
orig.currentRound.winner = Team.We;
|
||||
copy = new Game(orig.toStruct());
|
||||
assert.strictEqual(
|
||||
copy.rounds.length, orig.rounds.length, "rounds match");
|
||||
assert.deepEqual(
|
||||
copy.rounds[0].toStruct(), orig.rounds[0].toStruct(), "round matches");
|
||||
|
||||
orig.currentRound.winner = Team.We;
|
||||
copy = new Game(orig.toStruct());
|
||||
assert.deepEqual(copy.result, orig.result, "results match");
|
||||
});
|
||||
|
||||
QUnit.test("fromStruct - invalid", function(assert) {
|
||||
let struct = {};
|
||||
function doIt(message) {
|
||||
assert.throws(function() { new Game(struct); }, message);
|
||||
}
|
||||
|
||||
doIt("no goal");
|
||||
struct.goal = "3";
|
||||
doIt("string goal");
|
||||
struct.goal = Math.PI;
|
||||
doIt("non-int goal");
|
||||
struct.goal = 0;
|
||||
doIt("small goal");
|
||||
struct.goal = 3;
|
||||
|
||||
doIt("no rounds");
|
||||
struct.rounds = "nope";
|
||||
doIt("rounds not array");
|
||||
struct.rounds = ["nope", "again"];
|
||||
doIt("string array rounds");
|
||||
struct.rounds = [];
|
||||
|
||||
doIt("no currentRound");
|
||||
struct.currentRound = "nope";
|
||||
doIt("string currentround");
|
||||
struct.currentRound = null;
|
||||
doIt("missing currentRound");
|
||||
struct.currentRound = new Round().toStruct();
|
||||
new Game(struct);
|
||||
|
||||
struct.rounds = [ new RoundResult(3, Team.They).toStruct() ];
|
||||
doIt("unneeded currentRound");
|
||||
struct.currentRound = null;
|
||||
new Game(struct);
|
||||
});
|
||||
|
||||
// Data Import Tests
|
||||
// =================
|
||||
//
|
||||
// The tests named "fromStruct - vXX - XXXXX" are there to ensure that
|
||||
// future versions of the `Game` class still can correctly read in the
|
||||
// structural data exported by earlier versions. This is needed to ensure
|
||||
// that the data remains usable.
|
||||
//
|
||||
// These tests work by importing an old structural object, and then
|
||||
// exporting a new one. The new one should match with how the current
|
||||
// implementation would represent the same state.
|
||||
//
|
||||
// Therefore you should not modify the `struct` variables. Instead adjust
|
||||
// the `expected` variable, to make sure the reexported data matches what
|
||||
// is now correct.
|
||||
|
||||
QUnit.test("fromStruct - v1 - unfinished", function(assert) {
|
||||
let past = new RoundResult(2, Team.We);
|
||||
let current = new Round(2, 3);
|
||||
current.raise(Team.They);
|
||||
|
||||
let struct = {
|
||||
goal: 3,
|
||||
rounds: [{ winner: Team.We, points: 2 }],
|
||||
currentRound: currentRound.toJSON(),
|
||||
});
|
||||
rounds: [ past.toStruct() ],
|
||||
currentRound: current.toStruct(),
|
||||
};
|
||||
let game = new Game(struct);
|
||||
|
||||
assert.strictEqual(game.goal, 3, "goal");
|
||||
assert.strictEqual(game.rounds.length, 1, "one round played");
|
||||
assert.deepEqual(
|
||||
game.rounds[0].toJSON(),
|
||||
{ winner: Team.We, points: 2 },
|
||||
"correct past round");
|
||||
assert.deepEqual(
|
||||
game.currentRound.toJSON(),
|
||||
currentRound.toJSON(),
|
||||
"correct current round");
|
||||
assert.deepEqual(
|
||||
game.result,
|
||||
{
|
||||
winner: null,
|
||||
points: 0,
|
||||
ourPoints: 2,
|
||||
theirPoints: 0,
|
||||
},
|
||||
"intermediate results");
|
||||
});
|
||||
|
||||
QUnit.test("deserialize - finished", function(assert) {
|
||||
let game = new Game({
|
||||
let expected = {
|
||||
goal: 3,
|
||||
rounds: [{ winner: Team.They, points: 3 }],
|
||||
rounds: [ past.toStruct() ],
|
||||
currentRound: current.toStruct(),
|
||||
};
|
||||
assert.deepEqual(game.toStruct(), expected, "reexport matches");
|
||||
});
|
||||
|
||||
QUnit.test("fromStruct - v1 - finished", function(assert) {
|
||||
let round1 = new RoundResult(2, Team.We);
|
||||
let round2 = new RoundResult(3, Team.They);
|
||||
|
||||
let struct = {
|
||||
goal: 3,
|
||||
rounds: [ round1.toStruct(), round2.toStruct() ],
|
||||
currentRound: null,
|
||||
});
|
||||
};
|
||||
let game = new Game(struct);
|
||||
|
||||
assert.strictEqual(game.goal, 3, "goal");
|
||||
assert.strictEqual(game.rounds.length, 1, "one round played");
|
||||
assert.deepEqual(
|
||||
game.rounds[0].toJSON(),
|
||||
{ winner: Team.They, points: 3 },
|
||||
"correct past round");
|
||||
assert.strictEqual(game.currentRound, null, "no current round");
|
||||
assert.deepEqual(
|
||||
game.result,
|
||||
{
|
||||
winner: Team.They,
|
||||
points: 2,
|
||||
ourPoints: 0,
|
||||
theirPoints: 3,
|
||||
},
|
||||
"final results");
|
||||
});
|
||||
|
||||
QUnit.test("deserialize - invalid", function(assert) {
|
||||
let deso = {};
|
||||
assert.throws(function() { new Game(deso); }, "no goal");
|
||||
|
||||
deso.goal = "5";
|
||||
assert.throws(function() { new Game(deso); }, "string goal");
|
||||
|
||||
deso.goal = 5;
|
||||
assert.throws(function() { new Game(deso); }, "no rounds");
|
||||
|
||||
deso.rounds = ["nonono"];
|
||||
assert.throws(function() { new Game(deso); }, "string rounds");
|
||||
|
||||
deso.rounds = [];
|
||||
assert.throws(function() { new Game(deso); }, "no currentRound");
|
||||
|
||||
deso.currentRound = null;
|
||||
assert.throws(function() { new Game(deso); }, "missing currentRound");
|
||||
|
||||
deso.currentRound = "nonono";
|
||||
assert.throws(function() { new Game(deso); }, "broken currentRound");
|
||||
|
||||
deso.rounds = [{ winner: Team.We, points: 5 }];
|
||||
deso.currentRound = {
|
||||
points: 2,
|
||||
raisedLast: Team.They,
|
||||
winner: null,
|
||||
weLimit: 2,
|
||||
theyLimit: 5};
|
||||
assert.throws(function() { new Game(deso); }, "unneeded currentRound");
|
||||
|
||||
deso.goal = 11;
|
||||
new Game(deso);
|
||||
|
||||
deso.goal = 5;
|
||||
deso.currentRound = null;
|
||||
new Game(deso);
|
||||
});
|
||||
|
||||
QUnit.test("finished event", function(assert) {
|
||||
let game = new Game(2);
|
||||
game.addEventListener(Game.finishedEvent, function() {
|
||||
assert.step("event");
|
||||
});
|
||||
game.currentRound.winner = Team.They;
|
||||
assert.verifySteps(["event"], "event was triggered");
|
||||
let expected = {
|
||||
goal: 3,
|
||||
rounds: [ round1.toStruct(), round2.toStruct() ],
|
||||
currentRound: null,
|
||||
};
|
||||
assert.deepEqual(game.toStruct(), expected, "reexport matches");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
120
models/round.js
120
models/round.js
@ -8,6 +8,15 @@ export const Team = Object.freeze({
|
||||
We: 1,
|
||||
/** The "they" team, from the perspective of the score keeper. */
|
||||
They: 2,
|
||||
|
||||
/** Check if the passed value is a team.
|
||||
*
|
||||
* @param {Team} team The team to check.
|
||||
* @returns Whether the value is a team.
|
||||
*/
|
||||
isTeam(team) {
|
||||
return (team === Team.We) || (team === Team.They);
|
||||
}
|
||||
});
|
||||
|
||||
/** A single round of watten.
|
||||
@ -27,9 +36,15 @@ export class Round extends EventTarget {
|
||||
/** The event triggered when the round is won. */
|
||||
static winEvent= "roundWon";
|
||||
|
||||
/** The maximum the "we" team may raise to. */
|
||||
/** The maximum the "we" team may raise to.
|
||||
*
|
||||
* @todo rename to ourLimit
|
||||
*/
|
||||
#weLimit = 11;
|
||||
/** The maximum the "they" team may raise to. */
|
||||
/** The maximum the "they" team may raise to.
|
||||
*
|
||||
* @todo rename to theirLimit
|
||||
*/
|
||||
#theyLimit = 11;
|
||||
|
||||
constructor(value, theyLimit) {
|
||||
@ -44,48 +59,7 @@ export class Round extends EventTarget {
|
||||
this.#weLimit = value;
|
||||
this.#theyLimit = theyLimit;
|
||||
} else if (typeof value === "object" && theyLimit === undefined) {
|
||||
if (!("points" in value))
|
||||
throw new TypeError("missing points in deserialization object");
|
||||
if (typeof value.points !== "number")
|
||||
throw new TypeError("points in deserialization object must be number");
|
||||
this.#points = value.points;
|
||||
|
||||
if (!("raisedLast" in value))
|
||||
throw new TypeError("missing raisedLast in deserialization object");
|
||||
if (value.raisedLast !== Team.We
|
||||
&& value.raisedLast !== Team.They
|
||||
&& value.raisedLast !== null)
|
||||
{
|
||||
throw new TypeError(
|
||||
"team raising last must be an actual team in deserialization object"
|
||||
);
|
||||
}
|
||||
this.#raisedLast = value.raisedLast;
|
||||
|
||||
if (!("winner" in value))
|
||||
throw new TypeError("missing winner in deserialization object");
|
||||
if (value.winner !== Team.We
|
||||
&& value.winner !== Team.They
|
||||
&& value.winner !== null)
|
||||
{
|
||||
throw new TypeError(
|
||||
"winning team must be an actual team in deserialization object");
|
||||
}
|
||||
this.#winner = value.winner;
|
||||
|
||||
if (!("weLimit" in value))
|
||||
throw new TypeError("missing weLimit in deserialization object");
|
||||
if (typeof value.weLimit !== "number")
|
||||
throw new TypeError(
|
||||
"weLimit in deserialization object must be a number");
|
||||
this.#weLimit = value.weLimit;
|
||||
|
||||
if (!("theyLimit" in value))
|
||||
throw new TypeError("missing theyLimit in deserialization object");
|
||||
if (typeof value.theyLimit !== "number")
|
||||
throw new TypeError(
|
||||
"theyLimit in deserialization object must be a number");
|
||||
this.#theyLimit = value.theyLimit;
|
||||
this.#fromStruct(value);
|
||||
} else {
|
||||
throw new TypeError("unknown form for Round constructor");
|
||||
}
|
||||
@ -178,14 +152,62 @@ export class Round extends EventTarget {
|
||||
this.#points += 1;
|
||||
}
|
||||
|
||||
/** Export needed data for JSON serialization. */
|
||||
toJSON() {
|
||||
/** Export the data of this `Round` as a plain JS object with fields.
|
||||
*
|
||||
* The internals of the returned object are not stabilized, even if they are
|
||||
* visible. It should be treated as opaque.
|
||||
*
|
||||
* There are only two stabile uses of the object:
|
||||
* 1. It can be passed to the `Round` constructor as a single argument. The
|
||||
* constructor will then create a behaviourally identical instance to the
|
||||
* one from which the object was created. This is guaranteed to be
|
||||
* backwards compatible, i.e. a revised version of this class can still
|
||||
* use the objects created by an older version.
|
||||
* 2. It can be stored using IndexedDB.
|
||||
*/
|
||||
toStruct() {
|
||||
return {
|
||||
points: this.#points,
|
||||
raisedLast: this.#raisedLast,
|
||||
winner: this.#winner,
|
||||
weLimit: this.#weLimit,
|
||||
theyLimit: this.#theyLimit,
|
||||
};
|
||||
ourLimit: this.#weLimit,
|
||||
theirLimit: this.#theyLimit,
|
||||
}
|
||||
}
|
||||
|
||||
/** Read in an object created by `Round.toStruct` */
|
||||
#fromStruct(value) {
|
||||
if (typeof value !== "object")
|
||||
throw new TypeError("struct must be an object");
|
||||
|
||||
if (typeof value.points !== "number")
|
||||
throw new TypeError("struct must contain points as number");
|
||||
if (!Number.isInteger(value.points) || value.points < 2)
|
||||
throw new RangeError("struct must contain points >= 2 as integer");
|
||||
this.#points = value.points;
|
||||
|
||||
if (!("raisedLast" in value))
|
||||
throw new TypeError("struct must contain raisedLast");
|
||||
if (value.raisedLast !== null && !Team.isTeam(value.raisedLast))
|
||||
throw new TypeError("struct must contain raisedLast as Team or null");
|
||||
this.#raisedLast = value.raisedLast;
|
||||
|
||||
if (!("winner" in value))
|
||||
throw new TypeError("struct must contain winner");
|
||||
if (value.winner !== null && !Team.isTeam(value.winner))
|
||||
throw new TypeError("struct must contain winner as Team or null");
|
||||
this.#winner = value.winner;
|
||||
|
||||
if (typeof value.ourLimit !== "number")
|
||||
throw new TypeError("struct must contain ourLimit as number");
|
||||
if (!Number.isInteger(value.ourLimit) || value.ourLimit < 2)
|
||||
throw new RangeError("struct must contain ourLimit >= 2 as integer");
|
||||
this.#weLimit = value.ourLimit;
|
||||
|
||||
if (typeof value.theirLimit !== "number")
|
||||
throw new TypeError("struct must contain theirLimit as number");
|
||||
if (!Number.isInteger(value.theirLimit) || value.theirLimit < 2)
|
||||
throw new RangeError("struct must contain theirLimit >= 2 as integer");
|
||||
this.#theyLimit = value.theirLimit;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,73 +92,6 @@ export default function() {
|
||||
assert.false(round.canRaise(Team.They), "winner cannot raise");
|
||||
});
|
||||
|
||||
QUnit.test("JSON serialization", function(assert) {
|
||||
let round = new Round();
|
||||
assert.deepEqual(
|
||||
round.toJSON(),
|
||||
{
|
||||
points: 2,
|
||||
raisedLast: null,
|
||||
winner: null,
|
||||
weLimit: 11,
|
||||
theyLimit: 11,
|
||||
},
|
||||
"correct field override"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("JSON deserialization", function(assert) {
|
||||
let round = new Round({
|
||||
points: 7,
|
||||
raisedLast: Team.They,
|
||||
winner: null,
|
||||
weLimit: 6,
|
||||
theyLimit: 11,
|
||||
});
|
||||
assert.strictEqual(round.points, 7, "points correct");
|
||||
assert.false(round.canRaise(Team.They), "raiser cannot raise");
|
||||
assert.true(round.canRaise(Team.We), "others can raise");
|
||||
assert.false(round.decided, "noone won yet");
|
||||
|
||||
round.raise(Team.We);
|
||||
assert.strictEqual(round.winner, Team.They, "limits enforcement");
|
||||
});
|
||||
|
||||
QUnit.test("invalid JSON deserialization", function(assert) {
|
||||
let deso = {};
|
||||
assert.throws(function() { new Round(deso) }, "no points");
|
||||
|
||||
deso.points = "2";
|
||||
assert.throws(function() { new Round(deso) }, "string points");
|
||||
|
||||
deso.points = 2;
|
||||
assert.throws(function() { new Round(deso) }, "no raisedLast");
|
||||
|
||||
deso.raisedLast = "Team.We";
|
||||
assert.throws(function() { new Round(deso) }, "string raisedLast");
|
||||
|
||||
deso.raisedLast = Team.We;
|
||||
assert.throws(function() { new Round(deso) }, "no winner");
|
||||
|
||||
deso.winner = "Team.They";
|
||||
assert.throws(function() { new Round(deso) }, "string winner");
|
||||
|
||||
deso.winner = Team.They;
|
||||
assert.throws(function() { new Round(deso) }, "no weLimit");
|
||||
|
||||
deso.weLimit = "11";
|
||||
assert.throws(function() { new Round(deso) }, "string weLimit");
|
||||
|
||||
deso.weLimit = 11;
|
||||
assert.throws(function() { new Round(deso) }, "no theyLimit");
|
||||
|
||||
deso.theyLimit = "11";
|
||||
assert.throws(function() { new Round(deso) }, "string theyLimit");
|
||||
|
||||
deso.theyLimit = 11;
|
||||
new Round(deso);
|
||||
});
|
||||
|
||||
QUnit.test("victory causes event", function(assert) {
|
||||
let round = new Round();
|
||||
round.addEventListener(Round.winEvent, function() {
|
||||
@ -167,5 +100,159 @@ export default function() {
|
||||
round.winner = Team.We;
|
||||
assert.verifySteps(["event"], "event was triggered");
|
||||
});
|
||||
|
||||
QUnit.test("toStruct - unfinished", function(assert) {
|
||||
let round = new Round();
|
||||
let struct = round.toStruct();
|
||||
|
||||
let expected = {
|
||||
points: 2,
|
||||
raisedLast: null,
|
||||
winner: null,
|
||||
ourLimit: 11,
|
||||
theirLimit: 11,
|
||||
};
|
||||
|
||||
assert.deepEqual(struct, expected, "successfull structurizing");
|
||||
});
|
||||
|
||||
QUnit.test("toStruct - finished", function(assert) {
|
||||
let round = new Round(4, 3);
|
||||
round.raise(Team.We);
|
||||
round.raise(Team.They);
|
||||
let struct = round.toStruct();
|
||||
|
||||
let expected = {
|
||||
points: 3,
|
||||
raisedLast: Team.We,
|
||||
winner: Team.We,
|
||||
ourLimit: 4,
|
||||
theirLimit: 3,
|
||||
};
|
||||
|
||||
assert.deepEqual(struct, expected, "successfull structurizing");
|
||||
});
|
||||
|
||||
QUnit.test("fromStruct - current", function(assert) {
|
||||
let orig = new Round(3, 3);
|
||||
orig.raise(Team.We);
|
||||
|
||||
let copy = new Round(orig.toStruct());
|
||||
assert.strictEqual(copy.points, orig.points, "points match");
|
||||
assert.strictEqual(
|
||||
copy.canRaise(Team.We),
|
||||
orig.canRaise(Team.We),
|
||||
"can we raise matches");
|
||||
assert.strictEqual(
|
||||
copy.canRaise(Team.They),
|
||||
orig.canRaise(Team.They),
|
||||
"can they raise matches");
|
||||
|
||||
orig.winner = Team.They;
|
||||
copy = new Round(orig.toStruct());
|
||||
assert.strictEqual(copy.winner, orig.winner, "winners match");
|
||||
});
|
||||
|
||||
QUnit.test("fromStruct - invalid", function(assert) {
|
||||
let struct = {};
|
||||
function doIt(message) {
|
||||
assert.throws(function() { new Round(struct); }, message);
|
||||
}
|
||||
|
||||
doIt("no points");
|
||||
struct.points = "2";
|
||||
doIt("string points");
|
||||
struct.points = 1.5;
|
||||
doIt("non-int points");
|
||||
struct.points = 1;
|
||||
doIt("small points");
|
||||
struct.points = 2;
|
||||
|
||||
doIt("no raisedLast");
|
||||
struct.raisedLast = "we";
|
||||
doIt("string raisedLast");
|
||||
struct.raisedLast = -1;
|
||||
doIt("raisedLast not actual team");
|
||||
struct.raisedLast = null;
|
||||
|
||||
doIt("no winner");
|
||||
struct.winner = "they";
|
||||
doIt("string winner");
|
||||
struct.winner = -1;
|
||||
doIt("winner not actual team");
|
||||
struct.winner = null;
|
||||
|
||||
doIt("no ourLimit");
|
||||
struct.ourLimit = "11";
|
||||
doIt("string ourLimit");
|
||||
struct.ourLimit = 1;
|
||||
doIt("small ourLimit");
|
||||
struct.ourLimit = 11;
|
||||
|
||||
doIt("no theirLimit");
|
||||
struct.theirLimit = "11";
|
||||
doIt("string theirLimit");
|
||||
struct.theirLimit = 1;
|
||||
doIt("small theirLimit");
|
||||
struct.theirLimit = 11;
|
||||
|
||||
new Round(struct);
|
||||
});
|
||||
|
||||
// Data Import Tests
|
||||
// =================
|
||||
//
|
||||
// The tests named "fromStruct - vXX - XXXXX" are there to ensure that
|
||||
// future versions of the `Round` class still can correctly read in the
|
||||
// structural data exported by earlier versions. This is needed to ensure
|
||||
// that the data remains usable.
|
||||
//
|
||||
// These tests work by importing an old structural object, and then
|
||||
// exporting a new one. The new one should match with how the current
|
||||
// implementation would represent the same state.
|
||||
//
|
||||
// Therefore you should not modify the `struct` variables. Instead adjust
|
||||
// the `expected` variable, to make sure the reexported data matches what
|
||||
// is now correct.
|
||||
|
||||
QUnit.test("fromStruct - v1 - unfinished", function(assert) {
|
||||
let struct = {
|
||||
points: 2,
|
||||
raisedLast: null,
|
||||
winner: null,
|
||||
ourLimit: 11,
|
||||
theirLimit: 11,
|
||||
};
|
||||
let round = new Round(struct);
|
||||
|
||||
let expected = {
|
||||
points: 2,
|
||||
raisedLast: null,
|
||||
winner: null,
|
||||
ourLimit: 11,
|
||||
theirLimit: 11,
|
||||
};
|
||||
assert.deepEqual(round.toStruct(), expected, "reexport matches");
|
||||
});
|
||||
|
||||
QUnit.test("fromStruct - v1 - finished", function(assert) {
|
||||
let struct = {
|
||||
points: 3,
|
||||
raisedLast: Team.We,
|
||||
winner: Team.We,
|
||||
ourLimit: 4,
|
||||
theirLimit: 3
|
||||
};
|
||||
let round = new Round(struct);
|
||||
|
||||
let expected = {
|
||||
points: 3,
|
||||
raisedLast: Team.We,
|
||||
winner: Team.We,
|
||||
ourLimit: 4,
|
||||
theirLimit: 3
|
||||
};
|
||||
assert.deepEqual(round.toStruct(), expected, "reexport matches");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -19,17 +19,7 @@ export default class RoundResult {
|
||||
this.#points = value;
|
||||
this.#winner = winner;
|
||||
} else if (typeof value === "object" && winner === undefined) {
|
||||
if (!("points" in value))
|
||||
throw new TypeError("missing points in deserialization object");
|
||||
if (typeof value.points !== "number")
|
||||
throw new TypeEror("points in deserialization object must be number");
|
||||
this.#points = value.points;
|
||||
|
||||
if (!("winner" in value))
|
||||
throw new TypeError("missing winner in deserialization object");
|
||||
if (value.winner !== Team.We && value.winner !== Team.They)
|
||||
throw new TypeError("winner in deserialization object not real team");
|
||||
this.#winner = value.winner;
|
||||
this.#fromStruct(value);
|
||||
} else {
|
||||
throw new TypeError("unknown form for RoundResult constructor");
|
||||
}
|
||||
@ -48,11 +38,41 @@ export default class RoundResult {
|
||||
return this.#winner;
|
||||
}
|
||||
|
||||
/** Export needed data for JSON serialization. */
|
||||
toJSON() {
|
||||
/** Export the data of this `RoundResult` as a plain JS object with fields.
|
||||
*
|
||||
* The internals of the returned object are not stabilized, even if they are
|
||||
* visible. It should be treated as opaque.
|
||||
*
|
||||
* There are only two stabile uses of the object:
|
||||
* 1. It can be passed to the `RoundResult` constructor as a single argument.
|
||||
* The constructor will then create a behaviourally identical instance to
|
||||
* the one from which the object was created. This is guaranteed to be
|
||||
* backwards compatible, i.e. a revised version of this class can still
|
||||
* use the objects created by an older version.
|
||||
* 2. It can be stored using IndexedDB.
|
||||
*/
|
||||
toStruct() {
|
||||
return {
|
||||
points: this.#points,
|
||||
winner: this.#winner,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Read in an object created by `RoundResult.toStruct` */
|
||||
#fromStruct(value) {
|
||||
if (typeof value !== "object")
|
||||
throw new TypeError("struct must be an object");
|
||||
|
||||
if (typeof value.points !== "number")
|
||||
throw new TypeError("struct must contain points as number");
|
||||
if (!Number.isInteger(value.points) || value.points < 2)
|
||||
throw new RangeError("struct must contain points >= 2 as integer");
|
||||
this.#points = value.points;
|
||||
|
||||
if (!("winner" in value))
|
||||
throw new TypeError("struct must contain winner");
|
||||
if (!Team.isTeam(value.winner))
|
||||
throw new TypeError("struct must contain winner as Team");
|
||||
this.#winner = value.winner;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,42 +11,78 @@ export default function() {
|
||||
assert.strictEqual(rr.winner, Team.We, "correct winner");
|
||||
});
|
||||
|
||||
QUnit.test("serialization", function(assert) {
|
||||
let rr = new RoundResult(3, Team.They);
|
||||
assert.deepEqual(
|
||||
rr.toJSON(),
|
||||
{
|
||||
points: 3,
|
||||
QUnit.test("toStruct", function(assert) {
|
||||
let rr = new RoundResult(2, Team.They);
|
||||
let struct = rr.toStruct();
|
||||
|
||||
let expected = {
|
||||
points: 2,
|
||||
winner: Team.They,
|
||||
},
|
||||
"correct serialization object",
|
||||
);
|
||||
};
|
||||
|
||||
assert.deepEqual(struct, expected, "successfull structurizing");
|
||||
});
|
||||
|
||||
QUnit.test("deserialization", function(assert) {
|
||||
let rr = new RoundResult({
|
||||
QUnit.test("fromStruct - current", function(assert) {
|
||||
let orig = new RoundResult(3, Team.We);
|
||||
let copy = new RoundResult(orig.toStruct());
|
||||
assert.strictEqual(copy.points, orig.points, "points match");
|
||||
assert.strictEqual(copy.winner, orig.winner, "winners match");
|
||||
});
|
||||
|
||||
QUnit.test("fromStruct - invalid", function(assert) {
|
||||
let struct = {};
|
||||
function doIt(message) {
|
||||
assert.throws(function() { new Round(struct); }, message);
|
||||
}
|
||||
|
||||
doIt("no points");
|
||||
struct.points = "4";
|
||||
doIt("string points");
|
||||
struct.points = 4.1;
|
||||
doIt("non-int points");
|
||||
struct.points = 1;
|
||||
doIt("small points");
|
||||
struct.points = 4;
|
||||
|
||||
doIt("no winner");
|
||||
struct.winner = "they";
|
||||
doIt("string winner");
|
||||
struct.winner = -1;
|
||||
doIt("non-team winner");
|
||||
struct.winner = Team.They;
|
||||
|
||||
new RoundResult(struct);
|
||||
});
|
||||
|
||||
// Data Import Tests
|
||||
// =================
|
||||
//
|
||||
// The tests named "fromStruct - vXX - XXXXX" are there to ensure that
|
||||
// future versions of the `RoundResult` class still can correctly read in
|
||||
// the structural data exported by earlier versions. This is needed to
|
||||
// ensure that the data remains usable.
|
||||
//
|
||||
// These tests work by importing an old structural object, and then
|
||||
// exporting a new one. The new one should match with how the current
|
||||
// implementation would represent the same state.
|
||||
//
|
||||
// Therefore you should not modify the `struct` variables. Instead adjust
|
||||
// the `expected` variable, to make sure the reexported data matches what
|
||||
// is now correct.
|
||||
|
||||
QUnit.test("fromStruct - v1", function(assert) {
|
||||
let struct = {
|
||||
points: 4,
|
||||
winner: Team.We,
|
||||
});
|
||||
assert.strictEqual(rr.points, 4, "correct points");
|
||||
assert.strictEqual(rr.winner, Team.We, "correct winner");
|
||||
});
|
||||
winner: Team.They,
|
||||
};
|
||||
let rr = new RoundResult(struct);
|
||||
|
||||
QUnit.test("invalid deserialization", function(assert) {
|
||||
let deso = {};
|
||||
assert.throws(function() { new RoundResult(deso); }, "no points");
|
||||
|
||||
deso.points = "5";
|
||||
assert.throws(function() { new RoundResult(deso); }, "string points");
|
||||
|
||||
deso.points = 5;
|
||||
assert.throws(function() { new RoundResult(deso); }, "no winner");
|
||||
|
||||
deso.winner = "Team.They";
|
||||
assert.throws(function() { new RoundResult(deso); }, "string winner");
|
||||
|
||||
deso.winner = Team.They;
|
||||
new RoundResult(deso);
|
||||
let expected = {
|
||||
points: 4,
|
||||
winner: Team.They,
|
||||
};
|
||||
assert.deepEqual(rr.toStruct(), expected, "reexport matches");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -8,7 +8,21 @@ export default class Session {
|
||||
*
|
||||
* Only applies to new games.
|
||||
*/
|
||||
goal = 11;
|
||||
#goal = 11;
|
||||
|
||||
/** Get the goal for new games. */
|
||||
get goal() {
|
||||
return this.#goal;
|
||||
}
|
||||
|
||||
/** Set the goal for new games. */
|
||||
set goal(value) {
|
||||
if (typeof value !== "number")
|
||||
throw new TypeError("goal must be a number");
|
||||
if (!Number.isInteger(value) || value < 1)
|
||||
throw new RangeError("goal must be integer >= 1");
|
||||
this.#goal = value;
|
||||
}
|
||||
|
||||
/** The name or members of the "we" team. */
|
||||
ourTeam = "";
|
||||
@ -81,57 +95,70 @@ export default class Session {
|
||||
constructor(value) {
|
||||
if (value === undefined) {
|
||||
} else if (typeof value === "object") {
|
||||
if (!("goal" in value))
|
||||
throw new TypeError("missing goal in deserialization object");
|
||||
if (typeof value.goal !== "number")
|
||||
throw new TypeError("goal in deserialization object must be number");
|
||||
this.goal = value.goal;
|
||||
|
||||
if (!("ourTeam" in value))
|
||||
throw new TypeError("missing ourTeam in deserialization object");
|
||||
if (typeof value.ourTeam !== "string")
|
||||
throw new TypeError(
|
||||
"ourTeam in deserialization object must be string");
|
||||
this.ourTeam = value.ourTeam;
|
||||
|
||||
if (!("theirTeam" in value))
|
||||
throw new TypeError("missing theirTeam in deserialization object");
|
||||
if (typeof value.theirTeam !== "string")
|
||||
throw new TypeError(
|
||||
"theirTeam in deserialization object must be string");
|
||||
this.theirTeam = value.theirTeam;
|
||||
|
||||
if (!("games" in value))
|
||||
throw new TypeError("missing games in deserialization object");
|
||||
if (!Array.isArray(value.games))
|
||||
throw new TypeError("games in deserialization object must be array");
|
||||
for (let g of value.games) {
|
||||
let game = new Game (g);
|
||||
if (game.result.winner === null)
|
||||
throw new TypeError("past game cannot be unfinished");
|
||||
this.#games.push(game);
|
||||
}
|
||||
|
||||
if (!("currentGame" in value))
|
||||
throw new TypeError("missing currentGame in deserialization object");
|
||||
if (value.currentGame !== null) {
|
||||
this.#currentGame = new Game(value.currentGame);
|
||||
if (this.#currentGame.result.winner !== null)
|
||||
throw new Error("currentGame cannot be finished");
|
||||
}
|
||||
this.#fromStruct(value);
|
||||
} else {
|
||||
throw new TypeError("unknown form of Session constructor");
|
||||
}
|
||||
}
|
||||
|
||||
/** Export needed data for JSON serialization. */
|
||||
toJSON() {
|
||||
/** Export the data of this `Session` as a plain JS object with fields.
|
||||
*
|
||||
* The internals of the returned object are not stabilized, even if they are
|
||||
* visible. It should be treated as opaque.
|
||||
*
|
||||
* There are only two stabile uses of the object:
|
||||
* 1. It can be passed to the `Session` constructor as a single argument. The
|
||||
* constructor will then create a behaviourally identical instance to the
|
||||
* one from which the object was created. This is guaranteed to be
|
||||
* backwards compatible, i.e. a revised version of this class can still
|
||||
* use the objects created by an older version.
|
||||
* 2. It can be stored using IndexedDB.
|
||||
*/
|
||||
toStruct() {
|
||||
return {
|
||||
goal: this.goal,
|
||||
goal: this.#goal,
|
||||
ourTeam: this.ourTeam,
|
||||
theirTeam: this.theirTeam,
|
||||
games: this.#games,
|
||||
currentGame: this.#currentGame,
|
||||
games: this.#games.map((g) => g.toStruct()),
|
||||
currentGame:
|
||||
this.#currentGame !== null ? this.#currentGame.toStruct() : null,
|
||||
}
|
||||
}
|
||||
|
||||
/** Read in an object created by `Session.toStruct` */
|
||||
#fromStruct(value) {
|
||||
if (typeof value !== "object")
|
||||
throw new TypeError("struct must be an object");
|
||||
|
||||
if (typeof value.goal !== "number")
|
||||
throw new TypError("struct must contain goal as number");
|
||||
if (!Number.isInteger(value.goal) || value.goal < 1)
|
||||
throw new RangeError("struct must contain goal >= 1 as integer");
|
||||
this.#goal = value.goal;
|
||||
|
||||
if (typeof value.ourTeam !== "string")
|
||||
throw new TypeError("struct must contain ourTeam as string");
|
||||
this.ourTeam = value.ourTeam;
|
||||
|
||||
if (typeof value.theirTeam !== "string")
|
||||
throw new TypeError("struct must contain theirTeam as string");
|
||||
this.theirTeam = value.theirTeam;
|
||||
|
||||
if (!("games" in value))
|
||||
throw new TypeError("struct must contain games");
|
||||
if (!Array.isArray(value.games))
|
||||
throw new TypeError("struct must contain games as array");
|
||||
this.#games = value.games.map((g) => new Game(g));
|
||||
for (let g of this.#games)
|
||||
if (g.result.winner === null)
|
||||
throw new Error("past games must be finished");
|
||||
|
||||
if (typeof value.currentGame !== "object")
|
||||
throw new TypeError("struct must contain currentGame as object");
|
||||
if (value.currentGame !== null) {
|
||||
this.#currentGame = new Game(value.currentGame);
|
||||
if (this.#currentGame.result.winner !== null)
|
||||
throw new Error("currentGame in struct mustnot be finished");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,14 @@ export default function() {
|
||||
assert.strictEqual(session.theirTeam, "", "their team name");
|
||||
});
|
||||
|
||||
QUnit.test("set goal", function(assert) {
|
||||
let session = new Session();
|
||||
assert.strictEqual(session.goal, 11, "initial goal");
|
||||
session.goal = 3;
|
||||
assert.strictEqual(session.goal, 3, "changed goal");
|
||||
assert.throws(function() { session.goal = 0; }, "invalid goal");
|
||||
});
|
||||
|
||||
QUnit.test("start game", function(assert) {
|
||||
let session = new Session();
|
||||
session.anotherGame();
|
||||
@ -92,160 +100,198 @@ export default function() {
|
||||
"initial game still current");
|
||||
});
|
||||
|
||||
QUnit.test("serialization - new session", function(assert) {
|
||||
QUnit.test("toStruct - new session", function(assert) {
|
||||
let session = new Session();
|
||||
let json = session.toJSON();
|
||||
let struct = session.toStruct();
|
||||
|
||||
assert.deepEqual(
|
||||
json,
|
||||
{
|
||||
let expected = {
|
||||
goal: 11,
|
||||
ourTeam: "",
|
||||
theirTeam: "",
|
||||
games: [],
|
||||
currentGame: null,
|
||||
},
|
||||
"correct serialization");
|
||||
};
|
||||
|
||||
assert.deepEqual(struct, expected, "successfull structurizing");
|
||||
});
|
||||
|
||||
QUnit.test("serialization - finished & unfinished game", function(assert) {
|
||||
QUnit.test("toStruct - finished & unfinished game", function(assert) {
|
||||
let session = new Session();
|
||||
session.goal = 3;
|
||||
session.anotherGame();
|
||||
session.currentGame.currentRound.raise(Team.We);
|
||||
session.currentGame.currentRound.winner = Team.They;
|
||||
session.anotherGame();
|
||||
session.currentGame.currentRound.winner = Team.We;
|
||||
for (
|
||||
let i = 0;
|
||||
session.currentGame !== null && i < session.currentGame.goal;
|
||||
i += 2
|
||||
)
|
||||
session.currentGame.currentRound.winner = Team.They;
|
||||
|
||||
session.goal = 15;
|
||||
session.anotherGame();
|
||||
session.currentGame.currentRound.winner = Team.They;
|
||||
for (
|
||||
let i = 0;
|
||||
session.currentGame !== null && i < session.currentGame.goal - 2;
|
||||
i += 2
|
||||
)
|
||||
session.currentGame.currentRound.winner = Team.We;
|
||||
|
||||
session.goal = 5;
|
||||
session.ourTeam = "This is us!";
|
||||
session.theirTeam = "This is them!";
|
||||
let struct = session.toStruct();
|
||||
|
||||
let json = session.toJSON();
|
||||
json.games = [];
|
||||
for (let i = 0; i < session.games.length; i++)
|
||||
json.games.push(session.games[i].toJSON());
|
||||
json.currentGame = session.currentGame.toJSON();
|
||||
|
||||
assert.deepEqual(
|
||||
json,
|
||||
{
|
||||
goal: 5,
|
||||
let finished = new Game(3);
|
||||
finished.currentRound.raise(Team.We);
|
||||
finished.currentRound.winner = Team.They;
|
||||
let unfinished = new Game(3);
|
||||
unfinished.currentRound.winner = Team.We;
|
||||
let expected = {
|
||||
goal: 3,
|
||||
ourTeam: "This is us!",
|
||||
theirTeam: "This is them!",
|
||||
games: [
|
||||
session.games[0].toJSON(),
|
||||
],
|
||||
currentGame: session.currentGame.toJSON(),
|
||||
},
|
||||
"correct serialization");
|
||||
assert.strictEqual(json.games[0].goal, 11, "first goal");
|
||||
assert.strictEqual(json.currentGame.goal, 15, "second goal");
|
||||
games: [ finished.toStruct() ],
|
||||
currentGame: unfinished.toStruct()
|
||||
};
|
||||
|
||||
assert.deepEqual(struct, expected, "successfull structurizing");
|
||||
});
|
||||
|
||||
QUnit.test("deserialization - new session", function(assert) {
|
||||
let game = new Game();
|
||||
let json = {
|
||||
goal: 11,
|
||||
QUnit.test("fromStruct - current", function(assert) {
|
||||
let orig = new Session();
|
||||
orig.goal = 3;
|
||||
orig.ourTeam = "This is us!";
|
||||
orig.theirTeam = "This is them!";
|
||||
|
||||
let copy = new Session(orig.toStruct());
|
||||
assert.strictEqual(copy.goal, orig.goal, "goals match");
|
||||
assert.strictEqual(copy.ourTeam, orig.ourTeam, "our teams match");
|
||||
assert.strictEqual(copy.theirTeam, orig.theirTeam, "their teams match");
|
||||
assert.strictEqual(
|
||||
copy.games.length, orig.games.length, "amount of past games");
|
||||
assert.strictEqual(
|
||||
copy.currentGame, orig.currentGame, "no current games");
|
||||
assert.deepEqual(copy.result, orig.result, "results match");
|
||||
|
||||
orig.anotherGame();
|
||||
orig.currentGame.currentRound.raise(Team.They);
|
||||
orig.currentGame.currentRound.winner = Team.We;
|
||||
orig.anotherGame();
|
||||
orig.currentGame.currentRound.winner = Team.They;
|
||||
|
||||
copy = new Session(orig.toStruct());
|
||||
assert.strictEqual(copy.games.length, 1, "single past game");
|
||||
assert.strictEqual(
|
||||
copy.games.length, orig.games.length, "amount of past games");
|
||||
assert.deepEqual(
|
||||
copy.games[0].toStruct(), orig.games[0].toStruct(), "past game");
|
||||
assert.deepEqual(
|
||||
copy.currentGame.toStruct(),
|
||||
orig.currentGame.toStruct(),
|
||||
"current game");
|
||||
assert.deepEqual(copy.result, orig.result, "results match");
|
||||
});
|
||||
|
||||
QUnit.test("fromStruct - invalid", function(assert) {
|
||||
let struct = {};
|
||||
function doIt(message) {
|
||||
assert.throws(function() { new Session(struct); }, message);
|
||||
}
|
||||
|
||||
let unfinished = new Game(3);
|
||||
unfinished.currentRound.winner = Team.We;
|
||||
let finished = new Game(3);
|
||||
finished.currentRound.raise(Team.We);
|
||||
finished.currentRound.winner = Team.They;
|
||||
|
||||
doIt("no goal");
|
||||
struct.goal = "3";
|
||||
doIt("string goal");
|
||||
struct.goal = Math.PI;
|
||||
doIt("non-int goal");
|
||||
struct.goal = 0;
|
||||
doIt("small goal");
|
||||
struct.goal = 3;
|
||||
|
||||
doIt("no ourTeam");
|
||||
struct.ourTeam = 5;
|
||||
doIt("number ourTeam");
|
||||
struct.ourTeam = "";
|
||||
|
||||
doIt("no theirTeam");
|
||||
struct.theirTeam = 6;
|
||||
doIt("number theirTeam");
|
||||
struct.theirTeam = "";
|
||||
|
||||
doIt("no games");
|
||||
struct.games = "nope";
|
||||
doIt("string games");
|
||||
struct.games = ["nope", "again"];
|
||||
doIt("string array games");
|
||||
struct.games = [unfinished.toStruct()];
|
||||
doIt("unfinished game in games");
|
||||
struct.games = [finished.toStruct()];
|
||||
|
||||
doIt("no currentGame");
|
||||
struct.currentGame = "nope";
|
||||
doIt("string currentGame");
|
||||
struct.currentGame = finished.toStruct();
|
||||
doIt("finished currentGame");
|
||||
struct.currentGame = unfinished.toStruct();
|
||||
|
||||
new Session(struct);
|
||||
|
||||
struct.games = [];
|
||||
struct.currentGame = null;
|
||||
new Session(struct);
|
||||
});
|
||||
|
||||
// Data Import Tests
|
||||
// =================
|
||||
//
|
||||
// The tests named "fromStruct - vXX - XXXXX" are there to ensure that
|
||||
// future versions of the `Session` class still can correctly read in the
|
||||
// structural data exported by earlier versions. This is needed to ensure
|
||||
// that the data remains usable.
|
||||
//
|
||||
// These tests work by importing an old structural object, and then
|
||||
// exporting a new one. The new one should match with how the current
|
||||
// implementation would represent the same state.
|
||||
//
|
||||
// Therefore you should not modify the `struct` variables. Instead adjust
|
||||
// the `expected` variable, to make sure the reexported data matches what
|
||||
// is now correct.
|
||||
|
||||
QUnit.test("fromStruct - v1 - new session", function(assert) {
|
||||
let struct = {
|
||||
goal: 3,
|
||||
ourTeam: "",
|
||||
theirTeam: "",
|
||||
games: [],
|
||||
currentGame: game.toJSON(),
|
||||
currentGame: null,
|
||||
};
|
||||
json.currentGame.currentRound = game.currentRound.toJSON();
|
||||
let session = new Session(struct);
|
||||
|
||||
let session = new Session(json);
|
||||
assert.strictEqual(session.goal, 11, "goal");
|
||||
assert.strictEqual(session.ourTeam, "", "our team name");
|
||||
assert.strictEqual(session.theirTeam, "", "their team name");
|
||||
assert.strictEqual(session.games.length, 0, "no past games");
|
||||
assert.deepEqual(session.currentGame.toJSON(), game.toJSON());
|
||||
let expected = {
|
||||
goal: 3,
|
||||
ourTeam: "",
|
||||
theirTeam: "",
|
||||
games: [],
|
||||
currentGame: null,
|
||||
};
|
||||
assert.deepEqual(session.toStruct(), expected, "reexport matches");
|
||||
});
|
||||
|
||||
QUnit.test("deserialization - un- and finished games", function(assert) {
|
||||
let finished = new Game(2);
|
||||
finished.currentRound.winner = Team.We;
|
||||
|
||||
QUnit.test("fromStruct - v1 - finished & unfinished", function(assert) {
|
||||
let finished = new Game(3);
|
||||
finished.currentRound.raise(Team.We);
|
||||
finished.currentRound.winner = Team.They;
|
||||
let unfinished = new Game(3);
|
||||
unfinished.currentRound.winner = Team.They;
|
||||
unfinished.currentRound.winner = Team.We;
|
||||
|
||||
let json = {
|
||||
goal: 4,
|
||||
let struct = {
|
||||
goal: 3,
|
||||
ourTeam: "This is us!",
|
||||
theirTeam: "This is them!",
|
||||
games: [finished],
|
||||
currentGame: unfinished,
|
||||
games: [ finished.toStruct() ],
|
||||
currentGame: unfinished.toStruct(),
|
||||
};
|
||||
let deso = JSON.parse(JSON.stringify(json));
|
||||
let session = new Session(deso);
|
||||
let session = new Session(struct);
|
||||
|
||||
assert.strictEqual(session.goal, 4, "goal");
|
||||
assert.strictEqual(session.ourTeam, "This is us!", "our team name");
|
||||
assert.strictEqual(session.theirTeam, "This is them!", "their team");
|
||||
assert.strictEqual(session.games.length, 1, "one past game");
|
||||
assert.deepEqual(
|
||||
session.games[0].toJSON(), finished.toJSON(), "finished game");
|
||||
assert.notStrictEqual(session.currentGame, null, "unfinished game here");
|
||||
assert.deepEqual(
|
||||
session.currentGame.toJSON(), unfinished.toJSON(), "unfinished game");
|
||||
});
|
||||
|
||||
QUnit.test("deserialization - invalid", function(assert) {
|
||||
let deso = {};
|
||||
assert.throws(function() { new Session(deso); }, "no goal");
|
||||
|
||||
deso.goal = "11";
|
||||
assert.throws(function() { new Session(deso); }, "string goal");
|
||||
|
||||
deso.goal = 11;
|
||||
assert.throws(function() { new Session(deso); }, "no ourTeam");
|
||||
|
||||
deso.ourTeam = 11;
|
||||
assert.throws(function() { new Session(deso); }, "number ourTeam");
|
||||
|
||||
deso.ourTeam = "";
|
||||
assert.throws(function() { new Session(deso); }, "no theirTeam");
|
||||
|
||||
deso.theirTeam = 11;
|
||||
assert.throws(function() { new Session(deso); }, "number theirTeam");
|
||||
|
||||
deso.theirTeam = "";
|
||||
assert.throws(function() { new Session(deso); }, "no games");
|
||||
|
||||
deso.games = null;
|
||||
assert.throws(function() { new Session(deso); }, "null games");
|
||||
|
||||
deso.games = [];
|
||||
assert.throws(function() { new Session(deso); }, "no currentGame");
|
||||
|
||||
deso.currentGame = {
|
||||
let expected = {
|
||||
goal: 3,
|
||||
rounds: [{ winner: Team.They, points: 3 }],
|
||||
currentRound: null,
|
||||
ourTeam: "This is us!",
|
||||
theirTeam: "This is them!",
|
||||
games: [ finished.toStruct() ],
|
||||
currentGame: unfinished.toStruct(),
|
||||
};
|
||||
assert.throws(function() { new Session(deso); }, "finished currentGame");
|
||||
|
||||
deso.currentGame = null;
|
||||
new Session(deso);
|
||||
|
||||
deso.games = [{
|
||||
goal: 3,
|
||||
rounds: [{ winner: Team.They, points: 2}],
|
||||
currentRound: (new Round(3, 2)).toJSON(),
|
||||
}];
|
||||
assert.throws(function() { new Session(deso); }, "unfinished past");
|
||||
assert.deepEqual(session.toStruct(), expected, "reexport matches");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user