Files
2026-07-21 16:50:58 +02:00

96 lines
4.1 KiB
TypeScript

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<void> {
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();
});
});