Complete risk-based toolbox regression coverage
All checks were successful
Deploy Sokko G / deploy (push) Successful in 20s

This commit is contained in:
Shinuwa 2026-09-17 23:54:46 +02:00
parent a01f605d55
commit f0797ccf14
5 changed files with 166 additions and 10 deletions

View file

@ -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."}}

50
docs/TEST_STRATEGY.md Normal file
View file

@ -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.

90
e2e/basic-tools.spec.js Normal file
View file

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

View file

@ -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();
});

View file

@ -415,21 +415,11 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(imageAnnotationModule, /createMarkerId/);
assert.match(imageAnnotationModule, /<Icon name="zoom"/);
assert.match(imageAnnotationModule, /TextExchangeActions/);
assert.match(linksModule, /export function LinksModule/);
assert.match(linksModule, /TextExchangeActions/);
assert.match(linksModule, /editing &&/);
assert.match(linksModule, /normalizeUrl/);
assert.match(linksModule, /copyText/);
assert.match(linksModule, /useGroupedReorder/);
assert.match(linksModule, /useDraftForm/);
assert.match(linksModule, /linkDraft\.getFieldProps/);
assert.match(linksModule, /link-drag-handle/);
assert.match(countersModule, /export function CountersModule/);
assert.match(countersModule, /editing &&/);
assert.match(countersModule, /counter-actions/);
assert.match(countersModule, /useGroupedReorder/);
assert.match(countersModule, /useDraftForm/);
assert.match(countersModule, /counterDraft\.getFieldProps/);
assert.match(countersModule, /orientation: "horizontal"/);
assert.match(countersModule, /counter-drag-handle/);
assert.match(countersModule, /TextExchangeActions/);