diff --git a/data/db.test.js b/data/db.test.js index 077e3da..5a1d39c 100644 --- a/data/db.test.js +++ b/data/db.test.js @@ -48,6 +48,7 @@ export default function() { }); }); + // close db after each test hooks.afterEach(function() { if (inst !== null) if (inst.open) diff --git a/data/session_repo.js b/data/session_repo.js new file mode 100644 index 0000000..838c00f --- /dev/null +++ b/data/session_repo.js @@ -0,0 +1,97 @@ +"use strict"; + +import Session from "../models/session.js"; +import WbDb from "./db.js"; + +/** A transaction or known type that can be turned into a transaction. + * @typedef {IDBTransaction|IDBDatabase|WbDb=} Transactable + * + * 1. If the transactable is a transaction already, it shall be used directy. + * No validation of the accessible object stores or mode should be done, the + * creator is responsible for ensuring correctness of those. + * 2. If the transactable is a database, a transaction with the provided stores + * shall be started on it. + * 3. If the transactable is an instance of `WbDb`, a transaction shall be + * started on its database. + * 4. Otherwise a transaction shall be started on the main `WbDb` instance. + */ + +/** Transform a `Transactable` into a transaction. + * + * @param {Transactable} value The `Transactable` to turn into a transaction. + * @param {string[]} stores + * The object stores to acquire if the transaction has to be started. + * @param {IDBTransactionMode=} mode The transaction mode to use. + * @returns {IDBTransaction} A usable transaction. + */ +function toTransaction(value, stores, mode) { + if (value === undefined) + value = WbDb.get(); + if (value instanceof WbDb) + value = value.db; + if (value instanceof IDBDatabase) + value = value.transaction(stores, mode); + if (!(value instanceof IDBTransaction)) + throw new TypeError("transaction must be or become a IDBTransaction"); + return value; +} + +/** Turn a request into a promise. + * + * The promise fulfills if the request succeeds, and rejects if it fails. + * + * @param {IDBRequest} request The request to wrap. + * @returns A promise linked to the request. + */ +function requestToPromise(request) { + return new Promise(function(resolve, reject) { + request.onsuccess = (req) => resolve(req.target.result); + request.onerror = reject; + }); +} + +/** Collection of static function to interact with stored sessions. */ +export default class SessionRepo { + constructor() { + throw new TypeError("SessionRepo cannot be constructed"); + } + + /** Put a session into the repository. + * + * If the passed session has no `id` set, the newly stored ID will be + * inserted back into it. + * + * @param {Session} session The session to store. + * @param {Transactable} transaction A transaction to use. + * + * @returns {Promise} A promise containing the ID of the add session. + */ + static put(session, transaction) { + if (!(session instanceof Session)) + throw new TypeError("session to put in must be an actual Session"); + transaction = toTransaction(transaction, [WbDb.OS_SESSIONS], "readwrite"); + let sessions = transaction.objectStore(WbDb.OS_SESSIONS); + + let struct = session.toStruct(); + let req = requestToPromise(sessions.put(struct)); + + if (session.id === null) + req.then((id) => session.id = id); + + return req; + } + + /** Get all sessions in the repository. + * + * @param {Transactable} transaction A transaction to use. + * + * @returns {Promise} A promise containing the stored sessions. + */ + static async getAll(transaction) { + transaction = toTransaction(transaction, [WbDb.OS_SESSIONS], "readonly"); + let sessions = transaction.objectStore(WbDb.OS_SESSIONS); + + sessions = await requestToPromise(sessions.getAll()); + return sessions.map((session) => new Session(session)); + } +} diff --git a/data/session_repo.test.js b/data/session_repo.test.js new file mode 100644 index 0000000..efed904 --- /dev/null +++ b/data/session_repo.test.js @@ -0,0 +1,132 @@ +"use strict"; + +import { Team } from "../models/round.js"; +import Session from "../models/session.js"; +import WbDb from "./db.js"; +import SessionRepo from "./session_repo.js"; + +/** The instance used for the current test. + * + * Put test instances of `WbDb` into this variable. If it is used, the + * connection is automatically closed after the test is done. That in turn + * means that the database can then be deleted immediately, thus speeding up + * the next test. + * + * @type {WbDb} + */ +let inst = null; + +/** Wait for the `WbDb.EVENT_CHANGE` to be fired on `instance`. + * + * Uses the `inst` global variable if no `instance` parameter is passed. + * + * @param {WbDb=} instance The instance to wait on. + */ +function waitForChange(instance) { + return new Promise(function(resolve) { + (instance ?? inst).addEventListener(WbDb.EVENT_CHANGE, function() { + resolve(); + }); + }); +} + +export default function() { + QUnit.module("session_repo", function(hooks) { + + // delete test database before each test + hooks.beforeEach(function() { + return new Promise(function(resolve) { + let req = indexedDB.deleteDatabase(WbDb.DB_NAME_TEST); + req.onsuccess = function() { + resolve(); + }; + }); + }); + + // close db after each test + hooks.afterEach(function() { + if (inst !== null) + if (inst.open) + inst.db.close(); + inst = null; + }); + + QUnit.test("cannot call constructor", function(assert) { + assert.throws( + function() { new SessionRepo(); }, + new TypeError("SessionRepo cannot be constructed")); + }); + + QUnit.test("initially no sessions", async function(assert) { + inst = WbDb.get(true); + await waitForChange(inst); + let sessions = await SessionRepo.getAll(inst); + assert.strictEqual(sessions.length, 0, "no sessions"); + }); + + QUnit.test("store single session", async function(assert) { + inst = WbDb.get(true); + let req = waitForChange(inst); + + let session = new Session(); + session.ourTeam = "This is us!"; + session.theirTeam = "This is them!"; + session.goal = 2; + session.anotherGame(); + session.currentGame.currentRound.winner = Team.We; + session.anotherGame(); + assert.strictEqual(session.id, null, "no initial session id"); + + await req; + let id = await SessionRepo.put(session, inst); + assert.strictEqual(session.id, id, "session id has been updated"); + + let sessions = await SessionRepo.getAll(inst); + assert.strictEqual(sessions.length, 1, "one stored session"); + assert.deepEqual( + sessions[0].toStruct(), session.toStruct(), "sessions match"); + }); + + QUnit.test("store two sessions", async function(assert) { + inst = WbDb.get(true); + let req = waitForChange(inst); + + let first = new Session(); + first.ourTeam = "Team A"; + first.theirTeam = "Team 1"; + first.goal = 2; + first.anotherGame(); + first.currentGame.currentRound.winner = Team.We; + first.anotherGame(); + + let second = new Session(); + second.ourTeam = "Team B"; + second.theirTeam = "Team 2"; + second.goal = 3; + second.anotherGame(); + second.currentGame.currentRound.raise(Team.We); + second.currentGame.currentRound.winner = Team.They; + + await req; + + let putFirst = SessionRepo.put(first, inst); + let putSecond = SessionRepo.put(second, inst); + await Promise.all([putFirst, putSecond]); + + let sessions = await SessionRepo.getAll(inst); + assert.strictEqual(sessions.length, 2, "two sessions stored"); + assert.notStrictEqual(sessions[0].id, sessions[1].id, "IDs don't match"); + + for (let session of sessions) { + let expected = null; + if (session.id === first.id) { + expected = first.toStruct(); + } else if (session.id === second.id) { + expected = second.toStruct(); + } + + assert.deepEqual(session.toStruct(), expected, "sessions match"); + } + }); + }); +} diff --git a/test.js b/test.js index 92fb8a4..d41b87d 100644 --- a/test.js +++ b/test.js @@ -6,6 +6,7 @@ import game from "./models/game.test.js"; import session from "./models/session.test.js"; import db from "./data/db.test.js"; +import session_repo from "./data/session_repo.test.js"; QUnit.module("models", function() { round(); @@ -16,4 +17,5 @@ QUnit.module("models", function() { QUnit.module("data", function() { db(); + session_repo(); });