import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { expect, test } from "@playwright/test"; const repoRoot = process.cwd(); const gifFlow = path.join(repoRoot, "apps/web/public/tour/flow-builder.gif"); const gifRecipe = path.join(repoRoot, "apps/web/public/tour/recipe-sheet.gif"); const assertFfmpeg = () => { try { execFileSync("ffmpeg", ["-version"], { stdio: "ignore" }); } catch { throw new Error("ffmpeg est requis pour npm run tour:capture (ex. apt install ffmpeg)"); } }; const webmToGif = (webm: string, gif: string) => { execFileSync( "ffmpeg", [ "-y", "-i", webm, "-vf", "fps=8,scale=360:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128:stats_mode=single[p];[s1][p]paletteuse=dither=bayer", gif ], { stdio: "inherit" } ); }; const seedStableSession = async (context: import("@playwright/test").BrowserContext) => { await context.addInitScript(() => { localStorage.setItem("ben-to-cookie-consent-v1", "analytics-rejected"); localStorage.setItem("ben-to.welcome-tour-dismissed", "v1"); }); }; test.describe.configure({ mode: "serial" }); test.describe("GIFs public/tour (Playwright + ffmpeg)", () => { test("flow-builder.gif", async ({ browser }, testInfo) => { assertFfmpeg(); fs.mkdirSync(path.dirname(gifFlow), { recursive: true }); const videoDir = testInfo.outputPath("flow-video"); fs.rmSync(videoDir, { recursive: true, force: true }); fs.mkdirSync(videoDir, { recursive: true }); const context = await browser.newContext({ recordVideo: { dir: videoDir, size: { width: 390, height: 760 } }, viewport: { width: 390, height: 760 } }); await seedStableSession(context); const page = await context.newPage(); 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(); await expect(page.locator(".builder-variant-base-reminder")).toBeVisible({ timeout: 25_000 }); const firstVariant = page.locator(".cards button.card").first(); await firstVariant.waitFor({ state: "visible" }); await firstVariant.click(); await page.waitForTimeout(500); /* S’arrête ici (étape garniture) pour ne pas dupliquer la fiche recette du second GIF. */ const video = page.video(); await page.close(); const webm = await video?.path(); await context.close(); if (!webm) throw new Error("Aucun fichier vidéo Playwright"); webmToGif(webm, gifFlow); }); test("recipe-sheet.gif", async ({ browser }, testInfo) => { assertFfmpeg(); fs.mkdirSync(path.dirname(gifRecipe), { recursive: true }); const videoDir = testInfo.outputPath("recipe-video"); fs.rmSync(videoDir, { recursive: true, force: true }); fs.mkdirSync(videoDir, { recursive: true }); const context = await browser.newContext({ recordVideo: { dir: videoDir, size: { width: 390, height: 760 } }, viewport: { width: 390, height: 760 } }); await seedStableSession(context); const page = await context.newPage(); await page.goto("/r/base%3Aonigiri/variant%3Aonigiri-kimchi-mozza"); await expect(page.locator("#recipe-sheet-title")).toBeVisible({ timeout: 25_000 }); const sheet = page.locator("section.recipe-sheet"); await page.waitForTimeout(400); await sheet.evaluate((el) => { el.scrollTo({ top: 0, behavior: "auto" }); }); await page.waitForTimeout(450); await sheet.evaluate((el) => { el.scrollTo({ top: 400, behavior: "auto" }); }); await page.waitForTimeout(450); await sheet.evaluate((el) => { el.scrollTo({ top: 900, behavior: "auto" }); }); await page.waitForTimeout(450); const video = page.video(); await page.close(); const webm = await video?.path(); await context.close(); if (!webm) throw new Error("Aucun fichier vidéo Playwright"); webmToGif(webm, gifRecipe); }); });