Cover table editing and cell selection workflows
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s

This commit is contained in:
Shinuwa 2026-09-17 17:42:09 +02:00
parent 418921934b
commit e3bc4ae3c9
4 changed files with 125 additions and 38 deletions

View file

@ -51,3 +51,4 @@
{"id":"int-20d1e0d862e6155eadf2c36eced314db","kind":"field_change","created_at":"2026-09-14T11:56:36.962825878Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-20d1e0d862e6155eadf2c36eced314db","kind":"field_change","created_at":"2026-09-14T11:56:36.962825878Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
{"id":"int-cc67f2645165e0eac9f57d71e8a34e57","kind":"field_change","created_at":"2026-09-14T12:11:10.207360007Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-cc67f2645165e0eac9f57d71e8a34e57","kind":"field_change","created_at":"2026-09-14T12:11:10.207360007Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
{"id":"int-f9245b926aa0f581b75f6b5904778e4e","kind":"field_change","created_at":"2026-09-14T13:08:47.57641261Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-f9245b926aa0f581b75f6b5904778e4e","kind":"field_change","created_at":"2026-09-14T13:08:47.57641261Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
{"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"}}

View file

@ -29,6 +29,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
- Pour le visualiseur d'images, vérifier la remise à zéro des marqueurs et des modes dessin/gomme au remplacement de l'image. - Pour le visualiseur d'images, vérifier la remise à zéro des marqueurs et des modes dessin/gomme au remplacement de l'image.
- Pour l'éditeur de couverture, vérifier les décodages concurrents, les erreurs après fermeture et la libération des bitmaps/URLs ; une image en chargement ne doit pas permettre de valider la couverture précédente. - Pour l'éditeur de couverture, vérifier les décodages concurrents, les erreurs après fermeture et la libération des bitmaps/URLs ; une image en chargement ne doit pas permettre de valider la couverture précédente.
- Les parcours `e2e/toolbox-cover.spec.js` vérifient le choix d'icône, l'enregistrement d'une couverture 620 × 248, sa persistance après rechargement et import/export, l'annulation et les fichiers illisibles. L'association à un jeu doit afficher sa couverture sans perdre l'image personnalisée, restaurée après dissociation ; choisir une icône doit supprimer la couverture personnalisée. - Les parcours `e2e/toolbox-cover.spec.js` vérifient le choix d'icône, l'enregistrement d'une couverture 620 × 248, sa persistance après rechargement et import/export, l'annulation et les fichiers illisibles. L'association à un jeu doit afficher sa couverture sans perdre l'image personnalisée, restaurée après dissociation ; choisir une icône doit supprimer la couverture personnalisée.
- Les parcours `e2e/table.spec.js` vérifient les en-têtes éditables sans changer les références des formules, le redimensionnement sans restauration des cellules supprimées, la navigation et la sélection clavier, la copie des valeurs TSV, le collage et l'effacement de blocs, le déplacement à la souris et l'insertion d'une plage dans une formule. Les valeurs et dimensions doivent survivre au rechargement.
- Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante. - Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante.
- Les parcours `e2e/calculator.spec.js` vérifient la validation des formules, l'édition parent/enfant avec recalcul et persistance, la suppression d'une branche active, le retour à un calcul racine, la réinitialisation du brouillon sans effacer les résultats, la copie pour checklist et la persistance du défilement. La réorganisation des racines doit conserver leurs descendants et refuser de changer le parent d'un enfant. - Les parcours `e2e/calculator.spec.js` vérifient la validation des formules, l'édition parent/enfant avec recalcul et persistance, la suppression d'une branche active, le retour à un calcul racine, la réinitialisation du brouillon sans effacer les résultats, la copie pour checklist et la persistance du défilement. La réorganisation des racines doit conserver leurs descendants et refuser de changer le parent d'un enfant.
- Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur. - Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur.

123
e2e/table.spec.js Normal file
View file

@ -0,0 +1,123 @@
// Rôle : vérifie les dimensions, sélections et échanges de cellules du tableau réel.
import { expect, test } from "@playwright/test";
import { Buffer } from "node:buffer";
async function setup(page) {
await page.goto("/#/toolboxes");
await page.getByText("Importer une toolbox", { exact: true }).locator('input[type="file"]').setInputFiles({
name: "table.json", mimeType: "application/json",
buffer: Buffer.from(JSON.stringify({
toolbox: { id: "table-fixture", name: "Table workflows", modules: [{ id: "grid", type: "table", title: "Tableau" }] },
modules: { grid: { rows: 3, columns: 3, cells: { A1: "2", B1: "3", C1: "=A1+B1", A2: "4", B2: "6", C3: "removed" } } },
})),
});
await page.getByRole("link", { name: "Ouvrir Table workflows", exact: true }).first().click();
const table = page.locator('[data-module-type="table"]');
await expect(table.getByRole("grid", { name: "Tableau", exact: true })).toBeVisible();
return table;
}
function cell(table, address) {
return table.getByRole("textbox", { name: `Cellule ${address}`, exact: true });
}
async function drag(page, from, to) {
await from.hover();
await page.mouse.down();
await to.hover();
await page.mouse.up();
}
test("headers retain cell references and resizing removes only out-of-bounds data", async ({ page }) => {
const table = await setup(page);
const column = table.getByRole("textbox", { name: "Intitulé de colonne A", exact: true });
const row = table.getByRole("textbox", { name: "Intitulé de ligne 1", exact: true });
await column.click();
await expect.poll(() => column.evaluate((input) => [input.selectionStart, input.selectionEnd])).toEqual([0, 1]);
await column.fill("Quantity");
await column.press("Enter");
await row.fill("Materials");
await row.press("Enter");
await column.fill("Cancelled");
await column.press("Escape");
await expect(column).toHaveValue("Quantity");
await expect(cell(table, "C1")).toHaveValue("5");
await page.reload();
await expect(column).toHaveValue("Quantity");
await expect(row).toHaveValue("Materials");
await table.getByRole("button", { name: "Supprimer la dernière ligne", exact: true }).click();
await table.getByRole("button", { name: "Supprimer la dernière colonne", exact: true }).click();
await expect(table.getByRole("textbox", { name: /^Cellule / })).toHaveCount(4);
await table.getByRole("button", { name: "Ajouter une ligne", exact: true }).click();
await table.getByRole("button", { name: "Ajouter une colonne", exact: true }).click();
await expect(cell(table, "C1")).toHaveValue("");
await expect(cell(table, "C3")).toHaveValue("");
await expect(cell(table, "A1")).toHaveValue("2");
for (let index = 0; index < 2; index += 1) {
await table.getByRole("button", { name: "Supprimer la dernière ligne", exact: true }).click();
await table.getByRole("button", { name: "Supprimer la dernière colonne", exact: true }).click();
}
await expect(table.getByRole("button", { name: "Supprimer la dernière ligne", exact: true })).toBeDisabled();
await expect(table.getByRole("button", { name: "Supprimer la dernière colonne", exact: true })).toBeDisabled();
await page.reload();
await expect(table.getByRole("textbox", { name: /^Cellule / })).toHaveCount(1);
await expect(cell(table, "A1")).toHaveValue("2");
await expect(column).toHaveValue("Quantity");
});
test("keyboard selection copies evaluated TSV, pastes blocks and clears only selected cells", async ({ page, context }) => {
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
const table = await setup(page);
await cell(table, "A1").click();
await cell(table, "A1").press("ArrowRight");
await expect(cell(table, "B1")).toBeFocused();
await cell(table, "B1").press("ArrowDown");
await expect(cell(table, "B2")).toBeFocused();
await cell(table, "B2").press("ArrowLeft");
await expect(cell(table, "A2")).toBeFocused();
await cell(table, "A2").press("ArrowUp");
await expect(cell(table, "A1")).toBeFocused();
await cell(table, "A1").press("Shift+ArrowRight");
await cell(table, "B1").press("Shift+ArrowRight");
await cell(table, "C1").press("ControlOrMeta+c");
await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("2\t3\t5");
await cell(table, "A3").click();
await cell(table, "A3").press("ControlOrMeta+v");
await expect(cell(table, "A3")).toHaveValue("2");
await expect(cell(table, "B3")).toHaveValue("3");
await expect(cell(table, "C3")).toHaveValue("5");
await cell(table, "A3").press("Delete");
await expect(cell(table, "A3")).toHaveValue("");
await expect(cell(table, "B3")).toHaveValue("");
await expect(cell(table, "C3")).toHaveValue("");
await cell(table, "A1").click();
await table.getByRole("button", { name: "Copier les valeurs", exact: true }).click();
await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("2\t3\t5\n4\t6\t\n\t\t");
await page.reload();
await expect(cell(table, "C1")).toHaveValue("5");
await expect(cell(table, "C3")).toHaveValue("");
});
test("pointer selections move blocks and insert ranges into formulas without changing source values", async ({ page }) => {
const table = await setup(page);
await drag(page, cell(table, "A2"), cell(table, "B2"));
await expect(table.locator(".table-cell-input.is-selected")).toHaveCount(2);
await drag(page, cell(table, "A2"), cell(table, "A3"));
await expect(cell(table, "A2")).toHaveValue("");
await expect(cell(table, "B2")).toHaveValue("");
await expect(cell(table, "A3")).toHaveValue("4");
await expect(cell(table, "B3")).toHaveValue("6");
await cell(table, "C2").dblclick();
await cell(table, "C2").fill("=");
await drag(page, cell(table, "A1"), cell(table, "B1"));
await expect(cell(table, "C2")).toHaveValue("=(A1+B1)");
await cell(table, "C2").press("Enter");
await expect(cell(table, "C2")).toHaveValue("5");
await expect(cell(table, "A1")).toHaveValue("2");
await expect(cell(table, "B1")).toHaveValue("3");
await page.reload();
await expect(cell(table, "C2")).toHaveValue("5");
await expect(cell(table, "A2")).toHaveValue("");
await expect(cell(table, "A3")).toHaveValue("4");
await expect(cell(table, "B3")).toHaveValue("6");
});

View file

@ -107,13 +107,6 @@ test("toolbox module registry and modules expose expected behavior", async () =>
const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8"); const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8");
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8"); const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8"); const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8");
const tableGrid = await readFile("website/src/features/toolboxes/modules/TableGrid.jsx", "utf8");
const tableGridParts = await readFile("website/src/features/toolboxes/modules/TableGridParts.jsx", "utf8");
const tableToolbar = await readFile("website/src/features/toolboxes/modules/TableToolbar.jsx", "utf8");
const tableInteractions = await readFile("website/src/features/toolboxes/modules/useTableInteractions.js", "utf8");
const tableClipboard = await readFile("website/src/features/toolboxes/modules/useTableClipboard.js", "utf8");
const tableBundle = `${tableModule}\n${tableGrid}\n${tableGridParts}\n${tableToolbar}\n${tableInteractions}\n${tableClipboard}`;
const tableUtils = await readFile("website/src/features/toolboxes/modules/tableUtils.js", "utf8");
const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8"); const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8");
const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8"); const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8");
const timerCountdown = await readFile("website/src/features/toolboxes/modules/TimerCountdown.jsx", "utf8"); const timerCountdown = await readFile("website/src/features/toolboxes/modules/TimerCountdown.jsx", "utf8");
@ -288,38 +281,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(randomizerBundle, /type="randomizer"/); assert.match(randomizerBundle, /type="randomizer"/);
assert.match(calculatorModule, /ResizeObserver/); assert.match(calculatorModule, /ResizeObserver/);
assert.match(calculatorModule, /--calculator-scroll-height/); assert.match(calculatorModule, /--calculator-scroll-height/);
assert.match(tableModule, /export function TableModule/);
assert.match(tableBundle, /Icon name="add-row"/);
assert.match(tableBundle, /Icon name="add-column"/);
assert.match(tableBundle, /Icon name="remove-row"/);
assert.match(tableBundle, /Icon name="remove-column"/);
assert.match(tableBundle, /insertFormulaReferenceSelection/);
assert.match(tableBundle, /startFormulaSelection/);
assert.match(tableBundle, /copyTsv/);
assert.match(tableBundle, /notepad-toolbar table-toolbar/);
assert.match(tableBundle, /notepad-toolbar-group/);
assert.match(tableBundle, /notepad-toolbar-button/);
assert.match(tableBundle, /selection/);
assert.match(tableBundle, /draggingSelectionRef/);
assert.match(tableBundle, /movingSelectionRef/);
assert.match(tableBundle, /copiedSelectionRef/);
assert.match(tableUtils, /moveSelectionCells/);
assert.match(tableUtils, /pasteCellBlock/);
assert.match(tableUtils, /parseTsvBlock/);
assert.match(tableGridParts, /is-move-target/);
assert.match(tableUtils, /isMultiCellSelection/);
assert.match(tableGrid, /export function TableGrid/);
assert.match(tableBundle, /handleCellKeyDown/);
assert.match(tableBundle, /clearSelection/);
assert.match(tableBundle, /event\.key === "Delete"/);
assert.match(tableBundle, /ArrowUp/);
assert.match(tableBundle, /ArrowDown/);
assert.match(tableBundle, /ArrowLeft/);
assert.match(tableBundle, /ArrowRight/);
assert.match(tableGridParts, /export function HeaderLabelInput/);
assert.match(tableGridParts, /useInlineEdit/);
assert.match(tableModule, /TextExchangeActions/); assert.match(tableModule, /TextExchangeActions/);
assert.match(tableGridParts, /event\.target\.select/);
assert.match(tableFormulaEngine, /export function evaluateTableCell/); assert.match(tableFormulaEngine, /export function evaluateTableCell/);
assert.doesNotMatch(tableFormulaEngine, /Function\(/); assert.doesNotMatch(tableFormulaEngine, /Function\(/);
assert.doesNotMatch(tableFormulaEngine, /\beval\(/); assert.doesNotMatch(tableFormulaEngine, /\beval\(/);