75 lines
3.3 KiB
JavaScript
75 lines
3.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { beforeEach, test } from "node:test";
|
|
import ts from "typescript";
|
|
|
|
// No indexedDB in node: the module falls back to its in-memory store, which is what we exercise.
|
|
const source = readFileSync(new URL("../src/player/saves.ts", import.meta.url), "utf8");
|
|
const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } });
|
|
const saves = await import(`data:text/javascript;base64,${Buffer.from(outputText).toString("base64")}`);
|
|
|
|
let remote, calls;
|
|
beforeEach(async () => {
|
|
for (const s of await saves.localAll()) await saves.localRemove(s.versionId, s.slot);
|
|
remote = new Map();
|
|
calls = [];
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
const method = init.method ?? "GET";
|
|
calls.push(`${method} ${url}`);
|
|
const m = /saves\/(\w+)$/.exec(url);
|
|
const json = (body, ok = true) => ({ ok, json: async () => body });
|
|
if (!m) return json([...remote.entries()].map(([slot, s]) => ({ slot, title: s.title, updated_at: s.updated_at })));
|
|
if (method === "PUT") {
|
|
const body = JSON.parse(init.body);
|
|
const row = { ...body, updated_at: "2030-01-01T00:00:00Z" };
|
|
remote.set(m[1], row);
|
|
return json(row);
|
|
}
|
|
return remote.has(m[1]) ? json(remote.get(m[1])) : json({}, false);
|
|
};
|
|
});
|
|
|
|
test("without the cloud nothing touches the network", async () => {
|
|
const api = saves.makeSaveApi(1, false);
|
|
await api.put("a", "t", { x: 1 });
|
|
assert.deepEqual(await api.get("a"), { x: 1 });
|
|
assert.equal((await api.list()).length, 1);
|
|
assert.deepEqual(calls, []);
|
|
await api.remove("a");
|
|
assert.equal(await api.get("a"), null);
|
|
});
|
|
|
|
test("saves are separated by game version", async () => {
|
|
await saves.makeSaveApi(1, false).put("a", "one", { v: 1 });
|
|
await saves.makeSaveApi(2, false).put("a", "two", { v: 2 });
|
|
assert.deepEqual(await saves.makeSaveApi(1, false).get("a"), { v: 1 });
|
|
assert.equal((await saves.makeSaveApi(2, false).list())[0].title, "two");
|
|
});
|
|
|
|
test("cloud put keeps a local copy and survives a server outage", async () => {
|
|
const api = saves.makeSaveApi(1, true);
|
|
await api.put("a", "t", { x: 1 });
|
|
assert.deepEqual(remote.get("a").data, { x: 1 });
|
|
globalThis.fetch = async () => { throw new Error("offline"); };
|
|
await api.put("b", "t", { y: 2 });
|
|
assert.deepEqual(await api.get("b"), { y: 2 });
|
|
assert.equal((await api.list()).length, 2);
|
|
});
|
|
|
|
test("the newer copy wins when loading", async () => {
|
|
remote.set("a", { title: "cloud", data: { from: "cloud" }, updated_at: "2031-01-01T00:00:00Z" });
|
|
await saves.localPut({ versionId: 1, slot: "a", title: "local", data: { from: "local" }, updated_at: "2020-01-01T00:00:00Z" });
|
|
assert.deepEqual(await saves.makeSaveApi(1, true).get("a"), { from: "cloud" });
|
|
assert.equal((await saves.localGet(1, "a")).title, "cloud");
|
|
});
|
|
|
|
test("export and import round-trip and reject foreign files", async () => {
|
|
await saves.makeSaveApi(3, false).put("quick", "q", { z: 3 });
|
|
const file = await saves.exportSaves();
|
|
await saves.localRemove(3, "quick");
|
|
assert.equal(await saves.importSaves(file), 1);
|
|
assert.deepEqual(await saves.makeSaveApi(3, false).get("quick"), { z: 3 });
|
|
assert.equal(await saves.importSaves(file), 0);
|
|
await assert.rejects(saves.importSaves('{"a":1}'));
|
|
});
|