import { expect, test } from "@playwright/test"; import AxeBuilder from "@axe-core/playwright"; import { WORDS } from "../../src/content"; // 실제 WAV를 디코드하고 Web Audio의 ended를 기다린다. 모의 엔진을 사용한 // 컴포넌트 검증과 달리 배포 산출물·자동 재생 정책·레이아웃을 함께 확인한다. test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음", async ({ page, }, info) => { const external: string[] = []; const errors: string[] = []; let audioPath = ""; page.on("request", (request) => { if (!request.url().startsWith("http://127.0.0.1:4173")) external.push(request.url()); if ( request.url().endsWith(".wav") && !request.url().endsWith("intro.wav") ) audioPath = new URL(request.url()).pathname; }); page.on("pageerror", (error) => errors.push(error.message)); await page.goto("/"); await expect( page.getByRole("heading", { name: "단어를 맞춰라" }), ).toBeVisible(); expect(audioPath).toBe(""); expect( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), ).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: `.agent/evidence/release-${info.project.name}-home.png`, fullPage: true, }); expect((await new AxeBuilder({ page }).analyze()).violations).toEqual( [], ); await page.emulateMedia({ reducedMotion: "reduce" }); await page .getByRole("button", { name: "게임 시작 · 소리 듣기" }) .click(); await expect( page.getByRole("button", { name: /듣는 중/ }), ).toBeDisabled(); await expect(page.locator(".sound-wave.active i").first()).toHaveCSS( "animation-name", "none", ); await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible(); for (let i = 0; i < 10; i++) { if (i) await page.getByRole("button", { name: /문장 듣기/ }).click(); const target = i % 2 ? page.getByRole("textbox", { name: "들은 단어" }) : page.getByRole("group", { name: "답 선택" }); await expect(target).toBeVisible(); // 열 가지 질문 모두 실제 렌더링을 거쳐 어절 보존과 패널 내 표시를 검증한다. const heading = page.locator(".question-panel h2"); await expect(heading).toHaveCSS("word-break", "keep-all"); expect( await heading.evaluate( (element) => element.scrollWidth <= element.clientWidth, ), ).toBe(true); const answer = WORDS.find((w) => audioPath.endsWith(`-${w.id}.wav`), )!.answer; if (i === 0) { expect( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), ).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: `.agent/evidence/release-${info.project.name}-game.png`, fullPage: true, }); expect( (await new AxeBuilder({ page }).analyze()).violations, ).toEqual([]); await page.getByRole("button", { name: /다시 듣기/ }).click(); await expect(target).toBeVisible(); } if (i % 2) { await target.fill(answer); await target.press("Enter"); } else { const button = page.getByRole("button", { name: new RegExp(answer), }); await button.focus(); await button.press("Enter"); } await expect( page.getByRole("heading", { name: "정답이에요!" }), ).toBeVisible(); await page .getByRole("button", { name: i === 9 ? /결과 보기/ : /다음 문제/ }) .click(); } await expect(page.getByText("100%")).toBeVisible(); expect( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), ).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: `.agent/evidence/release-${info.project.name}-results.png`, fullPage: true, }); expect((await new AxeBuilder({ page }).analyze()).violations).toEqual( [], ); await page.getByRole("button", { name: "다시 시작" }).click(); await expect( page.getByRole("button", { name: "게임 시작 · 소리 듣기" }), ).toBeVisible(); expect(external).toEqual([]); expect(errors).toEqual([]); }); test("오디오 HTTP 실패를 화면에 표시하고 재시도한다", async ({ page }) => { await page.route("**/audio/*.wav", (route) => route.fulfill({ status: 404, body: "missing" }), ); await page.goto("/"); await page .getByRole("button", { name: "게임 시작 · 소리 듣기" }) .click(); await expect(page.getByRole("alert")).toContainText( "소리를 재생하지 못했어요", ); await page.unroute("**/audio/*.wav"); await page.getByRole("button", { name: "다시 시도" }).click(); // 재시도는 안내부터 재생한다. 최대 8.1205 + 10.1725 + 시작·전환 0.4초에 // 로드·디코드·병렬 브라우저 스케줄링 여유를 더해 이 assertion만 30초로 제한한다. await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({ timeout: 30_000, }); await page.getByRole("button", { name: "처음으로" }).click(); await expect( page.getByRole("button", { name: "게임 시작 · 소리 듣기" }), ).toBeFocused(); });