diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl
index 0259c83..4841ea8 100644
--- a/.beads/interactions.jsonl
+++ b/.beads/interactions.jsonl
@@ -37,3 +37,5 @@
{"id":"int-4ece504edad6e21c4829485c33215835","kind":"field_change","created_at":"2026-09-12T07:06:41.579702638Z","actor":"Shinuwa","issue_id":"sokko-g-739","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Tutorial visibility now follows toolbox identity and request transitions before commit; tour keyed by toolbox resets step and start latch. Two StrictMode behavioral tests cover identity changes, one-time flag consumption, dismissal and pending requests. check:full passes with 34 E2E; targeted set-state-in-effect lint passes."}}
{"id":"int-c7fa4c94461f676392625658508ae231","kind":"field_change","created_at":"2026-09-12T10:15:48.480122568Z","actor":"Shinuwa","issue_id":"sokko-g-5rq","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Notepad now separates page identity from focused editing, replaces the editor DOM on page/module changes, preserves selection during local echoes and applies external updates after blur without stealing focus. Removed obsolete selection helpers and timestamp-only echo detection. check:full passes with 36 E2E; global set-state-in-effect diagnostic passes."}}
{"id":"int-e79db6c1131fb3c5c57dde09917ef735","kind":"field_change","created_at":"2026-09-12T10:50:44.387707762Z","actor":"Shinuwa","issue_id":"sokko-g-8po","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Enabled app-wide StrictMode, restored recommended react-hooks/set-state-in-effect rule, documented development checks and added an effective ESLint configuration regression test. npm run check:full passed: lint, 101 Node tests, build and 36 E2E. All C10 subsets complete."}}
+{"id":"int-4d9a5305507377a0f0d42d8c0e073c0e","kind":"field_change","created_at":"2026-09-12T11:13:23.757763936Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
+{"id":"int-e86990d8d18afb8dec65036e0ab7712f","kind":"field_change","created_at":"2026-09-12T11:14:17.70653268Z","actor":"Shinuwa","issue_id":"sokko-g-usb","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Reproducible production profiling completed and documented: npm run profile:toolboxes, four disk-backed scenarios and five genuine reloads each, 117.9 MiB identical media dataset. Raw and analysis reports in ignored .local/performance. Closed media retains about118MiB extra JS heap. check:full passes lint,101 Node tests,build,40 E2E. Initial read failure handling tracked separately as P1 sokko-g-d5u; no storage/schema changes."}}
diff --git a/README.md b/README.md
index cce76d1..2e3ec61 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,23 @@ effets de bord et les nettoyages manquants. Ces contrôles supplémentaires ne
s'exécutent pas en production. Les règles recommandées React Hooks, dont
`react-hooks/set-state-in-effect`, sont actives dans ESLint.
+Pour mesurer le chargement local des toolboxes, lancer `npm run profile:toolboxes`
+avec Chromium Playwright installé (`npx playwright install chromium`). Le script
+construit la production, utilise temporairement `127.0.0.1:4173` (le port doit être
+libre) et crée ses données dans des profils Chromium temporaires sur disque,
+sans toucher aux toolboxes personnelles. Il supprime uniquement ses profils
+temporaires et ferme le navigateur et le serveur à la fin.
+
+Les quatre scénarios comparent une base vide, 40 toolboxes légères, une toolbox
+avec 160 images WebP synthétiques et les mêmes images réparties sur 40 toolboxes.
+Cinq rechargements après une navigation de chauffe mesurent l'affichage de la
+liste après lecture IndexedDB, la durée de lecture, les tâches longues et le tas
+JavaScript après GC. Le rapport brut est écrit dans
+`.local/performance/toolbox-loading.json`, hors Git. Exécuter ce profil seul, sans
+build ou tests concurrents. Ce n'est ni un budget CI, ni une mesure du démarrage
+à cache froid, ni de la mémoire totale du navigateur ; les résultats dépendent
+de la machine et ne couvrent pas le décodage des images dans une toolbox ouverte.
+
## Contenu éditable
Les contenus maintenus à la main sont regroupés dans `website/public/data`.
diff --git a/e2e/fixtures/shell-probe.js b/e2e/fixtures/shell-probe.js
new file mode 100644
index 0000000..119b0ed
--- /dev/null
+++ b/e2e/fixtures/shell-probe.js
@@ -0,0 +1,21 @@
+// Rôle : monte les vrais overlays et textes éditoriaux sous StrictMode pour tester leur cycle de vie.
+import { createElement, StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { flushSync } from "react-dom";
+import { ConfirmModal, CreateToolboxModal, NotificationToast } from "../../website/src/components/ToolboxModals.jsx";
+import { RichText } from "../../website/src/components/RichText.jsx";
+
+export function mountShellProbe(element) {
+ const root = createRoot(element);
+ const closed = [];
+ const onClose = (value) => closed.push(value);
+ function render({ confirm = false, create = false, toast = null, text = "" } = {}) {
+ flushSync(() => root.render(createElement(StrictMode, null,
+ confirm && createElement(ConfirmModal, { key: "confirm", title: "Confirmation test", message: "Continuer ?", onClose }),
+ create && createElement(CreateToolboxModal, { key: "create", onClose }),
+ toast && createElement(NotificationToast, { key: "toast", ...toast, onClose }),
+ createElement("div", { id: "editorial-text", key: "text" }, createElement(RichText, { text }))
+ )));
+ }
+ return { render, closed, unmount: () => root.unmount() };
+}
diff --git a/e2e/shell-behavior.spec.js b/e2e/shell-behavior.spec.js
new file mode 100644
index 0000000..02fc37c
--- /dev/null
+++ b/e2e/shell-behavior.spec.js
@@ -0,0 +1,96 @@
+// Rôle : couvre les durées des alertes, le nettoyage des overlays et le rendu éditorial sans inspecter leur source.
+/* global window, document, CustomEvent */
+import { test, expect } from "@playwright/test";
+import { fileURLToPath, URL } from "node:url";
+
+const probeUrl = `/@fs/${fileURLToPath(new URL("./fixtures/shell-probe.js", import.meta.url))}`;
+
+async function setup(page, probe = false) {
+ await page.goto("/");
+ await expect(page.locator(".app-shell")).toBeVisible();
+ if (probe) {
+ await page.evaluate(async (url) => {
+ const { mountShellProbe } = await import(url);
+ const element = document.createElement("div");
+ element.id = "shell-probe";
+ document.body.append(element);
+ window.shellProbe = mountShellProbe(element);
+ }, probeUrl);
+ }
+ await page.clock.install({ time: new Date("2026-09-12T12:00:00Z") });
+ await page.clock.pauseAt(new Date("2026-09-12T12:00:01Z"));
+}
+
+async function notify(page, message, durationMs) {
+ await page.evaluate((detail) => window.dispatchEvent(new CustomEvent("sokkog:notify", { detail })), { message, durationMs });
+ await expect(page.getByRole("status").filter({ hasText: message })).toBeVisible();
+}
+
+test("application alerts use short clipboard and default durations and dismiss individually", async ({ page }) => {
+ await setup(page);
+ await notify(page, "Texte copié");
+ await notify(page, "Sauvegarde terminée");
+ await page.clock.runFor(4999);
+ await expect(page.getByRole("status")).toHaveCount(2);
+ await page.clock.runFor(1);
+ await expect(page.getByRole("status")).toHaveText("Sauvegarde terminée");
+ await page.clock.runFor(114999);
+ await expect(page.getByRole("status")).toHaveCount(1);
+ await page.clock.runFor(1);
+ await expect(page.getByRole("status")).toHaveCount(0);
+
+ await notify(page, "Minimum", 0);
+ await notify(page, "Personnalisée", 2000);
+ await page.clock.runFor(999);
+ await expect(page.getByRole("status")).toHaveCount(2);
+ await page.clock.runFor(1);
+ await expect(page.getByRole("status")).toHaveText("Personnalisée");
+ await page.getByRole("button", { name: "Fermer l'alerte" }).dispatchEvent("click");
+ await expect(page.getByRole("status")).toHaveCount(0);
+ await page.clock.runFor(2000);
+ await expect(page.getByRole("status")).toHaveCount(0);
+});
+
+test("toast replacement and unmount cancel obsolete expiration callbacks under StrictMode", async ({ page }) => {
+ await setup(page, true);
+ await page.evaluate(() => window.shellProbe.render({ toast: { id: "old", message: "Ancienne", durationMs: 1000 } }));
+ await page.clock.runFor(500);
+ await page.evaluate(() => window.shellProbe.render({ toast: { id: "new", message: "Nouvelle", durationMs: 2000 } }));
+ await page.clock.runFor(1999);
+ expect(await page.evaluate(() => window.shellProbe.closed)).toEqual([]);
+ await page.clock.runFor(1);
+ expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new"]);
+ await page.evaluate(() => {
+ window.shellProbe.render({ toast: { id: "unmounted", message: "Fermée" } });
+ window.shellProbe.unmount();
+ });
+ await page.clock.runFor(120000);
+ expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new"]);
+});
+
+test("overlapping modals keep scroll locked until the last unmount under StrictMode", async ({ page }) => {
+ await setup(page, true);
+ await page.evaluate(() => window.shellProbe.render({ confirm: true, create: true }));
+ await expect(page.getByRole("dialog")).toHaveCount(2);
+ await expect(page.locator("body")).toHaveCSS("overflow-y", "hidden");
+ await page.evaluate(() => window.shellProbe.render({ create: true }));
+ await expect(page.getByRole("dialog")).toHaveCount(1);
+ await expect(page.locator("body")).toHaveCSS("overflow-y", "hidden");
+ await page.evaluate(() => window.shellProbe.unmount());
+ await expect(page.getByRole("dialog")).toHaveCount(0);
+ await expect(page.locator("body")).not.toHaveCSS("overflow-y", "hidden");
+});
+
+test("editorial text preserves line breaks and italics but never interprets arbitrary HTML", async ({ page }) => {
+ await setup(page, true);
+ await page.evaluate(() => window.shellProbe.render({ text: 'Première\nSeconde\n
' }));
+ const text = page.locator("#editorial-text");
+ await expect(text.locator("br")).toHaveCount(2);
+ await expect(text.locator("i")).toHaveText("Seconde");
+ await expect(text.locator("img, script")).toHaveCount(0);
+ await expect(text).toContainText('
');
+ await page.evaluate(() => window.shellProbe.render({ text: "Remplacé" }));
+ await expect(text).toHaveText("Remplacé");
+ await expect(text.locator("br, i")).toHaveCount(0);
+ await page.evaluate(() => window.shellProbe.unmount());
+});
diff --git a/package.json b/package.json
index 4736915..c6b9ac6 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"check": "node --check server.mjs && node --check vite.config.js && npm run lint && npm run lists:index && npm test && npm run build",
"check:full": "npm run check && npm run test:e2e",
"test": "node --test",
- "test:e2e": "playwright test"
+ "test:e2e": "playwright test",
+ "profile:toolboxes": "npm run build && node scripts/profile-toolbox-loading.mjs"
},
"dependencies": {
"@vitejs/plugin-react": "^6.0.3",
diff --git a/scripts/profile-toolbox-loading.mjs b/scripts/profile-toolbox-loading.mjs
new file mode 100644
index 0000000..58a409b
--- /dev/null
+++ b/scripts/profile-toolbox-loading.mjs
@@ -0,0 +1,184 @@
+// Rôle : mesure le démarrage du build de production avec des toolboxes synthétiques dans un navigateur isolé.
+/* global window, document, indexedDB, IDBObjectStore, MutationObserver, requestAnimationFrame */
+import assert from "node:assert/strict";
+import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
+import { cpus, platform } from "node:os";
+import { chromium } from "@playwright/test";
+import { preview } from "vite";
+
+const repetitions = 5;
+const scenarios = [
+ { name: "empty", toolboxes: 0, imagesPerToolbox: 0 },
+ { name: "40-light", toolboxes: 40, imagesPerToolbox: 0 },
+ { name: "1-heavy", toolboxes: 1, imagesPerToolbox: 160 },
+ { name: "40-media", toolboxes: 40, imagesPerToolbox: 4 }
+];
+const requested = process.argv.slice(2);
+for (const name of requested) assert.ok(scenarios.some((scenario) => scenario.name === name), `Unknown scenario: ${name}`);
+
+async function seed(page, scenario) {
+ await page.route("**/__profile-seed", (route) => route.fulfill({ contentType: "text/html", body: "
Profile seed" }));
+ await page.goto("/__profile-seed");
+ return page.evaluate(async ({ toolboxes: count, imagesPerToolbox }) => {
+ // Le PRNG fixe le contenu et la taille des images, indépendamment du regroupement en toolboxes.
+ const canvas = document.createElement("canvas");
+ canvas.width = 1024;
+ canvas.height = 768;
+ const context = canvas.getContext("2d");
+ const pixels = context.createImageData(canvas.width, canvas.height);
+ let random = 123456;
+ const boxes = [];
+ const modules = [];
+ let mediaBytes = 0;
+ for (let box = 0; box < count; box += 1) {
+ const id = `profile-${box}`;
+ const images = [];
+ for (let image = 0; image < imagesPerToolbox; image += 1) {
+ for (let pixel = 0; pixel < pixels.data.length; pixel += 4) {
+ random = (Math.imul(random, 1664525) + 1013904223) >>> 0;
+ pixels.data[pixel] = random & 255;
+ pixels.data[pixel + 1] = (random >>> 8) & 255;
+ pixels.data[pixel + 2] = (random >>> 16) & 255;
+ pixels.data[pixel + 3] = 255;
+ }
+ context.putImageData(pixels, 0, 0);
+ const dataUrl = canvas.toDataURL("image/webp", 0.85);
+ mediaBytes += dataUrl.length;
+ images.push({ id: `image-${image}`, label: `Image ${image}`, dataUrl });
+ }
+ boxes.push({
+ id, name: `Profile ${box}`, updatedAt: "2026-09-12T12:00:00.000Z",
+ modules: [{ id: "memos", type: "memos", title: "Mémos" }, { id: "images", type: "images", title: "Images" }]
+ });
+ modules.push({ key: `${id}:memos`, data: { items: [{ id: "memo", text: "Profil de chargement" }] } });
+ if (images.length) modules.push({ key: `${id}:images`, data: { images } });
+ }
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.open("sokkog", 1);
+ request.onupgradeneeded = () => {
+ request.result.createObjectStore("kv");
+ request.result.createObjectStore("modules", { keyPath: "key" });
+ };
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => {
+ const db = request.result;
+ const transaction = db.transaction(["kv", "modules"], "readwrite");
+ transaction.objectStore("kv").put(boxes, "toolboxes");
+ for (const module of modules) transaction.objectStore("modules").put(module);
+ transaction.oncomplete = () => { db.close(); resolve(); };
+ transaction.onabort = () => { db.close(); reject(transaction.error); };
+ transaction.onerror = () => reject(transaction.error);
+ };
+ });
+ return { mediaBytes, moduleCount: modules.length, imageCount: count * imagesPerToolbox };
+ }, scenario);
+}
+
+async function instrument(page, expectedCards) {
+ await page.addInitScript((count) => {
+ const metrics = { timeOrigin: performance.timeOrigin, reads: [], longTasks: [], readyMs: null };
+ window.toolboxProfile = metrics;
+ const observer = new MutationObserver(checkReady);
+ observer.observe(document, { childList: true, subtree: true });
+ let pendingPaint = false;
+ function checkReady() {
+ if (pendingPaint || !metrics.reads.some((read) => read.transactionMs != null)) return;
+ if (!document.querySelector(".toolbox-page") || document.querySelectorAll(".toolbox-card").length !== count) return;
+ pendingPaint = true;
+ observer.disconnect();
+ requestAnimationFrame(() => requestAnimationFrame(() => { metrics.readyMs = performance.now(); }));
+ }
+ const getAll = IDBObjectStore.prototype.getAll;
+ IDBObjectStore.prototype.getAll = function (...args) {
+ const start = performance.now();
+ const request = getAll.apply(this, args);
+ if (this.name === "modules") {
+ const read = { startMs: start };
+ metrics.reads.push(read);
+ request.addEventListener("success", () => {
+ read.requestMs = performance.now() - start;
+ read.entries = request.result.length;
+ });
+ request.addEventListener("error", () => { read.error = request.error?.message; });
+ this.transaction.addEventListener("abort", () => { read.error = this.transaction.error?.message; });
+ this.transaction.addEventListener("complete", () => {
+ read.transactionMs = performance.now() - start;
+ checkReady();
+ });
+ }
+ return request;
+ };
+ new PerformanceObserver((list) => {
+ for (const entry of list.getEntries()) metrics.longTasks.push({ startMs: entry.startTime, durationMs: entry.duration });
+ }).observe({ type: "longtask", buffered: true });
+ }, expectedCards);
+}
+
+const server = await preview({ configFile: false, root: "website", preview: { host: "127.0.0.1", port: 4173, strictPort: true } });
+try {
+ await mkdir(".local/performance", { recursive: true });
+ const report = { date: new Date().toISOString(), platform: platform(), cpu: cpus()[0]?.model, repetitions, scenarios: [] };
+ for (const scenario of scenarios.filter((scenario) => !requested.length || requested.includes(scenario.name))) {
+ const profilePath = await mkdtemp(".local/performance/profile-");
+ let context;
+ try {
+ context = await chromium.launchPersistentContext(profilePath, { headless: true, baseURL: "http://127.0.0.1:4173", viewport: { width: 1440, height: 1000 } });
+ report.browser = context.browser().version();
+ const seedPage = await context.newPage();
+ const dataset = await seed(seedPage, scenario);
+ console.log(`Seeded ${scenario.name}: ${dataset.imageCount} images`);
+ await seedPage.close();
+ const page = await context.newPage();
+ const errors = [];
+ page.on("pageerror", (error) => errors.push(error.message));
+ await instrument(page, scenario.toolboxes);
+ const cdp = await context.newCDPSession(page);
+ await cdp.send("Performance.enable");
+ const samples = [];
+ let previousTimeOrigin;
+ // Une navigation de chauffe exclut le premier chargement des bundles des cinq mesures.
+ for (let iteration = 0; iteration <= repetitions; iteration += 1) {
+ if (iteration === 0) await page.goto("/#/toolboxes");
+ else await page.reload();
+ try {
+ await page.waitForFunction(() => window.toolboxProfile?.readyMs != null);
+ } catch (error) {
+ console.error(scenario.name, iteration, errors, JSON.stringify(await page.evaluate(() => ({
+ metrics: window.toolboxProfile, cards: document.querySelectorAll(".toolbox-card").length,
+ text: document.body.innerText
+ })), null, 2));
+ throw error;
+ }
+ const sample = await page.evaluate(() => ({ ...window.toolboxProfile, cards: document.querySelectorAll(".toolbox-card").length }));
+ assert.notEqual(sample.timeOrigin, previousTimeOrigin, "Every sample must load a new document");
+ previousTimeOrigin = sample.timeOrigin;
+ assert.equal(sample.reads.length, 1, "Production must perform one initial modules read");
+ assert.equal(sample.reads[0].entries, dataset.moduleCount);
+ assert.equal(sample.cards, scenario.toolboxes);
+ assert.deepEqual(errors, []);
+ await cdp.send("HeapProfiler.collectGarbage");
+ const { metrics } = await cdp.send("Performance.getMetrics");
+ sample.jsHeapBytes = metrics.find((metric) => metric.name === "JSHeapUsedSize").value;
+ sample.longTasks = sample.longTasks.filter((task) => task.startMs < sample.readyMs);
+ if (iteration > 0) samples.push(sample);
+ }
+ const median = (values) => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)];
+ const summary = {
+ readyMedianMs: median(samples.map((sample) => sample.readyMs)),
+ readyMaxMs: Math.max(...samples.map((sample) => sample.readyMs)),
+ readMedianMs: median(samples.map((sample) => sample.reads[0].transactionMs)),
+ heapMedianMiB: median(samples.map((sample) => sample.jsHeapBytes / 1024 ** 2)),
+ longestTaskMs: Math.max(0, ...samples.flatMap((sample) => sample.longTasks.map((task) => task.durationMs)))
+ };
+ report.scenarios.push({ ...scenario, ...dataset, summary, samples });
+ console.log(scenario.name, JSON.stringify({ mediaMiB: dataset.mediaBytes / 1024 ** 2, ...summary }));
+ } finally {
+ await context?.close();
+ await rm(profilePath, { recursive: true, force: true });
+ }
+ }
+ await writeFile(".local/performance/toolbox-loading.json", `${JSON.stringify(report, null, 2)}\n`);
+ console.log("Report: .local/performance/toolbox-loading.json");
+} finally {
+ await new Promise((resolve, reject) => server.httpServer.close((error) => error ? reject(error) : resolve()));
+}
diff --git a/tests/static-app.test.mjs b/tests/static-app.test.mjs
index f7bafbb..1e37f3f 100644
--- a/tests/static-app.test.mjs
+++ b/tests/static-app.test.mjs
@@ -1,4 +1,4 @@
-// Rôle : vérifie les invariants statiques du shell, du routing et des overlays.
+// Rôle : conserve les conventions éditoriales et les assets du shell non couverts par les parcours navigateur.
import { readFile } from "node:fs/promises";
import { test } from "node:test";
import assert from "node:assert/strict";
@@ -6,67 +6,32 @@ import assert from "node:assert/strict";
test("vite entrypoint and app shell are wired", async () => {
const html = await readFile("website/index.html", "utf8");
const source = await readFile("website/src/main.jsx", "utf8");
- const appDataHook = await readFile("website/src/hooks/useAppData.js", "utf8");
const gameFiltersHook = await readFile("website/src/hooks/useGameFilters.js", "utf8");
const shell = await readFile("website/src/components/Shell.jsx", "utf8");
const overlays = await readFile("website/src/components/AppOverlays.jsx", "utf8");
- const modals = await readFile("website/src/components/ToolboxModals.jsx", "utf8");
const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8");
- const richText = await readFile("website/src/components/RichText.jsx", "utf8");
const homePage = await readFile("website/src/pages/HomePage.jsx", "utf8");
const homeOnboarding = await readFile("website/src/pages/HomeOnboardingSection.jsx", "utf8");
const aboutPage = await readFile("website/src/pages/AboutPage.jsx", "utf8");
- const libraryPage = await readFile("website/src/pages/LibraryPage.jsx", "utf8");
- const moduleRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
- const hashRouter = await readFile("website/src/router/hashRouter.js", "utf8");
const routeContent = await readFile("website/src/router/RouteContent.jsx", "utf8");
- const bodyScrollLock = await readFile("website/src/utils/bodyScrollLock.js", "utf8");
assert.match(html, /<\/div>/);
assert.match(html, /\/favicon\.ico/);
assert.match(html, /\/src\/main\.jsx/);
assert.match(source, /styles\/main\.scss/);
- assert.match(source, /components\/Shell\.jsx/);
- assert.match(source, /components\/AppOverlays\.jsx/);
- assert.match(source, /router\/RouteContent\.jsx/);
- assert.match(source, /hooks\/useAppData\.js/);
assert.match(source, /hooks\/useGameFilters\.js/);
assert.doesNotMatch(source, /DEFAULT_SITE_CONTENT/);
- assert.match(appDataHook, /export function useAppData/);
- assert.match(appDataHook, /\/data\/site\.json/);
- assert.match(appDataHook, /setSiteContentError/);
assert.match(gameFiltersHook, /export function useGameFilters/);
- assert.match(hashRouter, /export function useHashRoute/);
- assert.match(hashRouter, /export function navigate/);
- assert.match(routeContent, /export function RouteContent/);
- assert.match(routeContent, /pages\/HomePage\.jsx/);
assert.match(routeContent, /pages\/AboutPage\.jsx/);
- assert.match(routeContent, /pages\/LibraryPage\.jsx/);
- assert.match(routeContent, /route === "\/library"/);
assert.match(routeContent, /features\/games\/GameRoute\.jsx/);
- assert.match(shell, /export function Shell/);
assert.doesNotMatch(shell, /content\.navigation\.library/);
assert.match(shell, /sidebar-about-link/);
assert.match(shell, /dragon-sidebar\.png/);
assert.match(shell, /ImportButton/);
- assert.match(overlays, /export function AppOverlays/);
- assert.match(overlays, /\.\/ToolboxModals\.jsx/);
assert.match(overlays, /\.\/ImageViewer\.jsx/);
assert.match(overlays, /uid\("marker"\)/);
- assert.match(modals, /export function ConfirmModal/);
- assert.match(modals, /export function CreateToolboxModal/);
- assert.match(modals, /export function LinkToolboxModal/);
- assert.match(modals, /export function NotificationToast/);
- assert.match(modals, /durationMs = 120000/);
- assert.match(modals, /setTimeout\(\(\) => onClose\(id\), durationMs\)/);
- assert.match(source, /SHORT_NOTIFICATION_DURATION_MS = 5000/);
- assert.match(source, /getNotificationDuration/);
- assert.match(modals, /useModalScrollLock/);
assert.match(imageViewer, /export function ImageViewer/);
assert.match(imageViewer, /function openImageInNewTab/);
- assert.match(richText, /value\.split\("\\n"\)/);
- assert.match(richText, /
{
assert.match(aboutPage, /siteContent\.home\.onboarding/);
assert.match(aboutPage, /module-icon-\$\{group\.icon \|\| "notepad"\}/);
assert.match(aboutPage, /about-tool-examples/);
- assert.match(libraryPage, /export function LibraryPage/);
- assert.match(libraryPage, /LIBRARY_TOOLBOX_ID/);
- assert.match(libraryPage, /toolboxModuleComponents\.jsx/);
- assert.match(moduleRegistry, /export const TOOLBOX_MODULE_LOADERS/);
- assert.match(moduleRegistry, /taskPlanner: \(\) => import\("\.\/modules\/TaskPlannerModule\.jsx"\)/);
- assert.match(moduleRegistry, /equipmentPlanner: \(\) => import\("\.\/modules\/EquipmentPlannerModule\.jsx"\)/);
- assert.match(libraryPage, /normalizeEquipmentPlannerData/);
- assert.match(libraryPage, /summarizeEquipmentPlannerData/);
- assert.match(bodyScrollLock, /modalLocks/);
- assert.match(bodyScrollLock, /is-modal-open/);
});
test("project documentation tracks storage and feature updates", async () => {
diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs
index c52a661..946940c 100644
--- a/tests/static-toolboxes.test.mjs
+++ b/tests/static-toolboxes.test.mjs
@@ -34,7 +34,6 @@ test("toolbox storage, cards and pages are wired", async () => {
const guidedTooltipTour = await readFile("website/src/components/GuidedTooltipTour.jsx", "utf8");
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
const groupedReorderHook = await readFile("website/src/hooks/useGroupedReorder.js", "utf8");
- const inlineEditHook = await readFile("website/src/hooks/useInlineEdit.js", "utf8");
const draftFormHook = await readFile("website/src/hooks/useDraftForm.js", "utf8");
assert.match(`${indexedToolboxesHook}\n${toolboxPages}`, /indexedDbStorage\.js/);
@@ -124,15 +123,9 @@ test("toolbox storage, cards and pages are wired", async () => {
assert.match(toolboxPages, /setDrawerGameId\(""\)/);
assert.match(toolboxPages, /moduleColumns/);
assert.match(toolboxPages, /normalizeCombosData/);
- assert.match(toolboxPages, /function ToolboxView/);
- assert.match(toolboxPages, /GuidedTooltipTour/);
- assert.match(toolboxPages, /data-toolbox-tutorial-target/);
- assert.match(guidedTooltipTour, /document\.querySelector/);
assert.match(guidedTooltipTour, /--tutorial-arrow-left/);
assert.match(guidedTooltipTour, /--tutorial-arrow-top/);
assert.match(guidedTooltipTour, /data-placement/);
- assert.match(toolboxPages, /toolbox\.tutorial === true/);
- assert.match(toolboxPages, /tutorial: false/);
assert.doesNotMatch(toolboxPages, /usePointerReorder/);
assert.match(storageQuota, /export function StorageQuota/);
assert.match(storageQuota, /role="progressbar"/);
@@ -148,20 +141,13 @@ test("toolbox storage, cards and pages are wired", async () => {
assert.match(groupedReorderHook, /export function applyGroupedReorderOperation/);
assert.match(groupedReorderHook, /data-reorder-orientation/);
assert.match(groupedReorderHook, /operation\.sourceParentId === operation\.targetParentId/);
- assert.match(inlineEditHook, /export function useInlineEdit/);
- assert.match(inlineEditHook, /skipCommitRef/);
- assert.match(inlineEditHook, /blurOnEscape/);
- assert.match(inlineEditHook, /commitOnEnter/);
- assert.match(inlineEditHook, /getContentEditableProps/);
assert.match(draftFormHook, /export function useDraftForm/);
assert.match(draftFormHook, /getFieldProps/);
assert.match(draftFormHook, /handleSubmit/);
assert.match(draftFormHook, /preventDefault/);
- assert.match(toolboxCoverEditor, /createImageBitmap/);
assert.match(toolboxCoverEditor, /image\/webp/);
assert.match(toolboxCoverEditor, /OUTPUT_WIDTH = 620/);
assert.match(toolboxCoverEditor, /OUTPUT_HEIGHT = 248/);
- assert.match(toolboxCoverEditor, /getCoverCropSourceRect/);
assert.doesNotMatch(toolboxCoverEditor, /react-easy-crop|react-image-crop/);
});