183 lines
6.8 KiB
TypeScript
183 lines
6.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import AxeBuilder from "@axe-core/playwright";
|
|
import { WORDS } from "../../src/content";
|
|
import { isAudioRequest } from "./audio-requests";
|
|
|
|
// 실제 WAV를 디코드하고 Web Audio의 ended를 기다린다. 모의 엔진을 사용한
|
|
// 컴포넌트 검증과 달리 배포 산출물·자동 재생 정책·레이아웃을 함께 확인한다.
|
|
test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음", async ({
|
|
page,
|
|
}, info) => {
|
|
const external: string[] = [];
|
|
const errors: string[] = [];
|
|
let audioPath = "";
|
|
const audioUrls: URL[] = [];
|
|
page.on("request", (request) => {
|
|
const url = new URL(request.url());
|
|
if (url.origin !== "http://127.0.0.1:4173") external.push(request.url());
|
|
if (isAudioRequest(url)) {
|
|
audioUrls.push(url);
|
|
if (url.pathname !== "/audio/intro.wav") audioPath = 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: info.outputPath("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.poll(() => audioUrls.length).toBe(2);
|
|
expect(audioUrls.map((url) => url.searchParams.get("v"))).toEqual([
|
|
"0.2.5",
|
|
"0.2.5",
|
|
]);
|
|
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 < 3; 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: info.outputPath("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 === 2 ? /결과 보기/ : /다음 문제/ })
|
|
.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: info.outputPath("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(new Set(audioUrls.map((url) => url.searchParams.get("v")))).toEqual(
|
|
new Set(["0.2.5"]),
|
|
);
|
|
expect(
|
|
audioUrls.filter((url) => url.pathname !== "/audio/intro.wav"),
|
|
).toHaveLength(4);
|
|
expect(external).toEqual([]);
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
for (const failed of ["intro", "target"]) {
|
|
test(`${failed} WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다`, async ({
|
|
page,
|
|
}) => {
|
|
const audioUrls: string[] = [];
|
|
const missing: string[] = [];
|
|
page.on("request", (request) => {
|
|
if (isAudioRequest(new URL(request.url()))) audioUrls.push(request.url());
|
|
});
|
|
const matchesFailure = (url: URL) =>
|
|
isAudioRequest(url) &&
|
|
(failed === "intro"
|
|
? url.pathname === "/audio/intro.wav"
|
|
: url.pathname !== "/audio/intro.wav");
|
|
await page.route(matchesFailure, (route) => {
|
|
missing.push(route.request().url());
|
|
return route.fulfill({ status: 404, body: "missing" });
|
|
});
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
|
await expect(page.getByRole("alert")).toContainText(
|
|
"소리를 재생하지 못했어요",
|
|
);
|
|
expect(missing).toHaveLength(1);
|
|
expect(new URL(missing[0]!).searchParams.get("v")).toBe("0.2.5");
|
|
expect(audioUrls).toHaveLength(2);
|
|
await page.unroute(matchesFailure);
|
|
await page.getByRole("button", { name: "다시 시도" }).click();
|
|
// 안내 7.056 + 최대 목표 11.256초와 로드·디코드 여유를 포함한다.
|
|
await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
expect(audioUrls).toHaveLength(4);
|
|
expect(audioUrls.slice(2).sort()).toEqual(audioUrls.slice(0, 2).sort());
|
|
expect(
|
|
new Set(audioUrls.map((url) => new URL(url).searchParams.get("v"))),
|
|
).toEqual(new Set(["0.2.5"]));
|
|
const targetPath = audioUrls
|
|
.map((url) => new URL(url).pathname)
|
|
.find((path) => path !== "/audio/intro.wav")!;
|
|
const answer = WORDS.find((word) =>
|
|
targetPath.endsWith(`-${word.id}.wav`),
|
|
)!.answer;
|
|
await page.getByRole("button", { name: new RegExp(answer) }).click();
|
|
await expect(
|
|
page.getByRole("heading", { name: "정답이에요!" }),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: "문제 풀이 종료" }).click();
|
|
await page.getByRole("button", { name: "처음 화면으로" }).click();
|
|
await expect(
|
|
page.getByRole("button", { name: "게임 시작 · 소리 듣기" }),
|
|
).toBeFocused();
|
|
});
|
|
}
|