Improve compact desktop usability and action guidance
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s

This commit is contained in:
Shinuwa 2026-10-05 21:47:16 +02:00
parent 0ac1a0974e
commit 0fc18cd8b9
24 changed files with 325 additions and 90 deletions

View file

@ -8,7 +8,7 @@ async function setup(page) {
await dialog.getByLabel("Nom").fill("Calculator workflows");
await dialog.getByRole("button", { name: "Créer", exact: true }).click();
await page.getByRole("button", { name: "Ajouter un outil", exact: true }).click();
await page.getByRole("dialog", { name: "Ajouter un outil" }).getByRole("button", { name: "Calculateur", exact: true }).click();
await page.getByRole("dialog", { name: "Ajouter un outil" }).getByRole("button", { name: "Calculateur" }).click();
return page.locator('[data-module-type="calculator"]');
}

View file

@ -18,11 +18,14 @@ async function setup(page) {
context.fillRect(0, 0, 32, 32);
const green = canvas.toDataURL();
const targetAt = Date.now() + 60000;
const currentDate = new Date();
const currentDateKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}`;
const toolboxes = [
{ id: "a", name: "Alpha", updatedAt: "2026-09-12T12:00:00Z", modules: [
{ id: "images", type: "images", title: "Images" },
{ id: "map", type: "imageAnnotation", title: "Carte" },
{ id: "timer", type: "timer", title: "Timer" }
{ id: "timer", type: "timer", title: "Timer" },
{ id: "calendar", type: "calendar", title: "Calendrier" }
] },
{ id: "b", name: "Beta", updatedAt: "2026-09-12T12:00:00Z", modules: [{ id: "images", type: "images", title: "Images" }] }
];
@ -30,6 +33,7 @@ async function setup(page) {
"a:images": { images: [{ id: "red", label: "Rouge", dataUrl: red }] },
"a:map": { image: green, markers: [{ id: "marker", x: 25, y: 50, label: "Entrée" }], drawings: { strokes: [{ id: "stroke", color: "#f6c453", width: 4, points: [{ x: 10, y: 10 }, { x: 80, y: 80 }] }] } },
"a:timer": { countdowns: [{ id: "alert", type: "duration", label: "Alerte fermée", targetAt, durationMs: 60000, alertMode: "site" }] },
"a:calendar": { viewMode: "month", events: [{ id: "current", title: "Événement persistant", startDate: currentDateKey, endDate: currentDateKey, repeat: "none", alertMode: "site", alertOffsetMinutes: [0] }] },
"b:images": { images: [{ id: "green", label: "Vert", dataUrl: green }] }
};
await db.setToolboxSnapshot(toolboxes, { mhwilds: "a" }, modules);
@ -71,12 +75,16 @@ async function exportJson(page, buttonName = "Exporter tout") {
test("closed media stays unread while site-wide timer alerts still fire", async ({ page }) => {
const seeded = await setup(page);
expect(await page.evaluate(() => window.moduleReads)).not.toEqual([]);
expect(await page.evaluate(() => window.moduleReads.every((key) => key === "a:timer"))).toBe(true);
expect(await page.evaluate(() => window.moduleReads.every((key) => ["a:timer", "a:calendar"].includes(key)))).toBe(true);
await expect(page.getByRole("status").filter({ hasText: "Événement persistant" })).toBeVisible();
await page.clock.install({ time: new Date(seeded.targetAt - 3000) });
await page.clock.pauseAt(new Date(seeded.targetAt - 2000));
await page.clock.runFor(3000);
await expect(page.getByRole("status").filter({ hasText: "Alerte fermée" })).toBeVisible();
expect(await page.evaluate(() => window.moduleReads.every((key) => key === "a:timer"))).toBe(true);
await page.clock.runFor(7000);
await expect(page.getByRole("status").filter({ hasText: "Alerte fermée" })).toBeVisible();
await expect(page.getByRole("status").filter({ hasText: "Événement persistant" })).toBeVisible();
expect(await page.evaluate(() => window.moduleReads.every((key) => ["a:timer", "a:calendar"].includes(key)))).toBe(true);
});
test("creation, import and deletion preserve unread media and global exports include it", async ({ page }) => {
@ -86,14 +94,14 @@ test("creation, import and deletion preserve unread media and global exports inc
await page.getByRole("button", { name: "Créer", exact: true }).click();
await expect.poll(async () => (await disk(page)).toolboxes.length).toBe(3);
expect((await disk(page)).modules).toMatchObject(seeded.modules);
expect(await page.evaluate(() => window.moduleReads.every((key) => key === "a:timer"))).toBe(true);
expect(await page.evaluate(() => window.moduleReads.every((key) => ["a:timer", "a:calendar"].includes(key)))).toBe(true);
await page.goto("/#/toolboxes");
await page.getByText("Importer une toolbox", { exact: true }).locator("input").setInputFiles({
name: "import.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify({ toolbox: { id: "source", name: "Imported", modules: [] }, modules: {} }))
});
await expect(page.locator(".toolbox-card")).toHaveCount(4);
expect((await disk(page)).modules).toMatchObject(seeded.modules);
expect(await page.evaluate(() => window.moduleReads.every((key) => key === "a:timer"))).toBe(true);
expect(await page.evaluate(() => window.moduleReads.every((key) => ["a:timer", "a:calendar"].includes(key)))).toBe(true);
const exported = await exportJson(page);
for (const key of ["a:images", "a:map", "b:images"]) expect(exported.modules[key]).toEqual(seeded.modules[key]);
await page.getByRole("button", { name: "Supprimer Beta", exact: true }).click();

View file

@ -137,6 +137,7 @@ test("Aniimo copies visible species or forms and round-trips through checklist i
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
const copy = page.locator(".results-copy-button");
await expect(copy).toHaveAccessibleName(/sans les variantes/);
await expect(copy).toContainText("Utiliser les résultats");
await expect(page.locator('[data-copy-target="true"]')).toHaveText("1 / 88 Aniimos");
await copyVisibleResults(page);
await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("Loufeuteau:1");
@ -319,7 +320,9 @@ for (const [gameId, category, cardClass, dataPath, dataKey, checklistTitle] of [
await seed(page, gameId, { progress: [row(label, 1)] });
await page.goto(`/#/games/${gameId}/${category}`);
await expect(page.locator(cardClass).first()).toBeVisible();
await page.locator(".results-copy-button").click();
const resultsAction = page.locator(".results-copy-button");
await expect(resultsAction).toContainText("Utiliser les résultats");
await resultsAction.click();
const resultsDialog = page.getByRole("dialog", { name: "Utiliser les résultats visibles" });
await expect(resultsDialog.getByText(checklistTitle, { exact: true })).toBeVisible();
await resultsDialog.getByRole("button", { name: "Fermer", exact: true }).click();

View file

@ -1,15 +1,36 @@
// Rôle : vérifie en navigateur que la home charge les zones principales.
import { expect, test } from "@playwright/test";
import { resetSokkoStorage } from "./helpers/storage.js";
test.beforeEach(async ({ page }) => resetSokkoStorage(page));
test("home page renders the application shell", async ({ page }) => {
await page.goto("/");
await expect(page.locator(".app-shell")).toBeVisible();
const main = page.getByRole("main");
const hero = main.locator(".hero");
await expect(main).toBeVisible();
await expect(main.getByRole("heading", { name: "Sokko G", exact: true })).toBeVisible();
await expect(main.getByRole("link", { name: "Voir les toolboxes", exact: true })).toBeVisible();
const create = hero.getByRole("button", { name: "Créer ma première toolbox", exact: true });
await expect(create).toBeVisible();
await expect(main.getByRole("link", { name: "Voir les jeux", exact: true })).toBeVisible();
await expect(page.locator(".home-stats")).toBeVisible();
await expect(page.locator(".origin-section")).toBeVisible();
await create.click();
await expect(page.getByRole("dialog", { name: "Nouvelle toolbox", exact: true })).toBeVisible();
});
test("home links to the toolbox list once a toolbox exists", async ({ page }) => {
await page.locator(".hero").getByRole("button", { name: "Créer ma première toolbox", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "Nouvelle toolbox", exact: true });
await dialog.getByRole("textbox", { name: "Nom", exact: true }).fill("Accueil contextualisé");
await dialog.getByRole("button", { name: "Créer", exact: true }).click();
await page.goto("/#/");
const link = page.getByRole("main").getByRole("link", { name: "Voir mes toolboxes", exact: true });
await expect(link).toBeVisible();
await link.click();
await expect(page).toHaveURL(/#\/toolboxes$/);
});

View file

@ -63,7 +63,8 @@ test("reduced motion keeps previews still but manual selection remains available
test("home onboarding creates the initial tools and starts the tutorial", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: content.primaryAction, exact: true }).click();
const section = page.getByRole("region", { name: content.title, exact: true });
await section.getByRole("button", { name: content.primaryAction, exact: true }).click();
const dialog = page.getByRole("dialog");
await dialog.getByRole("textbox", { name: "Nom", exact: true }).fill("Prise en main");
await dialog.getByRole("button", { name: "Créer", exact: true }).click();

View file

@ -21,26 +21,22 @@ async function setup(page, probe = false) {
await page.clock.pauseAt(new Date("2026-09-12T12:00:01Z"));
}
async function notify(page, message, durationMs) {
await page.evaluate((detail) => window.dispatchEvent(new CustomEvent("sokkog:notify", { detail })), { message, durationMs });
async function notify(page, message, options = {}) {
await page.evaluate((detail) => window.dispatchEvent(new CustomEvent("sokkog:notify", { detail })), { message, ...options });
await expect(page.getByRole("status").filter({ hasText: message })).toBeVisible();
}
test("application alerts use short clipboard and default durations and dismiss individually", async ({ page }) => {
test("ordinary alerts last six seconds while explicit and persistent durations are respected", async ({ page }) => {
await setup(page);
await notify(page, "Texte copié");
await notify(page, "Sauvegarde terminée");
await page.clock.runFor(4999);
await page.clock.runFor(5999);
await expect(page.getByRole("status")).toHaveCount(2);
await page.clock.runFor(1);
await expect(page.getByRole("status")).toHaveText("Sauvegarde terminée");
await page.clock.runFor(114999);
await expect(page.getByRole("status")).toHaveCount(1);
await page.clock.runFor(1);
await expect(page.getByRole("status")).toHaveCount(0);
await notify(page, "Minimum", 0);
await notify(page, "Personnalisée", 2000);
await notify(page, "Minimum", { durationMs: 0 });
await notify(page, "Personnalisée", { durationMs: 2000 });
await page.clock.runFor(999);
await expect(page.getByRole("status")).toHaveCount(2);
await page.clock.runFor(1);
@ -49,6 +45,17 @@ test("application alerts use short clipboard and default durations and dismiss i
await expect(page.getByRole("status")).toHaveCount(0);
await page.clock.runFor(2000);
await expect(page.getByRole("status")).toHaveCount(0);
await notify(page, "Alerte persistante", { persistent: true });
await page.clock.runFor(120000);
await expect(page.getByRole("status")).toHaveText("Alerte persistante");
await page.getByRole("button", { name: "Fermer l'alerte" }).click();
await expect(page.getByRole("status")).toHaveCount(0);
for (let index = 1; index <= 6; index += 1) await notify(page, `Pile ${index}`);
await expect(page.getByRole("status")).toHaveCount(5);
await expect(page.getByRole("status").filter({ hasText: "Pile 1" })).toHaveCount(0);
await expect(page.getByRole("status").filter({ hasText: "Pile 6" })).toBeVisible();
});
test("toast replacement and unmount cancel obsolete expiration callbacks under StrictMode", async ({ page }) => {
@ -60,12 +67,17 @@ test("toast replacement and unmount cancel obsolete expiration callbacks under S
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual([]);
await page.clock.runFor(1);
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new"]);
await page.evaluate(() => window.shellProbe.render({ toast: { id: "default", message: "Par défaut" } }));
await page.clock.runFor(5999);
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new"]);
await page.clock.runFor(1);
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new", "default"]);
await page.evaluate(() => {
window.shellProbe.render({ toast: { id: "unmounted", message: "Fermée" } });
window.shellProbe.unmount();
});
await page.clock.runFor(120000);
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new"]);
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new", "default"]);
});
test("overlapping modals keep scroll locked until the last unmount under StrictMode", async ({ page }) => {

View file

@ -1,12 +1,14 @@
// Rôle : couvre les parcours navigateur principaux des toolboxes locales.
/* global window, document, navigator, CustomEvent, FileSystemDirectoryHandle */
import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resetSokkoStorage } from "./helpers/storage.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturePath = path.join(__dirname, "fixtures", "toolbox-export.json");
const site = JSON.parse(readFileSync(path.join(__dirname, "..", "website", "public", "data", "site.json"), "utf8"));
async function collectBrowserErrors(page) {
const errors = [];
@ -63,6 +65,65 @@ test("creates a toolbox and lazy-loads an added tool", async ({ page }) => {
await expect(page.locator("[data-module-type='goalTree']")).toBeVisible();
});
test("add-tool modal explains every tool without overflowing compact desktop widths", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 720 });
await createToolbox(page, "Catalogue des outils");
await page.getByRole("button", { name: "Ajouter un outil", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "Ajouter un outil", exact: true });
const cards = dialog.locator(".tool-add-card");
const descriptions = Object.entries(site.toolboxes.addToolDescriptions);
await expect(cards).toHaveCount(descriptions.length);
for (const [index, [, description]] of descriptions.entries()) {
await expect(dialog.getByText(description, { exact: true })).toBeVisible();
await expect(cards.nth(index).locator("strong")).not.toHaveCSS("text-overflow", "ellipsis");
}
for (const [width, expectedColumns] of [[768, 4], [960, 5], [1024, 5], [1280, 5]]) {
await page.setViewportSize({ width, height: 720 });
await expect(dialog).toBeVisible();
const layout = await dialog.evaluate((element) => {
const rect = element.getBoundingClientRect();
const grid = element.querySelector(".tool-add-grid");
return {
left: rect.left,
right: rect.right,
height: rect.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
columns: window.getComputedStyle(grid).gridTemplateColumns.split(" ").length,
overflowY: window.getComputedStyle(element).overflowY,
scrollbarColor: window.getComputedStyle(element).scrollbarColor,
scrollbarWidth: window.getComputedStyle(element).scrollbarWidth,
webkitScrollbarWidth: window.getComputedStyle(element, "::-webkit-scrollbar").width,
scrolls: element.scrollHeight > element.clientHeight,
pageFits: document.documentElement.scrollWidth <= window.innerWidth
};
});
expect(layout.left).toBeGreaterThanOrEqual(0);
expect(layout.right).toBeLessThanOrEqual(layout.viewportWidth);
expect(layout.right - layout.left).toBeGreaterThanOrEqual(Math.min(900, width - 32) - 2);
expect(layout.height).toBeLessThanOrEqual(layout.viewportHeight);
expect(layout.columns).toBe(expectedColumns);
expect(layout.overflowY).toBe("auto");
expect(layout.scrollbarColor).toContain("88, 55, 174");
expect(layout.scrollbarWidth).toBe("thin");
expect(layout.webkitScrollbarWidth).toBe("6px");
expect(layout.scrolls).toBe(true);
expect(layout.pageFits).toBe(true);
}
const categorySections = dialog.locator(".tool-add-category-section");
await expect(categorySections).toHaveCount(5);
await expect(categorySections.locator("h3")).toHaveText(Object.values(site.library.categories));
await expect(dialog.locator("[aria-pressed]")).toHaveCount(0);
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
await page.getByRole("button", { name: "Afficher l'ajout rapide", exact: true }).click();
const quickImage = page.getByRole("button", { name: "Ajouter Images", exact: true });
await quickImage.focus();
await expect(page.getByRole("tooltip", { name: "Images", exact: true })).toBeVisible();
});
test("keeps an uncommitted memo through unrelated app updates and saves multiline text", async ({ page }) => {
await createToolbox(page, "Memo draft toolbox");
const memos = moduleByType(page, "memos");