96 lines
4.1 KiB
TypeScript
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();
|
|
});
|
|
});
|