// Rôle : vérifie les brouillons RNG et les changements d'image sans état périmé sous StrictMode. /* global window, document */ import { test, expect } from "@playwright/test"; import { fileURLToPath, URL } from "node:url"; const probeUrl = `/@fs/${fileURLToPath(new URL("./fixtures/editor-lifecycle-probe.js", import.meta.url))}`; async function setup(page, kind, props = {}) { await page.goto("/"); await page.evaluate(async ({ url, kind, props }) => { const { mountEditorProbe, controlImageDecoding } = await import(url); document.getElementById("app").style.display = "none"; const element = document.createElement("div"); element.id = "editor-probe"; document.body.prepend(element); if (kind === "cover" || kind === "viewer") { const canvas = document.createElement("canvas"); canvas.width = 800; canvas.height = 400; const ctx = canvas.getContext("2d"); ctx.fillStyle = "#22d3ee"; ctx.fillRect(0, 0, 800, 400); window.probeImage = canvas.toDataURL(); if (kind === "cover") { window.probeFile = await new Promise((resolve) => canvas.toBlob(resolve)); window.decoding = controlImageDecoding(); if (props.existingCover) props.existingCover = window.probeImage; else props.file = window.probeFile; } else { window.viewerImage = { dataUrl: window.probeImage, canAnnotate: true, markers: [{ id: "a", label: "First", x: 30, y: 30 }], onChangeMarkers: () => {}, onChangeDrawings: () => {} }; props.image = window.viewerImage; } } window.editorProbe = mountEditorProbe(element, kind, props); }, { url: probeUrl, kind, props }); return page.locator("#editor-probe"); } test("RNG drafts follow rate and module identity without losing unrelated edits", async ({ page }) => { const rates = [ { id: "a", label: "First", rate: "10", costPerDraw: "12" }, { id: "b", label: "Second", rate: "20", costPerDraw: "34", occurrenceValue: 2, occurrenceUnit: "perDay" } ]; const editor = await setup(page, "rng", { data: { rates, activeRateId: "a" } }); const cost = editor.getByLabel("Coût d'un tirage"); await cost.fill("999"); await page.evaluate(() => window.editorProbe.update({})); await expect(cost).toHaveValue("999"); const snapshots = await page.evaluate((rates) => { window.editorProbe.update({ data: { rates, activeRateId: "b" } }); return window.editorProbe.snapshots; }, rates); expect(snapshots.length).toBeGreaterThan(0); for (const values of snapshots) { expect(values).toContain("34"); expect(values).not.toContain("999"); } await editor.getByRole("tab", { name: "Prévisions", exact: true }).click(); await expect(editor.getByRole("combobox", { name: "Période", exact: true })).toHaveValue("perDay"); await cost.fill("777"); await page.evaluate(() => window.editorProbe.update({ moduleId: "other" })); await expect(cost).toHaveValue("34"); await cost.fill("56"); await cost.press("Tab"); expect(await page.evaluate(() => window.editorProbe.saves.at(-1).rates[1].costPerDraw)).toBe("56"); await page.evaluate(() => window.editorProbe.update({ editing: true })); const label = editor.locator('.rng-simulator-rate-form input').first(); await label.fill("Unsubmitted"); await page.evaluate(() => window.editorProbe.update({ editing: false })); await expect(label).toHaveCount(0); await page.evaluate(() => window.editorProbe.update({ editing: true })); await expect(label).toHaveValue(""); }); test("viewer resets markers and drawing tools before committing a replacement image", async ({ page }) => { const editor = await setup(page, "viewer"); const marker = editor.getByRole("textbox", { name: "Libellé du marqueur 1" }); await marker.fill("Edited"); await editor.getByTitle("Dessiner", { exact: true }).click(); await editor.getByTitle("Gommer", { exact: true }).click(); await page.evaluate(() => window.editorProbe.update({})); await expect(marker).toHaveValue("Edited"); await expect(editor.getByTitle("Gommer", { exact: true })).toHaveAttribute("aria-pressed", "true"); const snapshots = await page.evaluate(() => { window.editorProbe.update({ image: { ...window.viewerImage, drawingMode: "temporary", markers: [{ id: "b", label: "Second", x: 60, y: 60 }] } }); return window.editorProbe.snapshots; }); expect(snapshots.length).toBeGreaterThan(0); for (const values of snapshots) expect(values).toEqual(["Second"]); await expect(editor.getByTitle("Dessiner", { exact: true })).toHaveAttribute("aria-pressed", "false"); await editor.getByTitle("Dessiner", { exact: true }).click(); await expect(editor.getByTitle("Gommer", { exact: true })).toHaveAttribute("aria-pressed", "false"); await expect(editor.getByRole("button", { name: "Annotations temporaires" })).toBeVisible(); await page.evaluate(() => window.editorProbe.unmount()); }); test("cover ignores obsolete decodes and releases every bitmap and object URL", async ({ page }) => { const errors = []; page.on("pageerror", (error) => errors.push(error.message)); const editor = await setup(page, "cover"); const save = editor.getByRole("button", { name: "Valider", exact: true }); await expect(save).toBeDisabled(); expect(await page.evaluate(() => window.decoding.pending.length)).toBe(2); await page.evaluate(async () => { await window.decoding.resolve(0); await window.decoding.resolve(1); }); await expect(save).toBeEnabled(); await expect(editor.locator("img")).toBeVisible(); await editor.getByLabel("Zoom", { exact: true }).press("End"); await save.click(); expect(await page.evaluate(() => window.editorProbe.saves[0])).toMatch(/^data:image\/(webp|jpeg);base64,/); await page.evaluate(() => window.editorProbe.update({ file: window.probeFile.slice() })); await expect(save).toBeDisabled(); await expect(editor.locator("img")).toHaveCount(0); await page.evaluate(() => window.editorProbe.update({ file: window.probeFile.slice() })); await page.evaluate(async () => { await window.decoding.resolve(3); window.decoding.reject(2); }); await expect(save).toBeEnabled(); await expect(editor.locator(".toolbox-cover-editor-error")).toHaveCount(0); await page.evaluate(() => window.editorProbe.update({ file: window.probeFile.slice() })); await page.evaluate(() => window.decoding.reject(4)); await expect(save).toBeDisabled(); await expect(editor.locator(".toolbox-cover-editor-error")).toBeVisible(); await page.evaluate(() => { window.editorProbe.update({ file: window.probeFile.slice() }); window.editorProbe.unmount(); }); await page.evaluate(() => window.decoding.resolve(5)); const resources = await page.evaluate(() => { window.decoding.restore(); return { created: window.decoding.created, revoked: window.decoding.revoked, closed: window.decoding.closed }; }); expect(resources.revoked.sort()).toEqual(resources.created.sort()); expect(resources.closed.sort()).toEqual([0, 1, 3, 5]); expect(errors).toEqual([]); }); test("cover loads an existing image and clears it when the source is removed", async ({ page }) => { const editor = await setup(page, "cover", { existingCover: true }); await expect.poll(() => page.evaluate(() => window.decoding.pending.length)).toBe(2); await page.evaluate(async () => { await window.decoding.resolve(0); await window.decoding.resolve(1); }); await expect(editor.getByRole("button", { name: "Valider", exact: true })).toBeEnabled(); await page.evaluate(() => window.editorProbe.update({ existingCover: "" })); await expect(editor.locator("img")).toHaveCount(0); await expect(editor.getByRole("button", { name: "Valider", exact: true })).toBeDisabled(); const resources = await page.evaluate(() => { window.editorProbe.unmount(); window.decoding.restore(); return { created: window.decoding.created, closed: window.decoding.closed }; }); expect(resources.created).toEqual([]); expect(resources.closed.sort()).toEqual([0, 1]); });