Improve compact desktop usability and action guidance
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
0ac1a0974e
commit
0fc18cd8b9
24 changed files with 325 additions and 90 deletions
|
|
@ -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"]');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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$/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue