Cover shared text import and export workflows
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
This commit is contained in:
parent
e3bc4ae3c9
commit
4eb4816937
4 changed files with 101 additions and 15 deletions
|
|
@ -52,3 +52,4 @@
|
||||||
{"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"}}
|
{"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"}}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- 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.
|
- 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.
|
||||||
|
- Les parcours `e2e/text-exchange.spec.js` vérifient les modales partagées via Tableau et Calculateur : export désactivé à vide, validation des imports, conservation des données après erreur, brouillon effacé à la réouverture, export en lecture seule et copie exacte. L'import doit persister avec ses formules, libellés et relations ; Annuler, Fermer, Échap et le fond de modale doivent fermer correctement, avec restitution du défilement.
|
||||||
- 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.
|
||||||
|
|
|
||||||
99
e2e/text-exchange.spec.js
Normal file
99
e2e/text-exchange.spec.js
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Rôle : vérifie les modales d'échange texte, les erreurs et la persistance des imports réels.
|
||||||
|
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: "exchange.json", mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify({
|
||||||
|
toolbox: { id: "exchange", name: "Text exchange", modules: [
|
||||||
|
{ id: "table", type: "table", title: "Tableau" },
|
||||||
|
{ id: "calc", type: "calculator", title: "Calculateur" },
|
||||||
|
] },
|
||||||
|
modules: { table: { rows: 2, columns: 2, cells: {} }, calc: { entries: [{ id: "original", label: "Original", value: 7, formula: "7" }] } },
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
await page.getByRole("link", { name: "Ouvrir Text exchange", exact: true }).first().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
test("table text import/export preserves formulas and labels, copies read-only text and resets cancelled drafts", async ({ page, context }) => {
|
||||||
|
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||||
|
await setup(page);
|
||||||
|
const table = page.locator('[data-module-type="table"]');
|
||||||
|
const importButton = table.getByRole("button", { name: "Importer TSV", exact: true });
|
||||||
|
await expect(importButton).toBeHidden();
|
||||||
|
await table.getByRole("button", { name: "Afficher l'import/export de Tableau", exact: true }).click();
|
||||||
|
await expect(table.getByRole("button", { name: "Exporter TSV", exact: true })).toBeDisabled();
|
||||||
|
const originalOverflow = await page.locator("body").evaluate((body) => body.style.overflow);
|
||||||
|
await importButton.click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Importer un tableau TSV", exact: true });
|
||||||
|
await expect(dialog.getByRole("textbox")).toBeFocused();
|
||||||
|
await expect(page.locator("body")).toHaveCSS("overflow", "hidden");
|
||||||
|
await dialog.getByRole("textbox").fill("cancelled");
|
||||||
|
await dialog.getByRole("button", { name: "Annuler", exact: true }).click();
|
||||||
|
await expect.poll(() => page.locator("body").evaluate((body) => body.style.overflow)).toBe(originalOverflow);
|
||||||
|
await importButton.click();
|
||||||
|
await expect(dialog.getByRole("textbox")).toHaveValue("");
|
||||||
|
await dialog.getByRole("button", { name: "Importer le TSV", exact: true }).click();
|
||||||
|
await expect(dialog.getByRole("alert")).toContainText("Le texte à importer est vide.");
|
||||||
|
await expect(dialog.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
|
||||||
|
const text = "@size: 2x2\n@columns: Quantity\tTotal\n@rows: Ore\tBars\n@tsv\n3\t=A1*2\n5\t=A2*2";
|
||||||
|
await dialog.getByRole("textbox").fill(text);
|
||||||
|
await expect(dialog.getByRole("alert")).toHaveCount(0);
|
||||||
|
await expect(dialog.getByRole("textbox")).toHaveAttribute("aria-invalid", "false");
|
||||||
|
await dialog.getByRole("button", { name: "Importer le TSV", exact: true }).click();
|
||||||
|
await expect(dialog).toBeHidden();
|
||||||
|
await expect(table.getByRole("textbox", { name: "Cellule B2", exact: true })).toHaveValue("10");
|
||||||
|
await page.reload();
|
||||||
|
await expect(table.getByRole("textbox", { name: "Intitulé de colonne A", exact: true })).toHaveValue("Quantity");
|
||||||
|
await expect(table.getByRole("textbox", { name: "Intitulé de ligne 1", exact: true })).toHaveValue("Ore");
|
||||||
|
await expect(table.getByRole("textbox", { name: "Cellule B1", exact: true })).toHaveValue("6");
|
||||||
|
await table.getByRole("button", { name: "Afficher l'import/export de Tableau", exact: true }).click();
|
||||||
|
await table.getByRole("button", { name: "Exporter TSV", exact: true }).click();
|
||||||
|
const exported = page.getByRole("dialog", { name: "Exporter un tableau TSV", exact: true });
|
||||||
|
await expect(exported.getByRole("textbox")).toHaveAttribute("readonly", "");
|
||||||
|
await expect(exported.getByRole("textbox")).toHaveValue(text);
|
||||||
|
await exported.getByRole("button", { name: "Copier le TSV", exact: true }).click();
|
||||||
|
await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe(text);
|
||||||
|
await expect(exported).toBeVisible();
|
||||||
|
await exported.getByRole("textbox").press("Escape");
|
||||||
|
await expect(exported).toBeHidden();
|
||||||
|
await expect.poll(() => page.locator("body").evaluate((body) => body.style.overflow)).toBe(originalOverflow);
|
||||||
|
await table.getByRole("button", { name: "Masquer l'import/export de Tableau", exact: true }).click();
|
||||||
|
await expect(importButton).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid calculator imports preserve existing results and a corrected import replaces them with a persistent hierarchy", async ({ page }) => {
|
||||||
|
await setup(page);
|
||||||
|
const calc = page.locator('[data-module-type="calculator"]');
|
||||||
|
await calc.getByRole("button", { name: "Afficher l'import/export de Calculateur", exact: true }).click();
|
||||||
|
const openImport = calc.getByRole("button", { name: "Importer du texte", exact: true });
|
||||||
|
await openImport.click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Importer des résultats", exact: true });
|
||||||
|
await dialog.getByRole("textbox").fill("invalid");
|
||||||
|
await dialog.getByRole("button", { name: "Importer le texte", exact: true }).click();
|
||||||
|
await expect(dialog.getByRole("alert")).toContainText("ligne calculateur invalide");
|
||||||
|
await expect(calc.locator(".calculator-entry em")).toHaveText(["Original"]);
|
||||||
|
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
|
await page.reload();
|
||||||
|
await expect(calc.locator(".calculator-entry em")).toHaveText(["Original"]);
|
||||||
|
await calc.getByRole("button", { name: "Afficher l'import/export de Calculateur", exact: true }).click();
|
||||||
|
await openImport.click();
|
||||||
|
await expect(dialog.getByRole("textbox")).toHaveValue("");
|
||||||
|
await expect(dialog.getByRole("alert")).toHaveCount(0);
|
||||||
|
const text = "@scrollResults: true\nOre: 10 | formula=10\n Bars: 20 | formula=base*2";
|
||||||
|
await dialog.getByRole("textbox").fill(text);
|
||||||
|
await dialog.getByRole("button", { name: "Importer le texte", exact: true }).click();
|
||||||
|
await expect(dialog).toBeHidden();
|
||||||
|
await expect(calc.locator(".calculator-entry em")).toHaveText(["Ore", "Bars"]);
|
||||||
|
await page.reload();
|
||||||
|
await expect(calc.locator(".calculator-entry em")).toHaveText(["Ore", "Bars"]);
|
||||||
|
await expect(calc.getByRole("button", { name: "Liste scrollable", exact: true })).toHaveAttribute("aria-pressed", "true");
|
||||||
|
await calc.getByRole("button", { name: "Afficher l'import/export de Calculateur", exact: true }).click();
|
||||||
|
await calc.getByRole("button", { name: "Exporter en format texte", exact: true }).click();
|
||||||
|
const exported = page.getByRole("dialog", { name: "Exporter des résultats", exact: true });
|
||||||
|
await expect(exported.getByRole("textbox")).toHaveValue(text);
|
||||||
|
await page.locator(".text-import-modal-root .confirm-backdrop").click({ position: { x: 10, y: 10 } });
|
||||||
|
await expect(exported).toBeHidden();
|
||||||
|
});
|
||||||
|
|
@ -106,7 +106,6 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const combosBundle = `${combosModule}\n${combosEditor}\n${combosParts}\n${combosList}\n${combosEditorState}\n${combosReorder}`;
|
const combosBundle = `${combosModule}\n${combosEditor}\n${combosParts}\n${combosList}\n${combosEditorState}\n${combosReorder}`;
|
||||||
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 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");
|
||||||
|
|
@ -165,9 +164,6 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const imagesModule = await readFile("website/src/features/toolboxes/modules/ImagesModule.jsx", "utf8");
|
const imagesModule = await readFile("website/src/features/toolboxes/modules/ImagesModule.jsx", "utf8");
|
||||||
const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8");
|
const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8");
|
||||||
const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
|
const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
|
||||||
const textImportModal = await readFile("website/src/features/toolboxes/modules/TextImportModal.jsx", "utf8");
|
|
||||||
const textExchangeActions = await readFile("website/src/features/toolboxes/modules/TextExchangeActions.jsx", "utf8");
|
|
||||||
const textExchangeHook = await readFile("website/src/features/toolboxes/modules/useTextExchange.js", "utf8");
|
|
||||||
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
||||||
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
||||||
const textImportShared = await readFile("website/src/features/toolboxes/modules/textImport/shared.js", "utf8");
|
const textImportShared = await readFile("website/src/features/toolboxes/modules/textImport/shared.js", "utf8");
|
||||||
|
|
@ -281,7 +277,6 @@ 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, /TextExchangeActions/);
|
|
||||||
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\(/);
|
||||||
|
|
@ -418,16 +413,6 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(checklistParts, /groupReorder\.startDrag/);
|
assert.match(checklistParts, /groupReorder\.startDrag/);
|
||||||
assert.match(checklistParts, /useInlineEdit/);
|
assert.match(checklistParts, /useInlineEdit/);
|
||||||
assert.match(checklistUtils, /export function moveChecklistItemToCategory/);
|
assert.match(checklistUtils, /export function moveChecklistItemToCategory/);
|
||||||
assert.match(textImportModal, /createPortal/);
|
|
||||||
assert.match(textImportModal, /lockBodyScroll/);
|
|
||||||
assert.match(textImportModal, /has-error/);
|
|
||||||
assert.match(textExchangeActions, /export function TextExchangeActions/);
|
|
||||||
assert.match(textExchangeActions, /Icon name="import"/);
|
|
||||||
assert.match(textExchangeActions, /Icon name="export"/);
|
|
||||||
assert.match(textExchangeActions, /useTextExchange/);
|
|
||||||
assert.match(textExchangeHook, /export function useTextExchange/);
|
|
||||||
assert.match(textExchangeHook, /importModuleText/);
|
|
||||||
assert.match(textExchangeHook, /exportModuleText/);
|
|
||||||
assert.match(imagesModule, /export function ImagesModule/);
|
assert.match(imagesModule, /export function ImagesModule/);
|
||||||
assert.match(imagesModule, /useGroupedReorder/);
|
assert.match(imagesModule, /useGroupedReorder/);
|
||||||
assert.match(imagesModule, /orientation: "horizontal"/);
|
assert.match(imagesModule, /orientation: "horizontal"/);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue