From f0797ccf14544eedbbac4b5c9a2d7b4d071232e4 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Thu, 17 Sep 2026 23:54:46 +0200 Subject: [PATCH] Complete risk-based toolbox regression coverage --- .beads/interactions.jsonl | 1 + docs/TEST_STRATEGY.md | 50 ++++++++++++++++++ e2e/basic-tools.spec.js | 90 +++++++++++++++++++++++++++++++++ e2e/task-planner.spec.js | 25 +++++++++ tests/static-toolboxes.test.mjs | 10 ---- 5 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 docs/TEST_STRATEGY.md create mode 100644 e2e/basic-tools.spec.js diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 90eec4c..49c5330 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -54,3 +54,4 @@ {"id":"int-bb9e0e4a748e0fa71f7a88e8d5f1b9c1","kind":"field_change","created_at":"2026-09-17T15:41:40.939310492Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-8ce072a6196bda5d63c77776fe0a1c14","kind":"field_change","created_at":"2026-09-17T16:06:03.481316789Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-d8c4d7fe12bbb2589779d4fe3d920ba7","kind":"field_change","created_at":"2026-09-17T20:29:55.005289442Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} +{"id":"int-3504141284535c146e3b392ad6bf43c8","kind":"field_change","created_at":"2026-09-17T21:51:54.241158283Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed agreed risk-based C06 scope on 2026-09-17: four final behavioral scenarios cover persistent Links, Counters, Checklist and planner subtree reorder; removed ten redundant implementation assertions. Coverage and optional extensions documented in docs/TEST_STRATEGY.md. npm run check:full passed: 107 Node tests, 113 development E2E and 1 production E2E. Exhaustive regex conversion and advanced permutations are explicitly outside closure scope."}} diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md new file mode 100644 index 0000000..40e7a84 --- /dev/null +++ b/docs/TEST_STRATEGY.md @@ -0,0 +1,50 @@ +# Test Strategy + +## Scope + +The risk-based completion boundary for audit C06 (2026-09-09), agreed on +2026-09-17, is regression coverage of important workflows, not elimination of +every source-text assertion. Beads task `sokko-g-zsd` tracks acceptance and status. +This document is a coverage reference, not a separate task backlog. + +An inventory found 350 `match` / `doesNotMatch` calls across the static app, +toolbox and style tests before the final batch. This counts assertion calls, +not bugs, missing scenarios or a percentage of work remaining. + +## Coverage Boundary + +| Risk | Behavioral coverage | +| --- | --- | +| Data loss, startup failure, deferred media | `e2e/persistence.spec.js`, `storage-startup.spec.js`, `deferred-media.spec.js`; Node persistence queue and image action tests | +| React lifecycle, shared editing and navigation | `e2e/editor-lifecycle.spec.js`, `inline-edit.spec.js`, `form-reorder.spec.js`, `shell-behavior.spec.js`, `session-state.spec.js` | +| Shared reorder policy and module order | `e2e/grouped-reorder.spec.js`, `module-order.spec.js` | +| Toolbox creation, import/export, text exchange | `e2e/toolbox.spec.js`, `text-exchange.spec.js`; Node toolbox module tests | +| Links: creation, copy, category removal preserving links, deletion and reload | `e2e/basic-tools.spec.js` | +| Counters: creation, signed values, reset, deletion and reload | `e2e/basic-tools.spec.js` | +| Checklist: quantities, clamping, completed category visibility and persistence | `e2e/basic-tools.spec.js` | +| Planner: editing, recurrence, prerequisites, subtree reorder and persistence | `e2e/task-planner.spec.js` | +| Representative visual states and responsive layouts | `e2e/style-layouts.spec.js`, `tool-style-states.spec.js`, `card-surfaces.spec.js`, `nds-controls.spec.js`, `nds-patterns.spec.js` | +| Production onboarding | `e2e/production/onboarding.spec.js` | + +These suites cover selected risks, not every permutation of every tool. +Public formats, product vocabulary, editorial validation, security restrictions +and deliberate architecture constraints retain static or pure-function tests. +Implementation-name assertions can be replaced when their owning code changes; +their mere presence is not a functional defect. + +## Optional Extensions + +Exhaustive advanced tool variants (calendar recurrence operations, goal graph +combinations, equipment and combo permutations), every text-exchange UI adapter, +fine calculator geometry and wholesale conversion of remaining implementation +assertions are outside this closure boundary. They are candidates when related +features change or a concrete regression demonstrates missing coverage, not +automatic follow-on batches required to finish this audit. + +## Validation + +Run targeted tests while implementing a grouped change, then `npm run check:full` +as the final gate: lint, Node tests, build, development E2E and production E2E. +Review the diff and report actual failures or limits before closing the task. +Do not introduce visual snapshots solely to prove that a page loads. +Keep detailed command logs outside tracked source and report concise results. diff --git a/e2e/basic-tools.spec.js b/e2e/basic-tools.spec.js new file mode 100644 index 0000000..cf54adf --- /dev/null +++ b/e2e/basic-tools.spec.js @@ -0,0 +1,90 @@ +// Rôle : couvre les mutations persistantes des liens, compteurs et checklists. +import { expect, test } from "@playwright/test"; +import { Buffer } from "node:buffer"; + +async function setup(page, type, data) { + await page.goto("/#/toolboxes"); + await page.getByText("Importer une toolbox", { exact: true }).locator('input[type="file"]').setInputFiles({ + name: "basic.json", mimeType: "application/json", + buffer: Buffer.from(JSON.stringify({ + toolbox: { id: "basic", name: "Basic workflows", modules: [{ id: "tool", type, title: "Tool" }] }, + modules: { tool: data }, + })), + }); + await page.getByRole("link", { name: "Ouvrir Basic workflows", exact: true }).first().click(); + const tool = page.locator(`[data-module-type="${type}"]`); + await tool.getByRole("button", { name: "Afficher l'ajout de Tool", exact: true }).click(); + return tool; +} + +test("links persist creation and copying; deleting a category preserves its links", async ({ page, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + const tool = await setup(page, "links", { links: [] }); + const form = tool.locator(".links-add-form"); + await form.getByPlaceholder("Nom du lien", { exact: true }).fill("Guide"); + await form.locator('input[type="url"]').fill("https://example.com/guide"); + await form.getByPlaceholder("Catégorie", { exact: true }).fill("Reference"); + await form.getByRole("button", { name: "Ajouter", exact: true }).click(); + const link = tool.getByRole("link", { name: /Guide/ }); + await expect(link).toHaveAttribute("href", "https://example.com/guide"); + await expect(link).toHaveAttribute("target", "_blank"); + await expect(link).toHaveAttribute("rel", /noreferrer/); + await expect(form.locator('input[type="url"]')).toHaveValue(""); + await tool.getByRole("button", { name: "Copier Guide", exact: true }).click(); + await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("https://example.com/guide"); + await page.reload(); + await expect(link).toHaveAttribute("href", "https://example.com/guide"); + await tool.getByRole("button", { name: "Supprimer la catégorie Reference", exact: true }).click(); + await expect(tool.getByRole("heading", { name: "Reference", exact: true })).toHaveCount(0); + await page.reload(); + await expect(link).toBeVisible(); + await tool.getByRole("button", { name: "Supprimer Guide", exact: true }).click(); + await page.reload(); + await expect(link).toHaveCount(0); +}); + +test("counters persist signed values and reset without affecting other counters", async ({ page }) => { + const tool = await setup(page, "counters", { counters: [{ id: "other", label: "Other", value: 9 }] }); + await tool.getByPlaceholder("Nom du compteur", { exact: true }).fill("Attempts"); + await tool.locator(".counters-add-form").getByRole("button", { name: "Ajouter", exact: true }).click(); + const item = tool.locator(".counter-item").filter({ has: page.getByRole("button", { name: "Supprimer Attempts", exact: true }) }); + await tool.getByRole("button", { name: "Incrémenter Attempts", exact: true }).click(); + await expect(item.locator("strong")).toHaveText("1"); + await tool.getByRole("button", { name: "Décrémenter Attempts", exact: true }).click({ clickCount: 2 }); + await expect(item.locator("strong")).toHaveText("-1"); + await page.reload(); + await expect(item.locator("strong")).toHaveText("-1"); + await tool.getByRole("button", { name: "Réinitialiser Attempts", exact: true }).click(); + await page.reload(); + await expect(item.locator("strong")).toHaveText("0"); + await tool.getByRole("button", { name: "Supprimer Attempts", exact: true }).click(); + await page.reload(); + await expect(tool.locator(".counter-item")).toHaveCount(1); + await expect(tool.locator(".counter-value strong")).toHaveText("9"); + await expect(tool.locator(".counter-value span")).toHaveText("Other"); +}); + +test("checklist quantities are clamped and completed categories hide without losing data", async ({ page }) => { + const tool = await setup(page, "checklist", { sections: [] }); + await tool.getByPlaceholder("Catégorie", { exact: true }).fill("Craft"); + await tool.getByPlaceholder("Nouvel item", { exact: true }).fill("Ore"); + await tool.getByRole("spinbutton", { name: "Quantité cible", exact: true }).fill("3"); + await tool.getByRole("button", { name: "Ajouter", exact: true }).click(); + const quantity = tool.getByRole("spinbutton", { name: "Quantité actuelle Ore", exact: true }); + await tool.getByRole("button", { name: "Ajouter une quantité", exact: true }).click(); + await expect(quantity).toHaveValue("1"); + await quantity.fill("99"); + await expect(quantity).toHaveValue("3"); + await tool.getByRole("radio", { name: "Cachée quand terminée", exact: true }).click(); + await expect(quantity).toHaveCount(0); + await page.reload(); + await expect(tool.getByRole("radio", { name: "Cachée quand terminée", exact: true })).toHaveAttribute("aria-checked", "true"); + await tool.getByRole("radio", { name: "Visible quand terminée", exact: true }).click(); + await expect(quantity).toHaveValue("3"); + await tool.getByRole("button", { name: "Retirer une quantité", exact: true }).click(); + await page.reload(); + await expect(quantity).toHaveValue("2"); + await tool.getByRole("button", { name: "Supprimer Ore", exact: true }).click(); + await page.reload(); + await expect(quantity).toHaveCount(0); +}); diff --git a/e2e/task-planner.spec.js b/e2e/task-planner.spec.js index 348b758..1bb3e27 100644 --- a/e2e/task-planner.spec.js +++ b/e2e/task-planner.spec.js @@ -106,3 +106,28 @@ test("recurring resets preserve unique completions and cleanup deletes only comp await expect(tool.getByRole("checkbox", { name: "Marquer effectué Child", exact: true })).toBeVisible(); await expect(tool.getByRole("checkbox", { name: "Marquer effectué Weekly", exact: true })).toBeVisible(); }); + +test("reordering a parent moves its subtree and rejects detaching a child", async ({ page }) => { + const tool = await setup(page); + await tool.getByRole("button", { name: "Réglages de la tâche Child", exact: true }).click(); + await tool.getByRole("combobox", { name: "Définir un parent", exact: true }).selectOption({ label: "Parent" }); + await tool.getByRole("button", { name: "Réglages de la tâche Child", exact: true }).click(); + async function drag(source, target) { + await tool.getByRole("button", { name: `Déplacer ${source}`, exact: true }).hover(); + await page.mouse.down(); + const box = await tool.getByRole("button", { name: `Déplacer ${target}`, exact: true }).boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height * 0.2, { steps: 5 }); + await page.mouse.up(); + } + const titles = () => tool.locator(".task-planner-title-input").evaluateAll((inputs) => inputs.map((input) => input.value)); + await drag("Weekly", "Parent"); + await expect.poll(titles).toEqual(["Weekly", "Parent", "Child"]); + await drag("Parent", "Weekly"); + await expect.poll(titles).toEqual(["Parent", "Child", "Weekly"]); + await drag("Child", "Weekly"); + await expect.poll(titles).toEqual(["Parent", "Child", "Weekly"]); + await page.reload(); + await expect.poll(titles).toEqual(["Parent", "Child", "Weekly"]); + await tool.getByRole("button", { name: "Réglages de la tâche Child", exact: true }).click(); + await expect(tool.getByRole("button", { name: "Retirer Parent", exact: true })).toBeVisible(); +}); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 368da12..c4f747f 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -415,21 +415,11 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(imageAnnotationModule, /createMarkerId/); assert.match(imageAnnotationModule, /