import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { expect, test } from "@playwright/test"; import AxeBuilder from "@axe-core/playwright"; const qualityOutDir = path.resolve(process.cwd(), "quality-out"); async function writeA11ySummary(violationCount: number, violationIds: string[]): Promise { await mkdir(qualityOutDir, { recursive: true }); const payload = { axeViolationCount: violationCount, axeViolationIds: violationIds, recordedAt: new Date().toISOString() }; await writeFile(path.join(qualityOutDir, "a11y-summary.json"), JSON.stringify(payload, null, 2), "utf8"); } test.describe("Construire un bento en 3 étapes", () => { test("builder flow and accessibility baseline", async ({ page }) => { await page.goto("/"); const firstBase = page.locator(".cards button.card").first(); await firstBase.waitFor({ state: "visible" }); await firstBase.click(); await page.getByRole("button", { name: /Continuer|Continue|계속|继续/i }).click(); const firstVariant = page.locator(".cards button.card").first(); await firstVariant.waitFor({ state: "visible" }); await expect(page.locator(".builder-variant-base-reminder")).toBeVisible(); await firstVariant.click(); await page.getByRole("button", { name: /Continuer|Continue|계속|继续/i }).click(); await expect(page.locator("#recipe-sheet-title")).toBeVisible(); await expect(page.getByRole("heading", { name: /Ingrédients|Ingredients|재료|食材/i })).toBeVisible(); const firstSpriteImg = page.locator(".recipe-sheet__ingredient-sprite img").first(); await expect(firstSpriteImg).toBeVisible(); await expect .poll(async () => firstSpriteImg.evaluate((el: HTMLImageElement) => el.naturalWidth > 0 && el.naturalHeight > 0), { timeout: 15_000 }) .toBeTruthy(); const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); await writeA11ySummary( accessibilityScanResults.violations.length, accessibilityScanResults.violations.map((v) => v.id) ); expect(accessibilityScanResults.violations).toEqual([]); }); }); test.describe("Partage et URL recette", () => { test("recipe share URL opens recipe view", async ({ page }) => { await page.goto("/r/base%3Aonigiri/variant%3Aonigiri-kimchi-mozza"); await expect(page.locator("#recipe-sheet-title")).toBeVisible({ timeout: 20_000 }); await expect(page.getByRole("heading", { name: /Ingrédients|Ingredients|재료|食材/i })).toBeVisible(); }); test("HTML initial contient les métadonnées Open Graph recette", async ({ page }) => { await page.goto("/r/base%3Aonigiri/variant%3Aonigiri-kimchi-mozza"); await expect(page.locator('meta[property="og:type"]')).toHaveAttribute("content", "article"); await expect(page.locator('meta[property="og:title"]')).toHaveAttribute("content", /Onigiri/); await expect(page.locator('meta[property="og:image"]')).toHaveAttribute("content", /^https?:\/\//); await expect(page.locator('meta[property="og:url"]')).toHaveAttribute( "content", /\/r\/base%3Aonigiri\/variant%3Aonigiri-kimchi-mozza$/ ); await expect(page.locator('meta[name="twitter:card"]')).toHaveAttribute( "content", "summary_large_image" ); }); }); test.describe("Performance (sanity)", () => { test("navigation home produit des métriques de timing", async ({ page }) => { await page.goto("/"); await page.waitForLoadState("domcontentloaded"); const timing = await page.evaluate(() => { const nav = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming | undefined; return nav ? { domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime, loadEventEnd: nav.loadEventEnd - nav.startTime } : null; }); await mkdir(qualityOutDir, { recursive: true }); await writeFile( path.join(qualityOutDir, "perf-summary.json"), JSON.stringify({ navigationTiming: timing, recordedAt: new Date().toISOString() }, null, 2), "utf8" ); expect(timing).not.toBeNull(); }); });