155 lines
6.8 KiB
JavaScript
155 lines
6.8 KiB
JavaScript
// Rôle : teste les écritures confirmées, échecs successifs et imports concurrents.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createPersistenceQueue } from "../website/src/features/toolboxes/storage/persistenceQueue.js";
|
|
import { useToolboxActions } from "../website/src/features/toolboxes/useToolboxActions.js";
|
|
import { applyModuleDataValue, removeModuleDataKeys } from "../website/src/features/toolboxes/storage/optimisticModuleData.js";
|
|
|
|
function deferred() {
|
|
let resolve, reject;
|
|
const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
for (const field of ["toolboxes", "links", "moduleData", "toastPosition"]) {
|
|
for (const outcomes of [[true, true], [true, false], [false, true], [false, false]]) {
|
|
test(`${field}: confirms only successful writes ${outcomes}`, async () => {
|
|
let disk = { [field]: "A" };
|
|
const errors = [];
|
|
const queue = createPersistenceQueue(disk, () => {});
|
|
const gates = [deferred(), deferred()];
|
|
let active = 0;
|
|
const write = (index) => queue.enqueue(
|
|
(current) => ({ ...current, [field]: index ? "C" : "B" }),
|
|
async (next) => {
|
|
assert.equal(++active, 1);
|
|
try { await gates[index].promise; disk = next; }
|
|
finally { active--; }
|
|
},
|
|
(error) => errors.push(error.message)
|
|
);
|
|
const first = write(0);
|
|
const second = write(1);
|
|
assert.equal(queue.getCurrent()[field], "C");
|
|
outcomes[0] ? gates[0].resolve() : gates[0].reject(new Error("first"));
|
|
assert.equal(await first, outcomes[0]);
|
|
assert.equal(queue.getCurrent()[field], "C");
|
|
outcomes[1] ? gates[1].resolve() : gates[1].reject(new Error("second"));
|
|
assert.equal(await second, outcomes[1]);
|
|
assert.deepEqual(queue.getCurrent(), disk);
|
|
assert.equal(disk[field], outcomes[1] ? "C" : outcomes[0] ? "B" : "A");
|
|
assert.equal(errors.length, outcomes.filter((ok) => !ok).length);
|
|
});
|
|
}
|
|
}
|
|
|
|
test("failed snapshot cannot undo a later edit or reintroduce a failed earlier write", async () => {
|
|
const initial = { toolboxes: ["existing"], links: {}, moduleData: { note: "A" } };
|
|
let disk = initial;
|
|
const queue = createPersistenceQueue(initial, () => {});
|
|
const gate = deferred();
|
|
const earlier = queue.enqueue((s) => ({ ...s, moduleData: { note: "B" } }), () => gate.promise);
|
|
const snapshot = queue.enqueue(
|
|
(s) => ({ ...s, toolboxes: ["imported", ...s.toolboxes] }),
|
|
async () => { throw new Error("abort"); }
|
|
);
|
|
const edit = queue.enqueue((s) => ({ ...s, moduleData: { note: "C" } }), async (next) => { disk = next; });
|
|
gate.reject(new Error("quota"));
|
|
assert.deepEqual(await Promise.all([earlier, snapshot, edit]), [false, false, true]);
|
|
assert.deepEqual(queue.getCurrent(), { ...initial, moduleData: { note: "C" } });
|
|
assert.deepEqual(disk, queue.getCurrent());
|
|
});
|
|
|
|
test("imports merge after file reading and preserve edits queued during the transaction", async () => {
|
|
const initial = { toolboxes: [{ id: "existing", modules: [] }], links: {}, moduleData: { note: "A" } };
|
|
let disk = initial;
|
|
const queue = createPersistenceQueue(initial, () => {});
|
|
const transaction = deferred();
|
|
const fileRead = deferred();
|
|
const notices = [];
|
|
const actions = useToolboxActions({
|
|
store: {
|
|
...initial,
|
|
persistToolboxSnapshot: (apply) => queue.enqueue(apply, async (next) => {
|
|
await transaction.promise;
|
|
disk = next;
|
|
})
|
|
},
|
|
notify: (value) => notices.push(value),
|
|
setConfirmModal: (value) => assert.fail(value.message)
|
|
});
|
|
const imported = actions.importToolbox({ text: () => fileRead.promise }, "game");
|
|
await queue.enqueue((s) => ({ ...s, moduleData: { note: "B" } }), async (next) => { disk = next; });
|
|
fileRead.resolve(JSON.stringify({ toolbox: { id: "source", name: "Import", modules: [] }, modules: {} }));
|
|
await Promise.resolve();
|
|
assert.equal(notices.length, 0);
|
|
const edit = queue.enqueue((s) => ({ ...s, moduleData: { ...s.moduleData, note: "C" } }), async (next) => { disk = next; });
|
|
transaction.resolve();
|
|
const toolbox = await imported;
|
|
await edit;
|
|
assert.equal(disk.moduleData.note, "C");
|
|
assert.deepEqual(disk.toolboxes.map((t) => t.id), [toolbox.id, "existing"]);
|
|
assert.equal(disk.links.game, toolbox.id);
|
|
assert.deepEqual(queue.getCurrent(), disk);
|
|
assert.equal(notices.length, 1);
|
|
});
|
|
|
|
test("failed module removal restores the confirmed value after an earlier failed edit", async () => {
|
|
const initial = { moduleData: { note: { text: "A" }, other: { text: "Other" } } };
|
|
const queue = createPersistenceQueue(initial, () => {});
|
|
const first = queue.enqueue(
|
|
(s) => ({ ...s, moduleData: applyModuleDataValue(s.moduleData, "note", { text: "B" }) }),
|
|
async () => { throw new Error("quota"); }
|
|
);
|
|
const second = queue.enqueue(
|
|
(s) => ({ ...s, moduleData: removeModuleDataKeys(s.moduleData, ["note"]) }),
|
|
async () => { throw new Error("abort"); }
|
|
);
|
|
assert.deepEqual(await Promise.all([first, second]), [false, false]);
|
|
assert.deepEqual(queue.getCurrent(), initial);
|
|
});
|
|
|
|
test("global imports started from the same render both survive", async () => {
|
|
const initial = { toolboxes: [], links: {}, moduleData: {} };
|
|
let disk = initial;
|
|
const queue = createPersistenceQueue(initial, () => {});
|
|
const actions = useToolboxActions({
|
|
store: {
|
|
...initial,
|
|
persistToolboxSnapshot: (apply) => queue.enqueue(apply, async (next) => { disk = next; })
|
|
},
|
|
notify() {},
|
|
setConfirmModal: (value) => assert.fail(value.message)
|
|
});
|
|
const file = (id) => ({ text: async () => JSON.stringify({
|
|
toolboxes: [{ id, name: id, modules: [{ id: "note", type: "notepad" }] }],
|
|
links: { [id]: id },
|
|
modules: { [`${id}:note`]: { text: id } }
|
|
}) });
|
|
assert.deepEqual(await Promise.all([
|
|
actions.importAllToolboxes(file("one")), actions.importAllToolboxes(file("two"))
|
|
]), [true, true]);
|
|
assert.equal(disk.toolboxes.length, 2);
|
|
assert.equal(Object.keys(disk.moduleData).length, 2);
|
|
assert.deepEqual(Object.keys(disk.links).sort(), ["one", "two"]);
|
|
assert.deepEqual(queue.getCurrent(), disk);
|
|
});
|
|
|
|
test("failed toolbox deletion preserves registry links and modules together", async () => {
|
|
const initial = {
|
|
toolboxes: [{ id: "existing", modules: [{ id: "note", type: "notepad" }] }],
|
|
links: { game: "existing" },
|
|
moduleData: { "existing:note": { text: "Keep" } }
|
|
};
|
|
const queue = createPersistenceQueue(initial, () => {});
|
|
const actions = useToolboxActions({
|
|
store: {
|
|
...initial,
|
|
persistToolboxSnapshot: (apply) => queue.enqueue(apply, async () => { throw new Error("abort"); })
|
|
}
|
|
});
|
|
const deleted = actions.deleteToolbox("existing");
|
|
assert.deepEqual(queue.getCurrent(), { toolboxes: [], links: {}, moduleData: {} });
|
|
assert.equal(await deleted, false);
|
|
assert.deepEqual(queue.getCurrent(), initial);
|
|
});
|