Add planner-aware memos and unified tool configuration panels
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s

This commit is contained in:
Shinuwa 2026-10-03 15:58:33 +02:00
parent acd9b2fe11
commit 808610dcf2
53 changed files with 842 additions and 329 deletions

View file

@ -8,7 +8,7 @@ import { normalizeChecklistData } from "../website/src/features/toolboxes/storag
const route = "/#/games/aniimo/aniimos";
const storageKey = "sokkog:guide-checklists";
const controls = (page, id) => page.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: `Retirer ${id}`, exact: true }) }).locator(".guide-checklist-controls");
const controlsToggle = (page, id) => page.getByRole("button", { name: `Lien avec le guide — ${id}`, exact: true });
const controlsToggle = (page, id) => page.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: `Retirer ${id}`, exact: true }) }).getByRole("button", { name: "Configuration", exact: true });
const linkButton = (page, id) => controls(page, id).locator(".guide-checklist-link");
const activeButtons = (page) => page.locator('.guide-checklist-link[aria-pressed="true"]');
const hiddenSwitch = (page) => page.locator('.guide-checklist-controls[data-linked="true"]').getByRole("button", { name: "Masquer les cochés", exact: true });
@ -97,7 +97,7 @@ async function closeDrawer(page) {
await page.locator("#toolbox-drawer").getByRole("button", { name: "Fermer", exact: true }).click();
}
async function openControls(page, id) {
const toggle = id ? controlsToggle(page, id) : page.locator('.module-guide-button[data-linked="true"]');
const toggle = id ? controlsToggle(page, id) : page.locator('.module-configuration-button[data-guide-linked="true"]');
await expect(toggle).toBeVisible();
if (await toggle.getAttribute("aria-expanded") === "false") await toggle.click();
}
@ -117,8 +117,8 @@ async function toggleHidden(page) {
}
async function assertLink(page, id, hidden) {
await openDrawer(page);
await expect(activeButtons(page)).toHaveCount(id ? 1 : 0);
if (id) await openControls(page, id);
await expect(activeButtons(page)).toHaveCount(id ? 1 : 0);
if (id) await expect(linkButton(page, id)).toHaveAttribute("aria-pressed", "true");
if (id) await expect(hiddenSwitch(page)).toHaveAttribute("aria-pressed", String(hidden));
else await expect(hiddenSwitch(page)).toHaveCount(0);
@ -206,11 +206,11 @@ for (const width of [1280, 390]) {
await expect(pageControls.getByRole("button")).toHaveCount(0);
await openDrawer(page);
await expect(activeButtons(page)).toHaveCount(0);
await expect(page.locator(".guide-checklist-controls")).toHaveCount(3);
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
await expect(page.locator(".guide-checklist-controls select")).toHaveCount(0);
await expect(controls(page, "unrelated")).toHaveCount(0);
await expect(controlsToggle(page, "unrelated")).toHaveCount(0);
await expect(controls(page, "capture")).toBeHidden();
await expect(controlsToggle(page, "unrelated")).toHaveCount(1);
await expect(controls(page, "capture")).toHaveCount(0);
await openControls(page, "capture");
await expect(controls(page, "capture").getByRole("button", { name: "Masquer les cochés" })).toBeDisabled();
const before = await readModule(page, "forms");
@ -218,15 +218,14 @@ for (const width of [1280, 390]) {
await expect(heading).toHaveAttribute("aria-expanded", "false");
expect(await heading.evaluate((button) => button.previousElementSibling.classList.contains("module-edit-button"))).toBe(true);
await heading.press("Enter");
await expect(heading).toHaveAttribute("aria-pressed", "true");
await expect(controls(page, "forms")).toHaveAttribute("id", await heading.getAttribute("aria-controls"));
await expect(heading).toHaveAttribute("aria-expanded", "true");
await expect(page.locator(`[id="${await heading.getAttribute("aria-controls")}"]`)).toHaveClass(/module-configuration-panel/);
await linkButton(page, "forms").press("Enter");
await expect(linkButton(page, "forms")).toHaveAttribute("aria-pressed", "true");
const panel = controls(page, "forms");
await heading.press("Space");
await expect(heading).toHaveAttribute("aria-expanded", "false");
await expect(heading).toHaveAttribute("aria-pressed", "false");
await expect(heading).toHaveAttribute("data-linked", "true");
await expect(heading).toHaveAttribute("data-guide-linked", "true");
await expect(linkButton(page, "forms")).toBeHidden();
await expect(card).toHaveAttribute("data-checklist-complete", "true");
await heading.press("Enter");
@ -357,7 +356,7 @@ test("renames retain a zero-match selection; changing toolbox clears the associa
await dialog.getByRole("combobox").selectOption("other");
await dialog.getByRole("button", { name: "Valider", exact: true }).click();
await expect(activeButtons(page)).toHaveCount(0);
await expect(page.locator(".guide-checklist-controls")).toHaveCount(1);
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
expect(await page.evaluate((key) => globalThis.localStorage.getItem(key), storageKey)).toBeNull();
});
@ -398,7 +397,7 @@ test("unavailable local storage retains links in memory across navigation; overv
await page.goto("/#/toolbox/guide");
await expect(page.locator('[data-module-type="checklist"]')).toHaveCount(4);
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
await expect(page.locator(".module-guide-button")).toHaveCount(0);
await expect(page.locator('.module-configuration-button[data-guide-linked="true"]')).toHaveCount(0);
await page.goto(route);
await assertLink(page, "perfect", true);
await openDrawer(page);

View file

@ -10,14 +10,41 @@ async function seedMemosToolbox(page) {
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction(["kv", "modules"], "readwrite");
const soonReset = new Date(Date.now() + 60 * 60 * 1000);
const resetTime = `${String(soonReset.getHours()).padStart(2, "0")}:${String(soonReset.getMinutes()).padStart(2, "0")}`;
tx.objectStore("kv").put([
{ id: "memo-toolbox", name: "Toolbox mémos", modules: [{ id: "memos", type: "memos", title: "Mémos" }], moduleColumns: 1 }
{
id: "memo-toolbox",
name: "Toolbox mémos",
modules: [
{ id: "memos", type: "memos", title: "Mémos" },
{ id: "planner-one", type: "taskPlanner", title: "Urgent planner" },
{ id: "planner-two", type: "taskPlanner", title: "Other planner" }
],
moduleColumns: 1
}
], "toolboxes");
tx.objectStore("kv").put({ mhwilds: "memo-toolbox" }, "links");
tx.objectStore("modules").put({
key: "memo-toolbox:memos",
data: { items: [{ id: "memo-1", text: "Préparer les potions", color: "#f6c453" }] }
});
tx.objectStore("modules").put({
key: "memo-toolbox:planner-one",
data: {
resetTime,
tasks: [
{ id: "category-parent", title: "Raid principal", type: "unique", category: "Raid" },
{ id: "daily-task", title: "Faire le donjon", type: "daily" },
{ id: "done-task", title: "Déjà terminée", type: "daily", checked: true, checkedAt: Date.now() }
],
relations: [{ id: "relation", fromTaskId: "daily-task", toTaskId: "category-parent" }]
}
});
tx.objectStore("modules").put({
key: "memo-toolbox:planner-two",
data: { tasks: [{ id: "unique-task", title: "Ponctuelle", type: "unique" }] }
});
tx.oncomplete = () => { db.close(); resolve(); };
tx.onerror = () => reject(tx.error);
};
@ -54,3 +81,34 @@ test("shows and persists the memo ticker on linked game and full toolbox pages",
await page.goto("/#/toolboxes");
await expect(globalTicker).toHaveCount(0);
});
test("links a planner, shows its reset warning and clears the link when it is removed", async ({ page }) => {
await seedMemosToolbox(page);
await page.goto("/#/toolbox/memo-toolbox");
const memos = page.locator('[data-module-id="memos"]');
await memos.getByRole("button", { name: "Configuration", exact: true }).click();
const dataSectionBox = await memos.locator(".module-configuration-data").boundingBox();
const settingsSectionBox = await memos.locator(".module-configuration-specific").boundingBox();
expect(settingsSectionBox.y).toBeGreaterThan(dataSectionBox.y + dataSectionBox.height);
expect(settingsSectionBox.width).toBeCloseTo(dataSectionBox.width, 0);
const plannerSelect = memos.getByRole("combobox", { name: "Planificateur lié", exact: true });
await expect(plannerSelect).toHaveValue("");
await plannerSelect.selectOption("planner-one");
const globalTicker = page.locator(".global-memos-ticker");
await expect(globalTicker).toContainText("Tâche : Raid Faire le donjon expire bientôt");
await expect(globalTicker).not.toContainText("Déjà terminée");
await expect(globalTicker.locator(".memos-ticker-item.is-task-warning .ui-icon-warning")).toHaveCount(2);
const planner = page.locator('[data-module-id="planner-one"]');
await planner.getByRole("button", { name: "Retirer Urgent planner", exact: true }).click();
await page.getByRole("dialog").getByRole("button", { name: "Retirer", exact: true }).click();
await expect(planner).toHaveCount(0);
await expect(plannerSelect).toHaveValue("");
await expect(globalTicker).not.toContainText("Faire le donjon");
await page.reload();
await memos.getByRole("button", { name: "Configuration", exact: true }).click();
await expect(plannerSelect).toHaveValue("");
});

View file

@ -97,7 +97,7 @@ test("recurring resets preserve unique completions and cleanup deletes only comp
await expect(hide).toHaveAttribute("aria-pressed", "true");
await hide.click();
await expect(tool.getByRole("checkbox", { name: "Marquer non effectué Parent", exact: true })).toBeChecked();
await tool.getByRole("button", { name: "Réglages du planificateur", exact: true }).click();
await tool.getByRole("button", { name: "Configuration", exact: true }).click();
const cleanup = tool.getByRole("button", { name: "Nettoyer les tâches ponctuelles effectuées", exact: true });
await cleanup.click();
await expect(cleanup).toBeDisabled();

View file

@ -23,7 +23,7 @@ test("table text import/export preserves formulas and labels, copies read-only t
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 table.getByRole("button", { name: "Configuration", 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();
@ -49,7 +49,7 @@ test("table text import/export preserves formulas and labels, copies read-only t
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: "Configuration", 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", "");
@ -60,14 +60,15 @@ test("table text import/export preserves formulas and labels, copies read-only t
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 table.getByRole("button", { name: "Configuration", exact: true }).click();
await expect(importButton).toBeHidden();
await expect(table.getByRole("button", { name: "Configuration", exact: true })).toHaveAttribute("aria-expanded", "false");
});
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();
await calc.getByRole("button", { name: "Configuration", 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 });
@ -78,7 +79,7 @@ test("invalid calculator imports preserve existing results and a corrected impor
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 calc.getByRole("button", { name: "Configuration", exact: true }).click();
await openImport.click();
await expect(dialog.getByRole("textbox")).toHaveValue("");
await expect(dialog.getByRole("alert")).toHaveCount(0);
@ -97,7 +98,7 @@ test("invalid calculator imports preserve existing results and a corrected impor
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: "Configuration", 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);

View file

@ -52,7 +52,18 @@ test("creates at most five spaces and moves tools with the accessible selector",
await expect(page.getByRole("tab", { name: "Divers" })).toHaveAttribute("aria-selected", "true");
await page.getByRole("tab", { name: "Défaut" }).click();
await page.getByRole("combobox", { name: "Déplacer vers… — Alpha" }).selectOption({ label: "Divers" });
const alpha = page.locator("[data-module-id='a']");
await alpha.getByRole("button", { name: "Configuration", exact: true }).click();
const organization = alpha.locator(".module-configuration-organization");
const data = alpha.locator(".module-configuration-data");
await expect(organization).toBeVisible();
await expect(data).toBeVisible();
const organizationBox = await organization.boundingBox();
const dataBox = await data.boundingBox();
expect(dataBox.y).toBeCloseTo(organizationBox.y, 0);
expect(dataBox.x).toBeGreaterThan(organizationBox.x + organizationBox.width);
expect(dataBox.width).toBeCloseTo(organizationBox.width, 0);
await alpha.getByRole("combobox", { name: "Déplacer vers un autre espace", exact: true }).selectOption({ label: "Divers" });
await expect(page.locator("[data-module-id='a']")).toHaveCount(0);
await page.getByRole("tab", { name: "Divers" }).click();
await expect(page.locator("[data-module-id='a']")).toBeVisible();
@ -66,6 +77,52 @@ test("creates at most five spaces and moves tools with the accessible selector",
await expect(page.getByRole("tab", { name: "Défaut" })).toHaveAttribute("aria-selected", "true");
});
test("keeps the compact header order and isolates configuration from add and collapsed content", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 900 });
await seedToolbox(page, {
id: "module-configuration",
name: "Module configuration",
modules: [{ id: "a", type: "counters", title: "Alpha" }],
moduleOrder: { one: ["a"], two: [["a"], []] }
});
const tool = page.locator("[data-module-id='a']");
await expect(tool.locator(".module-header-actions > button")).toHaveCount(5);
expect(await tool.locator(".module-header-actions > button").evaluateAll((buttons) => buttons.map((button) => button.className.split(" ")[0]))).toEqual([
"module-scroll-button",
"module-edit-button",
"module-configuration-button",
"module-delete-button",
"module-collapse-button"
]);
const add = tool.getByRole("button", { name: "Afficher l'ajout de Alpha", exact: true });
const configuration = tool.getByRole("button", { name: "Configuration", exact: true });
await add.click();
await expect(tool.locator(".module-add-panel")).toBeVisible();
await configuration.click();
await expect(add).toHaveAttribute("aria-pressed", "false");
await expect(tool.locator(".module-add-panel")).toHaveCount(0);
const panel = tool.getByRole("region", { name: "Configuration de Alpha", exact: true });
await expect(panel).toBeVisible();
await expect(panel.getByRole("button", { name: "Importer du texte", exact: true })).toBeVisible();
const toolBox = await tool.boundingBox();
const panelBox = await panel.boundingBox();
expect(panelBox.width).toBeGreaterThan(toolBox.width - 24);
expect(panelBox.width).toBeLessThan(toolBox.width);
await tool.getByRole("button", { name: "Masquer Alpha", exact: true }).click();
await expect(panel).toHaveCount(0);
await expect(tool.locator(".module-content")).toHaveCount(0);
await configuration.click();
await expect(panel).toBeVisible();
await expect(tool.locator(".module-content")).toBeHidden();
await expect(panel.getByRole("button", { name: "Importer du texte", exact: true })).toBeVisible();
await configuration.press("Escape");
await expect(panel).toHaveCount(0);
await expect(configuration).toHaveAttribute("aria-expanded", "false");
});
test("shows the tab insertion indicator on the correct side while reordering", async ({ page }) => {
await seedToolbox(page, {
id: "reorder-spaces",