Replace source assertions with browser coverage
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s

This commit is contained in:
Shinuwa 2026-09-12 23:35:50 +02:00
parent d0e1fd73b1
commit e6e08b458c
3 changed files with 94 additions and 14 deletions

View file

@ -42,3 +42,4 @@
{"id":"int-5a1d6dccbbcebf69807b6134785175ef","kind":"field_change","created_at":"2026-09-12T18:30:49.351851361Z","actor":"Shinuwa","issue_id":"sokko-g-d5u","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Protected IndexedDB bootstrap: readiness granted only after complete successful reads; all store writes rejected before initialization; create/import/export guards; startup error/loading gate with non-destructive retry; rejected connection promise evicted for reopening; generic accurate write-error title. Editorial strings validated and storage/workflow docs updated. Four new E2E cover repeated open/read failures, preserved data and exports, creation after retry, direct command guards and later write errors. Desktop/mobile screenshots reviewed. npm run check:full passed lint,101 Node tests,build,44 E2E. No schema or format changes."}} {"id":"int-5a1d6dccbbcebf69807b6134785175ef","kind":"field_change","created_at":"2026-09-12T18:30:49.351851361Z","actor":"Shinuwa","issue_id":"sokko-g-d5u","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Protected IndexedDB bootstrap: readiness granted only after complete successful reads; all store writes rejected before initialization; create/import/export guards; startup error/loading gate with non-destructive retry; rejected connection promise evicted for reopening; generic accurate write-error title. Editorial strings validated and storage/workflow docs updated. Four new E2E cover repeated open/read failures, preserved data and exports, creation after retry, direct command guards and later write errors. Desktop/mobile screenshots reviewed. npm run check:full passed lint,101 Node tests,build,44 E2E. No schema or format changes."}}
{"id":"int-224b7820169f57116ea4fd87a50560f1","kind":"field_change","created_at":"2026-09-12T21:26:21.88315065Z","actor":"Shinuwa","issue_id":"sokko-g-bhy","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented selective images/imageAnnotation hydration with local retry, preserved unread values in atomic snapshots, and serialized complete exports without populating the UI cache. No schema change; timer/calendar remain eager. check:full passed including 52 E2E; four production scenarios profiled alone, five reloads each. 40-media baseline 120.68 to 2.78 MiB JS heap, 610.1 to 445.4 ms median ready. Reports in .local/performance; docs updated. Opened media remain cached until reload. No git commit or push performed."}} {"id":"int-224b7820169f57116ea4fd87a50560f1","kind":"field_change","created_at":"2026-09-12T21:26:21.88315065Z","actor":"Shinuwa","issue_id":"sokko-g-bhy","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented selective images/imageAnnotation hydration with local retry, preserved unread values in atomic snapshots, and serialized complete exports without populating the UI cache. No schema change; timer/calendar remain eager. check:full passed including 52 E2E; four production scenarios profiled alone, five reloads each. 40-media baseline 120.68 to 2.78 MiB JS heap, 610.1 to 445.4 ms median ready. Reports in .local/performance; docs updated. Opened media remain cached until reload. No git commit or push performed."}}
{"id":"int-4036248c53312cfaafae4ee37bb145bd","kind":"field_change","created_at":"2026-09-12T21:26:40.724321767Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-4036248c53312cfaafae4ee37bb145bd","kind":"field_change","created_at":"2026-09-12T21:26:40.724321767Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
{"id":"int-37bf322d7d6ae9d06e033d9f16b7f057","kind":"field_change","created_at":"2026-09-12T21:33:50.250082802Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}

93
e2e/onboarding.spec.js Normal file
View file

@ -0,0 +1,93 @@
// Rôle : vérifie la prise en main, son carrousel et son intégration à la home et à la page À propos.
import { test, expect } from "@playwright/test";
import { readFile } from "node:fs/promises";
import { URL } from "node:url";
const site = JSON.parse(await readFile("website/public/data/site.json", "utf8"));
const content = site.home.onboarding;
async function setup(page, route = "/") {
await page.clock.install({ time: new Date("2026-09-12T12:00:00Z") });
await page.clock.pauseAt(new Date("2026-09-12T12:01:00Z"));
await page.goto(route);
const section = page.getByRole("region", { name: content.title, exact: true });
await expect(section).toBeVisible();
return section;
}
async function expectSlide(section, index) {
const slide = content.slides[index];
await expect(section.getByRole("button", { name: `${content.slideButtonLabel} ${index + 1}`, exact: true })).toHaveAttribute("aria-pressed", "true");
const image = section.getByRole("img", { name: slide.alt, exact: true });
await expect(image).toHaveAttribute("src", slide.src);
await expect.poll(() => image.evaluate((element) => element.naturalWidth)).toBeGreaterThan(0);
await expect(section.getByText(slide.title, { exact: true })).toBeVisible();
}
test("onboarding cycles, pauses on hover and focus, and allows manual selection", async ({ page }) => {
const section = await setup(page);
await expectSlide(section, 0);
await page.clock.runFor(4500);
await expectSlide(section, 1);
await section.getByRole("img", { name: content.slides[1].alt, exact: true }).hover();
await page.clock.runFor(9000);
await expectSlide(section, 1);
await page.mouse.move(0, 0);
await page.clock.runFor(4500);
await expectSlide(section, 2);
const first = section.getByRole("button", { name: `${content.slideButtonLabel} 1`, exact: true });
await first.focus();
await page.clock.runFor(9000);
await expectSlide(section, 2);
await first.press("Enter");
await expectSlide(section, 0);
await section.getByRole("button", { name: content.primaryAction, exact: true }).focus();
await page.clock.runFor(4500);
await expectSlide(section, 1);
await page.clock.runFor(4500 * (content.slides.length - 1));
await expectSlide(section, 0);
});
test("reduced motion keeps previews still but manual selection remains available", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
const section = await setup(page);
await page.clock.runFor(18000);
await expectSlide(section, 0);
await section.getByRole("button", { name: `${content.slideButtonLabel} 3`, exact: true }).click();
await expectSlide(section, 2);
await page.mouse.move(0, 0);
await section.getByRole("button", { name: content.primaryAction, exact: true }).focus();
await page.clock.runFor(18000);
await expectSlide(section, 2);
});
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 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();
await expect(page).toHaveURL(/#\/toolbox\//);
await expect(page.locator('[data-module-type="memos"]')).toBeVisible();
await expect(page.locator('[data-module-type="links"]')).toBeVisible();
await expect(page.locator(".toolbox-tutorial-count")).toHaveText(/^1\//);
});
test("about reuses onboarding without creation and exposes tool examples and destinations", async ({ page }) => {
await page.goto("/#/about");
await expect(page.getByRole("heading", { name: site.about.title, exact: true })).toBeVisible();
const section = page.getByRole("region", { name: content.title, exact: true });
await expect(section).toBeVisible();
await expect(section.getByRole("button", { name: content.primaryAction, exact: true })).toHaveCount(0);
for (const group of site.about.toolGroups) {
await expect(page.getByRole("list", { name: `Exemples pour ${group.title}`, exact: true }).getByRole("listitem")).toHaveText(group.examples);
}
for (const card of content.cards) {
await section.getByRole("link").filter({ hasText: card.title }).click();
await expect.poll(() => new URL(page.url()).hash).toBe(card.href);
await page.goBack();
await expect(section).toBeVisible();
}
await page.getByRole("link", { name: site.about.toolsLibraryLink, exact: true }).click();
await expect(page).toHaveURL(/#\/library$/);
});

View file

@ -11,7 +11,6 @@ test("vite entrypoint and app shell are wired", async () => {
const overlays = await readFile("website/src/components/AppOverlays.jsx", "utf8"); const overlays = await readFile("website/src/components/AppOverlays.jsx", "utf8");
const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8"); const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8");
const homePage = await readFile("website/src/pages/HomePage.jsx", "utf8"); const homePage = await readFile("website/src/pages/HomePage.jsx", "utf8");
const homeOnboarding = await readFile("website/src/pages/HomeOnboardingSection.jsx", "utf8");
const aboutPage = await readFile("website/src/pages/AboutPage.jsx", "utf8"); const aboutPage = await readFile("website/src/pages/AboutPage.jsx", "utf8");
const routeContent = await readFile("website/src/router/RouteContent.jsx", "utf8"); const routeContent = await readFile("website/src/router/RouteContent.jsx", "utf8");
@ -22,7 +21,6 @@ test("vite entrypoint and app shell are wired", async () => {
assert.match(source, /hooks\/useGameFilters\.js/); assert.match(source, /hooks\/useGameFilters\.js/);
assert.doesNotMatch(source, /DEFAULT_SITE_CONTENT/); assert.doesNotMatch(source, /DEFAULT_SITE_CONTENT/);
assert.match(gameFiltersHook, /export function useGameFilters/); assert.match(gameFiltersHook, /export function useGameFilters/);
assert.match(routeContent, /pages\/AboutPage\.jsx/);
assert.match(routeContent, /features\/games\/GameRoute\.jsx/); assert.match(routeContent, /features\/games\/GameRoute\.jsx/);
assert.doesNotMatch(shell, /content\.navigation\.library/); assert.doesNotMatch(shell, /content\.navigation\.library/);
assert.match(shell, /sidebar-about-link/); assert.match(shell, /sidebar-about-link/);
@ -32,21 +30,9 @@ test("vite entrypoint and app shell are wired", async () => {
assert.match(overlays, /uid\("marker"\)/); assert.match(overlays, /uid\("marker"\)/);
assert.match(imageViewer, /export function ImageViewer/); assert.match(imageViewer, /export function ImageViewer/);
assert.match(imageViewer, /function openImageInNewTab/); assert.match(imageViewer, /function openImageInNewTab/);
assert.match(homePage, /HomeOnboardingSection/);
assert.match(homePage, /import\.meta\.env\.DEV \|\| toolboxes\.length === 0/); assert.match(homePage, /import\.meta\.env\.DEV \|\| toolboxes\.length === 0/);
assert.match(homePage, /setCreateModal\(\{ tutorial: true \}\)/);
assert.match(homePage, /dragon-home\.png/); assert.match(homePage, /dragon-home\.png/);
assert.match(homeOnboarding, /export function HomeOnboardingSection/);
assert.match(homeOnboarding, /prefers-reduced-motion: reduce/);
assert.match(homeOnboarding, /onCreateToolbox/);
assert.match(homeOnboarding, /content\.primaryAction/);
assert.match(homeOnboarding, /content\.sliderTitle/);
assert.match(homeOnboarding, /home-onboarding-dots/);
assert.match(aboutPage, /export function AboutPage/);
assert.match(aboutPage, /HomeOnboardingSection/);
assert.match(aboutPage, /siteContent\.home\.onboarding/);
assert.match(aboutPage, /module-icon-\$\{group\.icon \|\| "notepad"\}/); assert.match(aboutPage, /module-icon-\$\{group\.icon \|\| "notepad"\}/);
assert.match(aboutPage, /about-tool-examples/);
}); });
test("project documentation tracks storage and feature updates", async () => { test("project documentation tracks storage and feature updates", async () => {