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