// 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); });