159 lines
6.0 KiB
TypeScript
159 lines
6.0 KiB
TypeScript
import { expect, test, type Locator } from "@playwright/test";
|
|
import { createQuestions } from "../../src/game";
|
|
|
|
test("전체 음량은 설정 흐름 안에 들어가며 키보드로 조절된다", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
const volume = page.getByRole("slider", { name: "전체 음량" });
|
|
// 문서상 소속뿐 아니라 컨트롤 전체가 카드 경계 안에 그려지는지 검증한다.
|
|
const expectInside = async (panel: Locator) => {
|
|
const bounds = (await panel.boundingBox())!;
|
|
const control = (await page.locator(".volume-row").boundingBox())!;
|
|
expect.soft(control.x).toBeGreaterThanOrEqual(bounds.x);
|
|
expect.soft(control.y).toBeGreaterThanOrEqual(bounds.y);
|
|
expect
|
|
.soft(control.x + control.width)
|
|
.toBeLessThanOrEqual(bounds.x + bounds.width);
|
|
expect
|
|
.soft(control.y + control.height)
|
|
.toBeLessThanOrEqual(bounds.y + bounds.height);
|
|
};
|
|
await expectInside(page.getByRole("region", { name: "나의 소리 탐험 준비" }));
|
|
const start = page.getByRole("button", {
|
|
name: "게임 시작 · 소리 듣기",
|
|
});
|
|
const control = (await page.locator(".volume-row").boundingBox())!;
|
|
expect
|
|
.soft(control.y + control.height)
|
|
.toBeLessThanOrEqual((await start.boundingBox())!.y);
|
|
await volume.focus();
|
|
await volume.press("ArrowRight");
|
|
await expect(volume).toHaveValue("36");
|
|
await volume.press("Tab");
|
|
await expect(start).toBeFocused();
|
|
await start.click();
|
|
await expectInside(page.getByRole("complementary"));
|
|
await expect(volume).toHaveValue("36");
|
|
await volume.focus();
|
|
await volume.press("ArrowLeft");
|
|
await expect(volume).toHaveValue("35");
|
|
// 음량을 패널로 옮겨도 카드와 바닥글이 맞붙지 않도록 기존 화면 구분을 유지한다.
|
|
const cards = (await page.locator(".game-columns").boundingBox())!;
|
|
const footer = (await page.getByRole("contentinfo").boundingBox())!;
|
|
expect(footer.y - (cards.y + cards.height)).toBeGreaterThanOrEqual(24);
|
|
expect(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("난이도 설명은 모바일에서도 보인다", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
|
// DOM에만 설명을 남기고 모바일 CSS로 숨기면 컴포넌트 검증은 통과하므로
|
|
// 실제 화면에서 난이도 배지와 쉬운 설명을 모두 읽을 수 있는지 확인한다.
|
|
await expect.soft(page.locator("#snr-help")).toBeVisible({ timeout: 1000 });
|
|
await expect
|
|
.soft(page.getByText("1단계 · SNR +6 dB"))
|
|
.toBeVisible({ timeout: 1000 });
|
|
await expect
|
|
.soft(page.locator("#snr-help"))
|
|
.toContainText("음성이 배경보다 6 dB 커요.", { timeout: 1000 });
|
|
});
|
|
|
|
// jsdom은 줄바꿈을 계산하지 않는다. 실제 Chrome에서 한국어 제목의
|
|
// computed style을 확인하고 360px에서도 질문과 페이지가 넘치지 않게 한다.
|
|
test("한국어 제목은 단어 중간에서 줄바꿈하지 않는다", async ({
|
|
page,
|
|
}, info) => {
|
|
await page.addInitScript(() => {
|
|
Date.now = () => 42;
|
|
});
|
|
await page.goto("/");
|
|
const wordBreaks = () =>
|
|
page.getByRole("heading").evaluateAll((headings) =>
|
|
headings.map((heading) => ({
|
|
title: heading.textContent,
|
|
wordBreak: getComputedStyle(heading).wordBreak,
|
|
})),
|
|
);
|
|
const homeHeadings = await wordBreaks();
|
|
expect.soft(homeHeadings).toEqual(
|
|
homeHeadings.map((heading) => ({
|
|
...heading,
|
|
wordBreak: "keep-all",
|
|
})),
|
|
);
|
|
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
|
const question = page.getByRole("heading", {
|
|
name: createQuestions(42, "mixed")[0]!.pattern.question,
|
|
});
|
|
await expect(question).toBeVisible();
|
|
// 전체 화면 캡처 전에 스크롤을 맞춰 화면 밖의 fixed 건너뛰기 링크가
|
|
// 이미지 안으로 들어오는 Chrome 캡처 아티팩트를 방지한다. 초점은 유지한다.
|
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
await page.screenshot({
|
|
path: info.outputPath("heading.png"),
|
|
fullPage: true,
|
|
});
|
|
const gameHeadings = await wordBreaks();
|
|
expect.soft(gameHeadings).toEqual(
|
|
gameHeadings.map((heading) => ({
|
|
...heading,
|
|
wordBreak: "keep-all",
|
|
})),
|
|
);
|
|
expect(
|
|
await question.evaluate(
|
|
(heading) => heading.scrollWidth <= heading.clientWidth,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
// 새 설정도 기존 카드와 같은 터치 크기를 갖고 360px에서 레이블과 함께 넘치지 않아야 한다.
|
|
test("시작 단계와 SNR 설정은 카드 안에 들어가고 터치 영역을 확보한다", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
const panel = (await page
|
|
.getByRole("region", { name: "나의 소리 탐험 준비" })
|
|
.boundingBox())!;
|
|
for (const control of [
|
|
page.getByLabel("시작 난이도"),
|
|
page.getByRole("slider", { name: "목표 음성 SNR" }),
|
|
]) {
|
|
const bounds = (await control.boundingBox())!;
|
|
expect(bounds.height).toBeGreaterThanOrEqual(44);
|
|
expect(bounds.x).toBeGreaterThanOrEqual(panel.x);
|
|
expect(bounds.x + bounds.width).toBeLessThanOrEqual(panel.x + panel.width);
|
|
}
|
|
// select의 상자만 들어가도 선택한 글자는 화살표 뒤에서 잘릴 수 있다.
|
|
const labelFits = await page.getByLabel("시작 난이도").evaluate((element) => {
|
|
const select = element as HTMLSelectElement;
|
|
const style = getComputedStyle(select);
|
|
const context = document.createElement("canvas").getContext("2d")!;
|
|
context.font = style.font;
|
|
const available =
|
|
select.clientWidth -
|
|
parseFloat(style.paddingLeft) -
|
|
parseFloat(style.paddingRight) -
|
|
24;
|
|
return (
|
|
context.measureText(select.selectedOptions[0]!.text).width <= available
|
|
);
|
|
});
|
|
expect(labelFits).toBe(true);
|
|
expect(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
).toBe(true);
|
|
});
|