136 lines
4.9 KiB
TypeScript
136 lines
4.9 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { WORDS } from "../../src/content";
|
|
|
|
// 글자별 스타일 추측 대신 실제 Range 줄 상자를 비교해 어절 내부 분리를 검출한다.
|
|
async function copyLayout(page: Page, selector: string) {
|
|
return page.locator(selector).evaluateAll((elements) =>
|
|
elements.flatMap((element) => {
|
|
const problems: string[] = [];
|
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
while (walker.nextNode()) {
|
|
const node = walker.currentNode;
|
|
for (const match of (node.textContent ?? "").matchAll(/[가-힣]+/g)) {
|
|
const range = document.createRange();
|
|
range.setStart(node, match.index!);
|
|
range.setEnd(node, match.index! + match[0].length);
|
|
if (new Set(Array.from(range.getClientRects(), (r) => r.y)).size > 1)
|
|
problems.push(`split: ${match[0]}`);
|
|
}
|
|
}
|
|
if (element.scrollWidth > element.clientWidth)
|
|
problems.push(`overflow: ${element.textContent}`);
|
|
return problems;
|
|
}),
|
|
);
|
|
}
|
|
|
|
test("리뷰: 한국어 모드·결과 어절은 320px와 360px에서도 갈라지거나 넘치지 않는다", async ({
|
|
page,
|
|
}, info) => {
|
|
for (const width of [320, 360]) {
|
|
await page.setViewportSize({ width, height: 800 });
|
|
await page.goto("/");
|
|
expect.soft(await copyLayout(page, ".mode-option small")).toEqual([]);
|
|
await page.screenshot({
|
|
path: info.outputPath(`copy-${width}-home.png`),
|
|
fullPage: true,
|
|
});
|
|
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
|
await page.getByRole("button", { name: "문제 풀이 종료" }).click();
|
|
expect
|
|
.soft(
|
|
await copyLayout(
|
|
page,
|
|
".results-panel h2, .results-panel p, .stats span, .results-panel th, .results-panel td",
|
|
),
|
|
)
|
|
.toEqual([]);
|
|
expect
|
|
.soft(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
)
|
|
.toBe(true);
|
|
await page.screenshot({
|
|
path: info.outputPath(`copy-${width}-results.png`),
|
|
fullPage: true,
|
|
});
|
|
}
|
|
});
|
|
|
|
test("리뷰: 실제 진행 막대는 현재 문항의 1/3·2/3·3/3을 채운다", async ({
|
|
page,
|
|
}, info) => {
|
|
let target = "";
|
|
page.on("request", (r) => {
|
|
const path = new URL(r.url()).pathname;
|
|
if (path.startsWith("/audio/") && path !== "/audio/intro.wav")
|
|
target = path;
|
|
});
|
|
await page.goto("/");
|
|
await page.getByRole("radio", { name: "보기에서 선택" }).check();
|
|
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
|
for (let index = 0; index < 3; index++) {
|
|
const bar = page.getByRole("progressbar");
|
|
expect
|
|
.soft(await bar.evaluate((el) => (el as HTMLProgressElement).position))
|
|
.toBeCloseTo((index + 1) / 3);
|
|
await expect
|
|
.soft(bar)
|
|
.toHaveAccessibleName("현재 문항 진행률", { timeout: 1000 });
|
|
const png = await bar.screenshot({
|
|
path: info.outputPath(`progress-${index + 1}.png`),
|
|
});
|
|
// 실제 그려진 중앙 행의 녹색 채움 비율까지 측정한다. DOM value만 맞는 빈 막대는 실패한다.
|
|
const fill = await page.evaluate(async (base64) => {
|
|
const bitmap = await createImageBitmap(
|
|
new Blob([Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))], {
|
|
type: "image/png",
|
|
}),
|
|
);
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = bitmap.width;
|
|
canvas.height = bitmap.height;
|
|
const context = canvas.getContext("2d")!;
|
|
context.drawImage(bitmap, 0, 0);
|
|
const row = context.getImageData(
|
|
0,
|
|
Math.floor(canvas.height / 2),
|
|
canvas.width,
|
|
1,
|
|
).data;
|
|
let green = 0;
|
|
for (let x = 0; x < canvas.width; x++) {
|
|
if (
|
|
Math.abs(row[x * 4]! - 101) < 12 &&
|
|
Math.abs(row[x * 4 + 1]! - 132) < 12 &&
|
|
Math.abs(row[x * 4 + 2]! - 80) < 12
|
|
)
|
|
green++;
|
|
}
|
|
bitmap.close();
|
|
return { pixels: green, width: canvas.width };
|
|
}, png.toString("base64"));
|
|
expect
|
|
.soft(Math.abs(fill.pixels - (fill.width * (index + 1)) / 3))
|
|
.toBeLessThanOrEqual(2);
|
|
if (index) await page.getByRole("button", { name: /문장 듣기/ }).click();
|
|
await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({
|
|
timeout: 30000,
|
|
});
|
|
const answer = WORDS.find((w) => target.endsWith(`-${w.id}.wav`))!.answer;
|
|
await page.getByRole("button", { name: new RegExp(answer) }).click();
|
|
await page
|
|
.getByRole("button", { name: index === 2 ? /결과 보기/ : /다음 문제/ })
|
|
.click();
|
|
}
|
|
await page.screenshot({
|
|
path: info.outputPath("normal-results.png"),
|
|
fullPage: true,
|
|
});
|
|
expect(
|
|
await copyLayout(page, ".results-panel h2, .results-panel p, .stats span"),
|
|
).toEqual([]);
|
|
});
|