Fix Notepad synchronization across pages and external updates
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s

This commit is contained in:
Shinuwa 2026-09-12 12:36:31 +02:00
parent 898fd5286f
commit b0e340efc7
7 changed files with 148 additions and 77 deletions

View file

@ -0,0 +1,45 @@
// Rôle : pilote les mises à jour locales et externes du Bloc Note réel sous StrictMode.
import { createElement, StrictMode, useState } from "react";
import { createRoot } from "react-dom/client";
import { flushSync } from "react-dom";
import { NotepadModule } from "../../website/src/features/toolboxes/modules/NotepadModule.jsx";
import { normalizeNotepadData } from "../../website/src/features/toolboxes/storage/toolboxStorage.js";
export function mountNotepadSyncProbe(element) {
const root = createRoot(element);
const writes = [];
let setInputs;
let inputs;
function Probe() {
const [value, setValue] = useState({
moduleId: "notes",
data: { activePageId: "a", pages: [
{ id: "a", title: "First", html: "<p>Alpha</p>" },
{ id: "b", title: "Second", html: "<p>Bravo</p>", drawingMode: "permanent" }
] }
});
inputs = value;
setInputs = setValue;
const context = {
getModuleData: () => value.data,
normalizeNotepadData,
uid: () => "new-page",
setModuleData: (_toolbox, moduleId, data) => {
writes.push({ moduleId, data });
setValue((current) => ({ ...current, data }));
}
};
return createElement(NotepadModule, { toolboxId: "probe", moduleId: value.moduleId, context });
}
flushSync(() => root.render(createElement(StrictMode, null, createElement(Probe))));
return {
writes,
read: () => inputs,
update: (patch) => flushSync(() => setInputs((current) => ({ ...current, ...patch }))),
patchPage: (id, patch) => flushSync(() => setInputs((current) => ({
...current, data: { ...current.data, pages: current.data.pages.map((page) => page.id === id ? { ...page, ...patch } : page) }
}))),
select: (id) => flushSync(() => setInputs((current) => ({ ...current, data: { ...current.data, activePageId: id } }))),
unmount: () => flushSync(() => root.unmount())
};
}

74
e2e/notepad-sync.spec.js Normal file
View file

@ -0,0 +1,74 @@
// Rôle : vérifie le contenu et la sélection du Bloc Note face aux changements de page et aux mises à jour externes.
/* global window, document, NodeFilter */
import { test, expect } from "@playwright/test";
import { fileURLToPath, URL } from "node:url";
const probeUrl = `/@fs/${fileURLToPath(new URL("./fixtures/notepad-sync-probe.js", import.meta.url))}`;
async function setup(page) {
await page.goto("/");
await page.evaluate(async (url) => {
const { mountNotepadSyncProbe } = await import(url);
document.getElementById("app").style.display = "none";
const element = document.createElement("div");
element.id = "notepad-probe";
document.body.prepend(element);
const button = document.createElement("button");
button.id = "outside-editor";
button.textContent = "Outside";
document.body.append(button);
window.notepadProbe = mountNotepadSyncProbe(element);
}, probeUrl);
return page.locator("#notepad-probe .notepad-editor");
}
test("focused page changes never save the old DOM into the new page or module", async ({ page }) => {
const editor = await setup(page);
await expect(editor).toHaveText("Alpha");
await editor.focus();
const content = await page.evaluate(() => {
window.notepadProbe.select("b");
return document.querySelector("#notepad-probe .notepad-editor").innerHTML;
});
expect(content).toBe("<p>Bravo</p>");
await editor.fill("Second edited");
const saved = await page.evaluate(() => window.notepadProbe.writes.at(-1));
expect(saved.data.pages.find((item) => item.id === "a").html).toBe("<p>Alpha</p>");
expect(saved.data.pages.find((item) => item.id === "b").text).toBe("Second edited");
await page.evaluate(() => window.notepadProbe.update({
moduleId: "other", data: { activePageId: "b", pages: [{ id: "b", title: "Other", html: "<p>Other module</p>" }] }
}));
await expect(editor).toHaveText("Other module");
await editor.fill("Other edited");
expect(await page.evaluate(() => window.notepadProbe.writes.at(-1).moduleId)).toBe("other");
await page.evaluate(() => window.notepadProbe.unmount());
});
test("local echoes preserve selection and external content synchronizes after blur", async ({ page }) => {
const editor = await setup(page);
await editor.fill("Local text");
await page.evaluate(() => {
const node = document.createTreeWalker(document.querySelector("#notepad-probe .notepad-editor"), NodeFilter.SHOW_TEXT).nextNode();
const range = document.createRange();
range.setStart(node, 2);
range.collapse(true);
const selection = document.getSelection();
selection.removeAllRanges();
selection.addRange(range);
window.notepadProbe.update({});
});
expect(await page.evaluate(() => document.getSelection().anchorOffset)).toBe(2);
await expect(editor).toHaveText("Local text");
const writeCount = await page.evaluate(() => window.notepadProbe.writes.length);
await page.evaluate(() => window.notepadProbe.patchPage("a", { html: "<p>External text</p>" }));
await expect(editor).toHaveText("Local text");
expect(await page.evaluate(() => document.getSelection().anchorOffset)).toBe(2);
await page.locator("#outside-editor").click();
await expect(editor).toHaveText("External text");
await expect(page.locator("#outside-editor")).toBeFocused();
expect(await page.evaluate(() => window.notepadProbe.writes.length)).toBe(writeCount);
// L'horodatage reste celui de la sauvegarde locale ; il ne suffit pas à reconnaître un écho.
await page.evaluate(() => window.notepadProbe.patchPage("a", { html: "<p>Another update</p>" }));
await expect(editor).toHaveText("Another update");
await page.evaluate(() => window.notepadProbe.unmount());
});