Files
Find-Word/tests/browser/game.spec.ts
T
2026-10-14 06:19:24 +00:00

150 lines
5.3 KiB
TypeScript

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