diff --git a/models/session.js b/models/session.js index 6443962..dbe8584 100644 --- a/models/session.js +++ b/models/session.js @@ -36,6 +36,30 @@ export default class Session extends EventTarget { if (this.#id !== null) throw new Error("the ID cannot be changed if it has been set"); this.#id = value; + } + + /** The time when this session was initially created. */ + #created = new Date(); + + /** Get the time when this session was initially created. */ + get created() { + return this.#created; + } + + /** The time when this session was last updated. */ + #updated = new Date(); + + /** Get the time when this session was last updated. */ + get updated() { + return this.#updated; + } + + /** Mark this session as changed. + * + * Triggers the `Session.EVENT_CHANGE` event and sets the update time. + */ + #changed() { + this.#updated = new Date(); this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE)); } @@ -57,7 +81,7 @@ export default class Session extends EventTarget { if (!Number.isInteger(value) || value < 1) throw new RangeError("goal must be integer >= 1"); this.#goal = value; - this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE)); + this.#changed(); } /** The name or members of the "we" team. */ @@ -71,7 +95,7 @@ export default class Session extends EventTarget { /** Set the name or members of the "we" team. */ set ourTeam(value) { this.#ourTeam = value; - this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE)); + this.#changed(); } /** The name or members of the "they" team. */ @@ -85,7 +109,7 @@ export default class Session extends EventTarget { /** Set the name or members of the "they" team. */ set theirTeam(value) { this.#theirTeam = value; - this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE)); + this.#changed(); } /** The finished games. @@ -117,7 +141,7 @@ export default class Session extends EventTarget { this.#currentGame = new Game(this.goal); this.#currentGame.addEventListener( Game.EVENT_CHANGE, this.#boundHandleGameChange); - this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE)); + this.#changed(); } } @@ -149,7 +173,7 @@ export default class Session extends EventTarget { this.#games.push(this.#currentGame); this.#currentGame = null; } - this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE)); + this.#changed(); } /** #handleGameChange, but bound to this instance. */ @@ -186,6 +210,8 @@ export default class Session extends EventTarget { games: this.#games.map((g) => g.toStruct()), currentGame: this.#currentGame !== null ? this.#currentGame.toStruct() : null, + created: this.#created, + updated: this.#updated, }; if (this.#id !== null) @@ -216,7 +242,7 @@ export default class Session extends EventTarget { if (typeof value.ourTeam !== "string") throw new TypeError("struct must contain ourTeam as string"); - this.ourTeam = value.ourTeam; + this.#ourTeam = value.ourTeam; if (typeof value.theirTeam !== "string") throw new TypeError("struct must contain theirTeam as string"); @@ -240,5 +266,21 @@ export default class Session extends EventTarget { if (this.#currentGame.result.winner !== null) throw new Error("currentGame in struct must not be finished"); } + + if ("created" in value) { + if (!(value.created instanceof Date)) + throw new TypeError( + "if struct contains creation time, it must be a date"); + this.#created = value.created; + } else + this.#created = new Date("2026-02-26T22:00:00"); + + if ("updated" in value) { + if (!(value.updated instanceof Date)) + throw new TypeError( + "if struct contains update time, it must be a date"); + this.#updated = value.updated; + } else + this.#updated = new Date("2026-02-26T22:00:00"); } } diff --git a/models/session.test.js b/models/session.test.js index 2ae03b3..089d107 100644 --- a/models/session.test.js +++ b/models/session.test.js @@ -7,6 +7,7 @@ import Session from "/models/session.js"; export default function() { QUnit.module("session", function() { QUnit.test("initial state", function(assert) { + let now = new Date(); let session = new Session(); assert.strictEqual(session.goal, 11, "initial goal"); assert.strictEqual(session.games.length, 0, "no finished games"); @@ -17,6 +18,9 @@ export default function() { "initially no points"); assert.strictEqual(session.ourTeam, "", "our team name"); assert.strictEqual(session.theirTeam, "", "their team name"); + assert.true(session.created >= now, "was created after start"); + assert.true( + session.created >= session.updated, "updated at or after creation"); }); QUnit.test("invalid constructor", function(assert) { @@ -49,6 +53,7 @@ export default function() { "small goal"); assert.verifySteps(["event"], "event happened once"); + assert.true(session.updated >= session.created, "was updated"); }); QUnit.test("start game", function(assert) { @@ -131,6 +136,7 @@ export default function() { }); session.anotherGame(); assert.verifySteps(["event"], "event was triggered"); + assert.true(session.updated >= session.created, "was updated"); }); QUnit.test("game change triggers change event", function(assert) { @@ -141,6 +147,7 @@ export default function() { }); session.currentGame.currentRound.raise(Team.They); assert.verifySteps(["event"], "event was triggered"); + assert.true(session.updated >= session.created, "was updated"); }); QUnit.test("setting ID", function(assert){ @@ -152,7 +159,7 @@ export default function() { session.id = 18; assert.strictEqual(session.id, 18, "correct id"); - assert.verifySteps(["event"], "event happened"); + assert.verifySteps([], "no event happened"); }); QUnit.test("setting our team", function(assert){ @@ -165,6 +172,7 @@ export default function() { session.ourTeam = "This is us!"; assert.strictEqual(session.ourTeam, "This is us!", "correct ourTeam"); assert.verifySteps(["event"], "event happened"); + assert.true(session.updated >= session.created, "was updated"); }); QUnit.test("setting their team", function(assert){ @@ -178,6 +186,7 @@ export default function() { assert.strictEqual( session.theirTeam, "This is them!", "correct theirTeam"); assert.verifySteps(["event"], "event happened"); + assert.true(session.updated >= session.created, "was updated"); }); QUnit.test("toStruct - new session", function(assert) { @@ -190,6 +199,8 @@ export default function() { theirTeam: "", games: [], currentGame: null, + created: session.created, + updated: session.updated, }; assert.deepEqual(struct, expected, "successfull structurizing"); @@ -219,7 +230,9 @@ export default function() { ourTeam: "This is us!", theirTeam: "This is them!", games: [ finished.toStruct() ], - currentGame: unfinished.toStruct() + currentGame: unfinished.toStruct(), + created: session.created, + updated: session.updated, }; assert.deepEqual(struct, expected, "successfull structurizing"); @@ -242,6 +255,8 @@ export default function() { assert.strictEqual( copy.currentGame, orig.currentGame, "no current games"); assert.deepEqual(copy.result, orig.result, "results match"); + assert.strictEqual(copy.created, orig.created, "same creation time"); + assert.strictEqual(copy.updated, orig.updated, "same update time"); orig.anotherGame(); orig.id = 15; @@ -263,6 +278,8 @@ export default function() { orig.currentGame.toStruct(), "current game"); assert.deepEqual(copy.result, orig.result, "results match"); + assert.strictEqual(copy.created, orig.created, "same creation time"); + assert.strictEqual(copy.updated, orig.updated, "same update time"); }); QUnit.test("fromStruct - invalid", function(assert) { @@ -347,6 +364,17 @@ export default function() { new Error("currentGame in struct must not be finished")); struct.currentGame = unfinished.toStruct(); + struct.created = "2026-02-26T22:00:00"; + doIt( + "string created", + new TypeError("if struct contains creation time, it must be a date")); + struct.created = new Date(struct.created); + struct.updated = "2026-02-26T22:00:00"; + doIt( + "string updated", + new TypeError("if struct contains update time, it must be a date")); + struct.updated = new Date(struct.updated); + new Session(struct); struct.games = []; @@ -386,6 +414,8 @@ export default function() { theirTeam: "", games: [], currentGame: null, + created: new Date("2026-02-26T22:00:00"), + updated: new Date("2026-02-26T22:00:00"), }; assert.deepEqual(session.toStruct(), expected, "reexport matches"); }); @@ -412,6 +442,8 @@ export default function() { theirTeam: "This is them!", games: [ finished.toStruct() ], currentGame: unfinished.toStruct(), + created: new Date("2026-02-26T22:00:00"), + updated: new Date("2026-02-26T22:00:00"), }; assert.deepEqual(session.toStruct(), expected, "reexport matches"); }); @@ -434,6 +466,8 @@ export default function() { theirTeam: "", games: [], currentGame: null, + created: new Date("2026-02-26T22:00:00"), + updated: new Date("2026-02-26T22:00:00"), }; assert.deepEqual(session.toStruct(), expected, "reexport matches"); }); @@ -462,6 +496,66 @@ export default function() { theirTeam: "This is them!", games: [ finished.toStruct() ], currentGame: unfinished.toStruct(), + created: new Date("2026-02-26T22:00:00"), + updated: new Date("2026-02-26T22:00:00"), + }; + assert.deepEqual(session.toStruct(), expected, "reexport matches"); + }); + + QUnit.test("fromStruct - v3 - new session", function(assert) { + let struct = { + id: 23, + goal: 3, + ourTeam: "", + theirTeam: "", + games: [], + currentGame: null, + created: new Date("2026-02-26T20:05:00"), + updated: new Date("2026-02-26T20:05:00"), + }; + let session = new Session(struct); + + let expected = { + id: 23, + goal: 3, + ourTeam: "", + theirTeam: "", + games: [], + currentGame: null, + created: new Date("2026-02-26T20:05:00"), + updated: new Date("2026-02-26T20:05:00"), + }; + assert.deepEqual(session.toStruct(), expected, "reexport matches"); + }); + + QUnit.test("fromStruct - v3 - 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.We; + + let struct = { + id: 17, + goal: 3, + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [ finished.toStruct() ], + currentGame: unfinished.toStruct(), + created: new Date("2026-02-26T20:05:00"), + updated: new Date("2026-02-26T20:05:00"), + }; + let session = new Session(struct); + + let expected = { + id: 17, + goal: 3, + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [ finished.toStruct() ], + currentGame: unfinished.toStruct(), + created: new Date("2026-02-26T20:05:00"), + updated: new Date("2026-02-26T20:05:00"), }; assert.deepEqual(session.toStruct(), expected, "reexport matches"); });