sokko-g/e2e/storage-startup.spec.js
Shinuwa d0e1fd73b1
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
Defer media loading and strengthen persistence tests
2026-09-12 23:28:05 +02:00

143 lines
7.9 KiB
JavaScript

// Rôle : vérifie qu'une panne IndexedDB ne peut pas transformer les données existantes en un état vide modifiable.
/* global window, document, IDBFactory, IDBObjectStore, DOMException, Buffer */
import { test, expect } from "@playwright/test";
import { fileURLToPath, URL } from "node:url";
import { resetSokkoStorage } from "./helpers/storage.js";
const probeUrl = `/@fs/${fileURLToPath(new URL("./fixtures/storage-startup-probe.js", import.meta.url))}`;
const original = {
toolboxes: [{ id: "saved", name: "Conservée", updatedAt: "2026-09-12T12:00:00.000Z", modules: [{ id: "note", type: "memos", title: "Mémos" }] }],
links: { mhwilds: "saved" },
modules: [{ key: "saved:note", data: { items: [{ id: "memo", text: "Ne pas perdre" }] } }]
};
async function setup(page, fault) {
await resetSokkoStorage(page);
await page.evaluate(async (data) => {
const db = await import("/src/utils/indexedDbStorage.js");
await db.setToolboxSnapshot(data.toolboxes, data.links, Object.fromEntries(data.modules.map((entry) => [entry.key, entry.data])));
}, original);
await page.addInitScript((fault) => {
window.storageFault = fault;
const open = IDBFactory.prototype.open;
IDBFactory.prototype.open = function (...args) {
if (window.storageFault === "open") throw new DOMException("Ouverture indisponible", "UnknownError");
return open.apply(this, args);
};
const getAll = IDBObjectStore.prototype.getAll;
IDBObjectStore.prototype.getAll = function (...args) {
if (window.storageFault === "read" && this.name === "modules") throw new DOMException("Lecture indisponible", "UnknownError");
return getAll.apply(this, args);
};
const put = IDBObjectStore.prototype.put;
IDBObjectStore.prototype.put = function (...args) {
if (window.storageFault === "write") throw new DOMException("Écriture indisponible", "UnknownError");
return put.apply(this, args);
};
}, fault);
await page.goto("/#/toolboxes");
await page.reload();
}
async function snapshot(page) {
return page.evaluate(async () => {
const fault = window.storageFault;
window.storageFault = "";
try {
const db = await import("/src/utils/indexedDbStorage.js");
return { toolboxes: await db.getToolboxes(), links: await db.getLinks(), modules: await db.getAllModuleData() };
} finally {
window.storageFault = fault;
}
});
}
for (const fault of ["open", "read"]) {
test(`initial ${fault} failure blocks the interface and retries without changing persisted data`, async ({ page }, testInfo) => {
await page.setViewportSize(fault === "read" ? { width: 390, height: 844 } : { width: 1440, height: 1000 });
await setup(page, fault);
await expect(page.getByRole("heading", { name: "Données locales indisponibles" })).toBeVisible();
await expect(page.getByRole("alert")).toHaveText(fault === "open" ? "Ouverture indisponible" : "Lecture indisponible");
await expect(page.getByRole("button", { name: /Nouvelle toolbox|Exporter tout|Importer/ })).toHaveCount(0);
await expect(page.getByText("Aucune toolbox", { exact: true })).toHaveCount(0);
await expect(page.getByText("Quota local atteint")).toHaveCount(0);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await page.screenshot({ path: testInfo.outputPath("storage-unavailable.png"), fullPage: true });
await page.getByRole("button", { name: "Réessayer" }).click();
await expect(page.getByRole("heading", { name: "Données locales indisponibles" })).toBeVisible();
await page.evaluate(() => { window.location.hash = "/toolbox/saved"; });
await expect(page.getByRole("heading", { name: "Toolbox introuvable" })).toHaveCount(0);
await expect(page.getByRole("heading", { name: "Données locales indisponibles" })).toBeVisible();
await page.evaluate(() => { window.storageFault = ""; });
await page.getByRole("button", { name: "Réessayer" }).click();
await expect(page.locator('[data-module-type="memos"]')).toBeVisible();
await expect(page.getByText("Ne pas perdre", { exact: true }).first()).toBeVisible();
expect(await snapshot(page)).toEqual(original);
await page.evaluate(() => { window.location.hash = "/toolboxes"; });
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Exporter tout", exact: true }).click();
const download = await downloadPromise;
const stream = await download.createReadStream();
const chunks = [];
for await (const chunk of stream) chunks.push(chunk);
const exported = JSON.parse(Buffer.concat(chunks).toString());
expect(exported.toolboxes[0].id).toBe("saved");
expect(exported.modules["saved:note"].items[0].text).toBe("Ne pas perdre");
expect(await snapshot(page)).toEqual(original);
await page.getByRole("button", { name: "Nouvelle toolbox", exact: true }).click();
await page.getByRole("textbox", { name: "Nom", exact: true }).fill("Après reprise");
await page.getByRole("button", { name: "Créer", exact: true }).click();
await expect.poll(async () => (await snapshot(page)).toolboxes.length).toBe(2);
const saved = await snapshot(page);
expect(saved.toolboxes.find((toolbox) => toolbox.id === "saved").name).toBe("Conservée");
expect(saved.modules.find((entry) => entry.key === "saved:note")).toEqual(original.modules[0]);
expect(saved.links).toEqual(original.links);
});
}
test("commands reject writes, creation, imports and exports while the store is not initialized", async ({ page }) => {
await setup(page, "read");
await page.evaluate(async (url) => {
const { mountStorageStartupProbe } = await import(url);
const element = document.createElement("div");
document.body.append(element);
window.storageProbe = mountStorageStartupProbe(element);
}, probeUrl);
await expect.poll(() => page.evaluate(() => window.storageProbe.store.loadError)).toBe("Lecture indisponible");
const results = await page.evaluate(async () => {
const { store, actions } = window.storageProbe;
const forbidden = () => { throw new Error("Incomplete state must not be consumed"); };
return {
ready: store.ready,
writes: await Promise.all([
store.setToolboxes(forbidden), store.setLinks(forbidden), store.setToastPosition("left"),
store.updateModuleData("saved", "note", {}), store.removeModuleData("saved", "note"),
store.removeToolboxModuleData({ id: "saved", modules: [{ id: "note" }] }),
store.persistToolboxSnapshot(forbidden)
]),
created: actions.createToolbox("New"),
imported: await actions.importToolbox({ text: forbidden }),
importedAll: await actions.importAllToolboxes({ text: forbidden }),
exported: await actions.exportToolbox("saved"), exportedAll: await actions.exportAllToolboxes(),
notices: window.storageProbe.notices
};
});
expect(results).toEqual({ ready: false, writes: Array(7).fill(false), created: null, imported: null, importedAll: false, exported: false, exportedAll: false, notices: [] });
expect(await snapshot(page)).toEqual(original);
await page.evaluate(() => window.storageProbe.unmount());
});
test("a later write failure uses an accurate title and preserves the confirmed toolbox", async ({ page }) => {
await setup(page, "");
await expect(page.locator(".toolbox-card")).toHaveCount(1);
await page.evaluate(() => { window.storageFault = "write"; });
await page.getByRole("button", { name: "Nouvelle toolbox", exact: true }).click();
await page.getByRole("textbox", { name: "Nom", exact: true }).fill("Not saved");
await page.getByRole("button", { name: "Créer", exact: true }).click();
await expect(page.getByRole("dialog", { name: "Erreur de stockage local" })).toBeVisible();
await expect(page.getByText("Quota local atteint")).toHaveCount(0);
await page.getByRole("button", { name: "Compris", exact: true }).click();
expect(await snapshot(page)).toEqual(original);
});