feat: release Find-Word v0.2.3
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"abdd2e2cf517d3c959fd-e748bd769b024912a05a",
|
||||
"abdd2e2cf517d3c959fd-f7936886d307bf504a2e",
|
||||
"abdd2e2cf517d3c959fd-297a2a889ff2a2c13233",
|
||||
"abdd2e2cf517d3c959fd-99533baf08fa35da0cd2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: review-fixes.spec.ts >> 리뷰: 실제 진행 막대는 현재 문항의 1/3·2/3·3/3을 채운다
|
||||
- Location: tests/browser/review-fixes.spec.ts:38:1
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(received).toBeCloseTo(expected)
|
||||
|
||||
Expected: 0.3333333333333333
|
||||
Received: 0
|
||||
|
||||
Expected precision: 2
|
||||
Expected difference: < 0.005
|
||||
Received difference: 0.3333333333333333
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(locator).toHaveAccessibleName(expected) failed
|
||||
|
||||
Locator: getByRole('progressbar')
|
||||
Expected: "현재 문항 진행률"
|
||||
Received: "전체 진행률"
|
||||
Timeout: 1000ms
|
||||
|
||||
Call log:
|
||||
- Expect "soft toHaveAccessibleName" getByRole('progressbar') with timeout 1000ms
|
||||
- waiting for getByRole('progressbar')
|
||||
11 × locator resolved to <progress max="3" value="0" aria-label="전체 진행률"></progress>
|
||||
- unexpected value "전체 진행률"
|
||||
|
||||
```
|
||||
|
||||
```yaml
|
||||
- progressbar "전체 진행률"
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toBeCloseTo(expected)
|
||||
|
||||
Expected: 0.6666666666666666
|
||||
Received: 0.3333333333333333
|
||||
|
||||
Expected precision: 2
|
||||
Expected difference: < 0.005
|
||||
Received difference: 0.3333333333333333
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(locator).toHaveAccessibleName(expected) failed
|
||||
|
||||
Locator: getByRole('progressbar')
|
||||
Expected: "현재 문항 진행률"
|
||||
Received: "전체 진행률"
|
||||
Timeout: 1000ms
|
||||
|
||||
Call log:
|
||||
- Expect "soft toHaveAccessibleName" getByRole('progressbar') with timeout 1000ms
|
||||
- waiting for getByRole('progressbar')
|
||||
11 × locator resolved to <progress max="3" value="1" aria-label="전체 진행률"></progress>
|
||||
- unexpected value "전체 진행률"
|
||||
|
||||
```
|
||||
|
||||
```yaml
|
||||
- progressbar "전체 진행률"
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toBeCloseTo(expected)
|
||||
|
||||
Expected: 1
|
||||
Received: 0.6666666666666666
|
||||
|
||||
Expected precision: 2
|
||||
Expected difference: < 0.005
|
||||
Received difference: 0.33333333333333337
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(locator).toHaveAccessibleName(expected) failed
|
||||
|
||||
Locator: getByRole('progressbar')
|
||||
Expected: "현재 문항 진행률"
|
||||
Received: "전체 진행률"
|
||||
Timeout: 1000ms
|
||||
|
||||
Call log:
|
||||
- Expect "soft toHaveAccessibleName" getByRole('progressbar') with timeout 1000ms
|
||||
- waiting for getByRole('progressbar')
|
||||
11 × locator resolved to <progress max="3" value="2" aria-label="전체 진행률"></progress>
|
||||
- unexpected value "전체 진행률"
|
||||
|
||||
```
|
||||
|
||||
```yaml
|
||||
- progressbar "전체 진행률"
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
1 | import { expect, test, type Page } from "@playwright/test";
|
||||
2 | import { WORDS } from "../../src/content";
|
||||
3 |
|
||||
4 | // 글자별 스타일 추측 대신 실제 Range 줄 상자를 비교해 어절 내부 분리를 검출한다.
|
||||
5 | async function copyLayout(page: Page, selector: string) {
|
||||
6 | return page.locator(selector).evaluateAll((elements) => elements.flatMap((element) => {
|
||||
7 | const problems: string[] = [];
|
||||
8 | const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
||||
9 | while (walker.nextNode()) {
|
||||
10 | const node = walker.currentNode;
|
||||
11 | for (const match of (node.textContent ?? "").matchAll(/[가-힣]+/g)) {
|
||||
12 | const range = document.createRange();
|
||||
13 | range.setStart(node, match.index!);
|
||||
14 | range.setEnd(node, match.index! + match[0].length);
|
||||
15 | if (new Set(Array.from(range.getClientRects(), (r) => r.y)).size > 1)
|
||||
16 | problems.push(`split: ${match[0]}`);
|
||||
17 | }
|
||||
18 | }
|
||||
19 | if (element.scrollWidth > element.clientWidth) problems.push(`overflow: ${element.textContent}`);
|
||||
20 | return problems;
|
||||
21 | }));
|
||||
22 | }
|
||||
23 |
|
||||
24 | test("리뷰: 한국어 모드·결과 어절은 320px와 360px에서도 갈라지거나 넘치지 않는다", async ({ page }, info) => {
|
||||
25 | for (const width of [320, 360]) {
|
||||
26 | await page.setViewportSize({ width, height: 800 });
|
||||
27 | await page.goto("/");
|
||||
28 | expect.soft(await copyLayout(page, ".mode-option small")).toEqual([]);
|
||||
29 | await page.screenshot({ path: info.outputPath(`copy-${width}-home.png`), fullPage: true });
|
||||
30 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
||||
31 | await page.getByRole("button", { name: "문제 풀이 종료" }).click();
|
||||
32 | expect.soft(await copyLayout(page, ".results-panel h2, .results-panel p, .stats span, .results-panel th, .results-panel td")).toEqual([]);
|
||||
33 | expect.soft(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
34 | await page.screenshot({ path: info.outputPath(`copy-${width}-results.png`), fullPage: true });
|
||||
35 | }
|
||||
36 | });
|
||||
37 |
|
||||
38 | test("리뷰: 실제 진행 막대는 현재 문항의 1/3·2/3·3/3을 채운다", async ({ page }, info) => {
|
||||
39 | let target = "";
|
||||
40 | page.on("request", (r) => {
|
||||
41 | const path = new URL(r.url()).pathname;
|
||||
42 | if (path.startsWith("/audio/") && path !== "/audio/intro.wav") target = path;
|
||||
43 | });
|
||||
44 | await page.goto("/");
|
||||
45 | await page.getByRole("radio", { name: "보기에서 선택" }).check();
|
||||
46 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
||||
47 | for (let index = 0; index < 3; index++) {
|
||||
48 | const bar = page.getByRole("progressbar");
|
||||
49 | expect.soft(await bar.evaluate((el) => (el as HTMLProgressElement).position)).toBeCloseTo((index + 1) / 3);
|
||||
> 50 | await expect.soft(bar).toHaveAccessibleName("현재 문항 진행률", { timeout: 1000 });
|
||||
| ^ Error: expect(locator).toHaveAccessibleName(expected) failed
|
||||
51 | await bar.screenshot({ path: info.outputPath(`progress-${index + 1}.png`) });
|
||||
52 | if (index) await page.getByRole("button", { name: /문장 듣기/ }).click();
|
||||
53 | await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({ timeout: 30000 });
|
||||
54 | const answer = WORDS.find((w) => target.endsWith(`-${w.id}.wav`))!.answer;
|
||||
55 | await page.getByRole("button", { name: new RegExp(answer) }).click();
|
||||
56 | await page.getByRole("button", { name: index === 2 ? /결과 보기/ : /다음 문제/ }).click();
|
||||
57 | }
|
||||
58 | await page.screenshot({ path: info.outputPath("normal-results.png"), fullPage: true });
|
||||
59 | expect(await copyLayout(page, ".results-panel h2, .results-panel p, .stats span")).toEqual([]);
|
||||
60 | });
|
||||
61 |
|
||||
```
|
||||
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 189 B |
|
After Width: | Height: | Size: 288 B |
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,185 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: review-fixes.spec.ts >> 리뷰: 실제 진행 막대는 현재 문항의 1/3·2/3·3/3을 채운다
|
||||
- Location: tests/browser/review-fixes.spec.ts:38:1
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(received).toBeCloseTo(expected)
|
||||
|
||||
Expected: 0.3333333333333333
|
||||
Received: 0
|
||||
|
||||
Expected precision: 2
|
||||
Expected difference: < 0.005
|
||||
Received difference: 0.3333333333333333
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(locator).toHaveAccessibleName(expected) failed
|
||||
|
||||
Locator: getByRole('progressbar')
|
||||
Expected: "현재 문항 진행률"
|
||||
Received: "전체 진행률"
|
||||
Timeout: 1000ms
|
||||
|
||||
Call log:
|
||||
- Expect "soft toHaveAccessibleName" getByRole('progressbar') with timeout 1000ms
|
||||
- waiting for getByRole('progressbar')
|
||||
11 × locator resolved to <progress max="3" value="0" aria-label="전체 진행률"></progress>
|
||||
- unexpected value "전체 진행률"
|
||||
|
||||
```
|
||||
|
||||
```yaml
|
||||
- progressbar "전체 진행률"
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toBeCloseTo(expected)
|
||||
|
||||
Expected: 0.6666666666666666
|
||||
Received: 0.3333333333333333
|
||||
|
||||
Expected precision: 2
|
||||
Expected difference: < 0.005
|
||||
Received difference: 0.3333333333333333
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(locator).toHaveAccessibleName(expected) failed
|
||||
|
||||
Locator: getByRole('progressbar')
|
||||
Expected: "현재 문항 진행률"
|
||||
Received: "전체 진행률"
|
||||
Timeout: 1000ms
|
||||
|
||||
Call log:
|
||||
- Expect "soft toHaveAccessibleName" getByRole('progressbar') with timeout 1000ms
|
||||
- waiting for getByRole('progressbar')
|
||||
11 × locator resolved to <progress max="3" value="1" aria-label="전체 진행률"></progress>
|
||||
- unexpected value "전체 진행률"
|
||||
|
||||
```
|
||||
|
||||
```yaml
|
||||
- progressbar "전체 진행률"
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toBeCloseTo(expected)
|
||||
|
||||
Expected: 1
|
||||
Received: 0.6666666666666666
|
||||
|
||||
Expected precision: 2
|
||||
Expected difference: < 0.005
|
||||
Received difference: 0.33333333333333337
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(locator).toHaveAccessibleName(expected) failed
|
||||
|
||||
Locator: getByRole('progressbar')
|
||||
Expected: "현재 문항 진행률"
|
||||
Received: "전체 진행률"
|
||||
Timeout: 1000ms
|
||||
|
||||
Call log:
|
||||
- Expect "soft toHaveAccessibleName" getByRole('progressbar') with timeout 1000ms
|
||||
- waiting for getByRole('progressbar')
|
||||
11 × locator resolved to <progress max="3" value="2" aria-label="전체 진행률"></progress>
|
||||
- unexpected value "전체 진행률"
|
||||
|
||||
```
|
||||
|
||||
```yaml
|
||||
- progressbar "전체 진행률"
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 4
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ "split: 만나요",
|
||||
+ "split: 최대",
|
||||
+ ]
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
1 | import { expect, test, type Page } from "@playwright/test";
|
||||
2 | import { WORDS } from "../../src/content";
|
||||
3 |
|
||||
4 | // 글자별 스타일 추측 대신 실제 Range 줄 상자를 비교해 어절 내부 분리를 검출한다.
|
||||
5 | async function copyLayout(page: Page, selector: string) {
|
||||
6 | return page.locator(selector).evaluateAll((elements) => elements.flatMap((element) => {
|
||||
7 | const problems: string[] = [];
|
||||
8 | const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
||||
9 | while (walker.nextNode()) {
|
||||
10 | const node = walker.currentNode;
|
||||
11 | for (const match of (node.textContent ?? "").matchAll(/[가-힣]+/g)) {
|
||||
12 | const range = document.createRange();
|
||||
13 | range.setStart(node, match.index!);
|
||||
14 | range.setEnd(node, match.index! + match[0].length);
|
||||
15 | if (new Set(Array.from(range.getClientRects(), (r) => r.y)).size > 1)
|
||||
16 | problems.push(`split: ${match[0]}`);
|
||||
17 | }
|
||||
18 | }
|
||||
19 | if (element.scrollWidth > element.clientWidth) problems.push(`overflow: ${element.textContent}`);
|
||||
20 | return problems;
|
||||
21 | }));
|
||||
22 | }
|
||||
23 |
|
||||
24 | test("리뷰: 한국어 모드·결과 어절은 320px와 360px에서도 갈라지거나 넘치지 않는다", async ({ page }, info) => {
|
||||
25 | for (const width of [320, 360]) {
|
||||
26 | await page.setViewportSize({ width, height: 800 });
|
||||
27 | await page.goto("/");
|
||||
28 | expect.soft(await copyLayout(page, ".mode-option small")).toEqual([]);
|
||||
29 | await page.screenshot({ path: info.outputPath(`copy-${width}-home.png`), fullPage: true });
|
||||
30 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
||||
31 | await page.getByRole("button", { name: "문제 풀이 종료" }).click();
|
||||
32 | expect.soft(await copyLayout(page, ".results-panel h2, .results-panel p, .stats span, .results-panel th, .results-panel td")).toEqual([]);
|
||||
33 | expect.soft(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
34 | await page.screenshot({ path: info.outputPath(`copy-${width}-results.png`), fullPage: true });
|
||||
35 | }
|
||||
36 | });
|
||||
37 |
|
||||
38 | test("리뷰: 실제 진행 막대는 현재 문항의 1/3·2/3·3/3을 채운다", async ({ page }, info) => {
|
||||
39 | let target = "";
|
||||
40 | page.on("request", (r) => {
|
||||
41 | const path = new URL(r.url()).pathname;
|
||||
42 | if (path.startsWith("/audio/") && path !== "/audio/intro.wav") target = path;
|
||||
43 | });
|
||||
44 | await page.goto("/");
|
||||
45 | await page.getByRole("radio", { name: "보기에서 선택" }).check();
|
||||
46 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
||||
47 | for (let index = 0; index < 3; index++) {
|
||||
48 | const bar = page.getByRole("progressbar");
|
||||
49 | expect.soft(await bar.evaluate((el) => (el as HTMLProgressElement).position)).toBeCloseTo((index + 1) / 3);
|
||||
50 | await expect.soft(bar).toHaveAccessibleName("현재 문항 진행률", { timeout: 1000 });
|
||||
51 | await bar.screenshot({ path: info.outputPath(`progress-${index + 1}.png`) });
|
||||
52 | if (index) await page.getByRole("button", { name: /문장 듣기/ }).click();
|
||||
53 | await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({ timeout: 30000 });
|
||||
54 | const answer = WORDS.find((w) => target.endsWith(`-${w.id}.wav`))!.answer;
|
||||
55 | await page.getByRole("button", { name: new RegExp(answer) }).click();
|
||||
56 | await page.getByRole("button", { name: index === 2 ? /결과 보기/ : /다음 문제/ }).click();
|
||||
57 | }
|
||||
58 | await page.screenshot({ path: info.outputPath("normal-results.png"), fullPage: true });
|
||||
> 59 | expect(await copyLayout(page, ".results-panel h2, .results-panel p, .stats span")).toEqual([]);
|
||||
| ^ Error: expect(received).toEqual(expected) // deep equality
|
||||
60 | });
|
||||
61 |
|
||||
```
|
||||
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 171 B |
|
After Width: | Height: | Size: 262 B |
|
After Width: | Height: | Size: 262 B |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,186 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: review-fixes.spec.ts >> 리뷰: 한국어 모드·결과 어절은 320px와 360px에서도 갈라지거나 넘치지 않는다
|
||||
- Location: tests/browser/review-fixes.spec.ts:24:1
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 4
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ "split: 탐험에서도",
|
||||
+ "split: 라운드",
|
||||
+ ]
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 3
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ "split: 입력",
|
||||
+ ]
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 4
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ "split: 만나요",
|
||||
+ "split: 최대",
|
||||
+ ]
|
||||
```
|
||||
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [ref=f1e2]:
|
||||
- link "본문으로 건너뛰기" [ref=f1e3] [cursor=pointer]:
|
||||
- /url: "#main"
|
||||
- banner [ref=f1e4]:
|
||||
- generic [ref=f1e5]:
|
||||
- heading "단어를 맞춰라" [level=1] [ref=f1e11]
|
||||
- generic [ref=f1e12]: v0.2.3
|
||||
- generic: 소리에 집중하는 시간
|
||||
- main [ref=f1e13]:
|
||||
- generic [ref=f1e14]:
|
||||
- text: 지금까지의 소리 탐험
|
||||
- generic [ref=f1e15]: S
|
||||
- heading "문제 풀이를 종료했어요." [active] [level=2] [ref=f1e23]
|
||||
- paragraph [ref=f1e24]: 작은 소리에 집중한 당신, 다음 탐험에서도 만나요.
|
||||
- paragraph [ref=f1e25]: 응답 0문항 · 정답 0문항
|
||||
- generic [ref=f1e26]:
|
||||
- generic [ref=f1e27]:
|
||||
- strong [ref=f1e28]: "0"
|
||||
- text: 총 점수
|
||||
- generic [ref=f1e29]:
|
||||
- strong [ref=f1e30]: 0%
|
||||
- generic [ref=f1e31]: 정답률 · 0/0
|
||||
- generic [ref=f1e32]:
|
||||
- strong [ref=f1e33]: 0회
|
||||
- text: 최고 연속 정답
|
||||
- table "난이도별 결과" [ref=f1e34]:
|
||||
- rowgroup [ref=f1e35]:
|
||||
- row [ref=f1e36]:
|
||||
- columnheader "난이도" [ref=f1e37]
|
||||
- columnheader "SNR" [ref=f1e38]
|
||||
- columnheader "맞힌 문제" [ref=f1e39]
|
||||
- rowgroup [ref=f1e40]:
|
||||
- row [ref=f1e41]:
|
||||
- rowheader "1단계" [ref=f1e42]
|
||||
- cell "SNR +6 dB" [ref=f1e43]
|
||||
- cell "0 / 0" [ref=f1e44]
|
||||
- row [ref=f1e45]:
|
||||
- rowheader "2단계" [ref=f1e46]
|
||||
- cell "SNR +3 dB" [ref=f1e47]
|
||||
- cell "0 / 0" [ref=f1e48]
|
||||
- row [ref=f1e49]:
|
||||
- rowheader "3단계" [ref=f1e50]
|
||||
- cell "SNR 0 dB" [ref=f1e51]
|
||||
- cell "0 / 0" [ref=f1e52]
|
||||
- row [ref=f1e53]:
|
||||
- rowheader "4단계" [ref=f1e54]
|
||||
- cell "SNR -3 dB" [ref=f1e55]
|
||||
- cell "0 / 0" [ref=f1e56]
|
||||
- row [ref=f1e57]:
|
||||
- rowheader "5단계" [ref=f1e58]
|
||||
- cell "SNR -6 dB" [ref=f1e59]
|
||||
- cell "0 / 0" [ref=f1e60]
|
||||
- paragraph [ref=f1e61]: 단계 × 100점 + 연속 정답 보너스 (라운드 내 최대 20점)
|
||||
- button "처음 화면으로" [ref=f1e62] [cursor=pointer]
|
||||
- generic [ref=f1e63]:
|
||||
- generic [ref=f1e64]: ◖)) 전체 음량
|
||||
- slider "전체 음량" [ref=f1e65] [cursor=pointer]: "35"
|
||||
- status [ref=f1e66]: 35%
|
||||
- contentinfo [ref=f1e67]:
|
||||
- paragraph [ref=f1e68]: Designed by OZ at Heat-T Medical Inc.
|
||||
- paragraph [ref=f1e69]: 청각적 주의·식별 게임 · 의료적 진단이나 치료를 제공하지 않습니다.
|
||||
- generic [ref=f1e70]: 개인정보 수집 없이, 오직 플레이만.
|
||||
- link "환경음 출처·라이선스" [ref=f1e71] [cursor=pointer]:
|
||||
- /url: /AMBIENCE_NOTICES.md
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
1 | import { expect, test, type Page } from "@playwright/test";
|
||||
2 | import { WORDS } from "../../src/content";
|
||||
3 |
|
||||
4 | // 글자별 스타일 추측 대신 실제 Range 줄 상자를 비교해 어절 내부 분리를 검출한다.
|
||||
5 | async function copyLayout(page: Page, selector: string) {
|
||||
6 | return page.locator(selector).evaluateAll((elements) => elements.flatMap((element) => {
|
||||
7 | const problems: string[] = [];
|
||||
8 | const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
||||
9 | while (walker.nextNode()) {
|
||||
10 | const node = walker.currentNode;
|
||||
11 | for (const match of (node.textContent ?? "").matchAll(/[가-힣]+/g)) {
|
||||
12 | const range = document.createRange();
|
||||
13 | range.setStart(node, match.index!);
|
||||
14 | range.setEnd(node, match.index! + match[0].length);
|
||||
15 | if (new Set(Array.from(range.getClientRects(), (r) => r.y)).size > 1)
|
||||
16 | problems.push(`split: ${match[0]}`);
|
||||
17 | }
|
||||
18 | }
|
||||
19 | if (element.scrollWidth > element.clientWidth) problems.push(`overflow: ${element.textContent}`);
|
||||
20 | return problems;
|
||||
21 | }));
|
||||
22 | }
|
||||
23 |
|
||||
24 | test("리뷰: 한국어 모드·결과 어절은 320px와 360px에서도 갈라지거나 넘치지 않는다", async ({ page }, info) => {
|
||||
25 | for (const width of [320, 360]) {
|
||||
26 | await page.setViewportSize({ width, height: 800 });
|
||||
27 | await page.goto("/");
|
||||
28 | expect.soft(await copyLayout(page, ".mode-option small")).toEqual([]);
|
||||
29 | await page.screenshot({ path: info.outputPath(`copy-${width}-home.png`), fullPage: true });
|
||||
30 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
||||
31 | await page.getByRole("button", { name: "문제 풀이 종료" }).click();
|
||||
> 32 | expect.soft(await copyLayout(page, ".results-panel h2, .results-panel p, .stats span, .results-panel th, .results-panel td")).toEqual([]);
|
||||
| ^ Error: expect(received).toEqual(expected) // deep equality
|
||||
33 | expect.soft(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
34 | await page.screenshot({ path: info.outputPath(`copy-${width}-results.png`), fullPage: true });
|
||||
35 | }
|
||||
36 | });
|
||||
37 |
|
||||
38 | test("리뷰: 실제 진행 막대는 현재 문항의 1/3·2/3·3/3을 채운다", async ({ page }, info) => {
|
||||
39 | let target = "";
|
||||
40 | page.on("request", (r) => {
|
||||
41 | const path = new URL(r.url()).pathname;
|
||||
42 | if (path.startsWith("/audio/") && path !== "/audio/intro.wav") target = path;
|
||||
43 | });
|
||||
44 | await page.goto("/");
|
||||
45 | await page.getByRole("radio", { name: "보기에서 선택" }).check();
|
||||
46 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
||||
47 | for (let index = 0; index < 3; index++) {
|
||||
48 | const bar = page.getByRole("progressbar");
|
||||
49 | expect.soft(await bar.evaluate((el) => (el as HTMLProgressElement).position)).toBeCloseTo((index + 1) / 3);
|
||||
50 | await expect.soft(bar).toHaveAccessibleName("현재 문항 진행률", { timeout: 1000 });
|
||||
51 | await bar.screenshot({ path: info.outputPath(`progress-${index + 1}.png`) });
|
||||
52 | if (index) await page.getByRole("button", { name: /문장 듣기/ }).click();
|
||||
53 | await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({ timeout: 30000 });
|
||||
54 | const answer = WORDS.find((w) => target.endsWith(`-${w.id}.wav`))!.answer;
|
||||
55 | await page.getByRole("button", { name: new RegExp(answer) }).click();
|
||||
56 | await page.getByRole("button", { name: index === 2 ? /결과 보기/ : /다음 문제/ }).click();
|
||||
57 | }
|
||||
58 | await page.screenshot({ path: info.outputPath("normal-results.png"), fullPage: true });
|
||||
59 | expect(await copyLayout(page, ".results-panel h2, .results-panel p, .stats span")).toEqual([]);
|
||||
60 | });
|
||||
61 |
|
||||
```
|
||||
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,186 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: review-fixes.spec.ts >> 리뷰: 한국어 모드·결과 어절은 320px와 360px에서도 갈라지거나 넘치지 않는다
|
||||
- Location: tests/browser/review-fixes.spec.ts:24:1
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 4
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ "split: 탐험에서도",
|
||||
+ "split: 라운드",
|
||||
+ ]
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 3
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ "split: 입력",
|
||||
+ ]
|
||||
```
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 4
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ "split: 만나요",
|
||||
+ "split: 최대",
|
||||
+ ]
|
||||
```
|
||||
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [ref=f1e2]:
|
||||
- link "본문으로 건너뛰기" [ref=f1e3] [cursor=pointer]:
|
||||
- /url: "#main"
|
||||
- banner [ref=f1e4]:
|
||||
- generic [ref=f1e5]:
|
||||
- heading "단어를 맞춰라" [level=1] [ref=f1e11]
|
||||
- generic [ref=f1e12]: v0.2.3
|
||||
- generic: 소리에 집중하는 시간
|
||||
- main [ref=f1e13]:
|
||||
- generic [ref=f1e14]:
|
||||
- text: 지금까지의 소리 탐험
|
||||
- generic [ref=f1e15]: S
|
||||
- heading "문제 풀이를 종료했어요." [active] [level=2] [ref=f1e23]
|
||||
- paragraph [ref=f1e24]: 작은 소리에 집중한 당신, 다음 탐험에서도 만나요.
|
||||
- paragraph [ref=f1e25]: 응답 0문항 · 정답 0문항
|
||||
- generic [ref=f1e26]:
|
||||
- generic [ref=f1e27]:
|
||||
- strong [ref=f1e28]: "0"
|
||||
- text: 총 점수
|
||||
- generic [ref=f1e29]:
|
||||
- strong [ref=f1e30]: 0%
|
||||
- generic [ref=f1e31]: 정답률 · 0/0
|
||||
- generic [ref=f1e32]:
|
||||
- strong [ref=f1e33]: 0회
|
||||
- text: 최고 연속 정답
|
||||
- table "난이도별 결과" [ref=f1e34]:
|
||||
- rowgroup [ref=f1e35]:
|
||||
- row [ref=f1e36]:
|
||||
- columnheader "난이도" [ref=f1e37]
|
||||
- columnheader "SNR" [ref=f1e38]
|
||||
- columnheader "맞힌 문제" [ref=f1e39]
|
||||
- rowgroup [ref=f1e40]:
|
||||
- row [ref=f1e41]:
|
||||
- rowheader "1단계" [ref=f1e42]
|
||||
- cell "SNR +6 dB" [ref=f1e43]
|
||||
- cell "0 / 0" [ref=f1e44]
|
||||
- row [ref=f1e45]:
|
||||
- rowheader "2단계" [ref=f1e46]
|
||||
- cell "SNR +3 dB" [ref=f1e47]
|
||||
- cell "0 / 0" [ref=f1e48]
|
||||
- row [ref=f1e49]:
|
||||
- rowheader "3단계" [ref=f1e50]
|
||||
- cell "SNR 0 dB" [ref=f1e51]
|
||||
- cell "0 / 0" [ref=f1e52]
|
||||
- row [ref=f1e53]:
|
||||
- rowheader "4단계" [ref=f1e54]
|
||||
- cell "SNR -3 dB" [ref=f1e55]
|
||||
- cell "0 / 0" [ref=f1e56]
|
||||
- row [ref=f1e57]:
|
||||
- rowheader "5단계" [ref=f1e58]
|
||||
- cell "SNR -6 dB" [ref=f1e59]
|
||||
- cell "0 / 0" [ref=f1e60]
|
||||
- paragraph [ref=f1e61]: 단계 × 100점 + 연속 정답 보너스 (라운드 내 최대 20점)
|
||||
- button "처음 화면으로" [ref=f1e62] [cursor=pointer]
|
||||
- generic [ref=f1e63]:
|
||||
- generic [ref=f1e64]: ◖)) 전체 음량
|
||||
- slider "전체 음량" [ref=f1e65] [cursor=pointer]: "35"
|
||||
- status [ref=f1e66]: 35%
|
||||
- contentinfo [ref=f1e67]:
|
||||
- paragraph [ref=f1e68]: Designed by OZ at Heat-T Medical Inc.
|
||||
- paragraph [ref=f1e69]: 청각적 주의·식별 게임 · 의료적 진단이나 치료를 제공하지 않습니다.
|
||||
- generic [ref=f1e70]: 개인정보 수집 없이, 오직 플레이만.
|
||||
- link "환경음 출처·라이선스" [ref=f1e71] [cursor=pointer]:
|
||||
- /url: /AMBIENCE_NOTICES.md
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
1 | import { expect, test, type Page } from "@playwright/test";
|
||||
2 | import { WORDS } from "../../src/content";
|
||||
3 |
|
||||
4 | // 글자별 스타일 추측 대신 실제 Range 줄 상자를 비교해 어절 내부 분리를 검출한다.
|
||||
5 | async function copyLayout(page: Page, selector: string) {
|
||||
6 | return page.locator(selector).evaluateAll((elements) => elements.flatMap((element) => {
|
||||
7 | const problems: string[] = [];
|
||||
8 | const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
||||
9 | while (walker.nextNode()) {
|
||||
10 | const node = walker.currentNode;
|
||||
11 | for (const match of (node.textContent ?? "").matchAll(/[가-힣]+/g)) {
|
||||
12 | const range = document.createRange();
|
||||
13 | range.setStart(node, match.index!);
|
||||
14 | range.setEnd(node, match.index! + match[0].length);
|
||||
15 | if (new Set(Array.from(range.getClientRects(), (r) => r.y)).size > 1)
|
||||
16 | problems.push(`split: ${match[0]}`);
|
||||
17 | }
|
||||
18 | }
|
||||
19 | if (element.scrollWidth > element.clientWidth) problems.push(`overflow: ${element.textContent}`);
|
||||
20 | return problems;
|
||||
21 | }));
|
||||
22 | }
|
||||
23 |
|
||||
24 | test("리뷰: 한국어 모드·결과 어절은 320px와 360px에서도 갈라지거나 넘치지 않는다", async ({ page }, info) => {
|
||||
25 | for (const width of [320, 360]) {
|
||||
26 | await page.setViewportSize({ width, height: 800 });
|
||||
27 | await page.goto("/");
|
||||
28 | expect.soft(await copyLayout(page, ".mode-option small")).toEqual([]);
|
||||
29 | await page.screenshot({ path: info.outputPath(`copy-${width}-home.png`), fullPage: true });
|
||||
30 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
||||
31 | await page.getByRole("button", { name: "문제 풀이 종료" }).click();
|
||||
> 32 | expect.soft(await copyLayout(page, ".results-panel h2, .results-panel p, .stats span, .results-panel th, .results-panel td")).toEqual([]);
|
||||
| ^ Error: expect(received).toEqual(expected) // deep equality
|
||||
33 | expect.soft(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
34 | await page.screenshot({ path: info.outputPath(`copy-${width}-results.png`), fullPage: true });
|
||||
35 | }
|
||||
36 | });
|
||||
37 |
|
||||
38 | test("리뷰: 실제 진행 막대는 현재 문항의 1/3·2/3·3/3을 채운다", async ({ page }, info) => {
|
||||
39 | let target = "";
|
||||
40 | page.on("request", (r) => {
|
||||
41 | const path = new URL(r.url()).pathname;
|
||||
42 | if (path.startsWith("/audio/") && path !== "/audio/intro.wav") target = path;
|
||||
43 | });
|
||||
44 | await page.goto("/");
|
||||
45 | await page.getByRole("radio", { name: "보기에서 선택" }).check();
|
||||
46 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
|
||||
47 | for (let index = 0; index < 3; index++) {
|
||||
48 | const bar = page.getByRole("progressbar");
|
||||
49 | expect.soft(await bar.evaluate((el) => (el as HTMLProgressElement).position)).toBeCloseTo((index + 1) / 3);
|
||||
50 | await expect.soft(bar).toHaveAccessibleName("현재 문항 진행률", { timeout: 1000 });
|
||||
51 | await bar.screenshot({ path: info.outputPath(`progress-${index + 1}.png`) });
|
||||
52 | if (index) await page.getByRole("button", { name: /문장 듣기/ }).click();
|
||||
53 | await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({ timeout: 30000 });
|
||||
54 | const answer = WORDS.find((w) => target.endsWith(`-${w.id}.wav`))!.answer;
|
||||
55 | await page.getByRole("button", { name: new RegExp(answer) }).click();
|
||||
56 | await page.getByRole("button", { name: index === 2 ? /결과 보기/ : /다음 문제/ }).click();
|
||||
57 | }
|
||||
58 | await page.screenshot({ path: info.outputPath("normal-results.png"), fullPage: true });
|
||||
59 | expect(await copyLayout(page, ".results-panel h2, .results-panel p, .stats span")).toEqual([]);
|
||||
60 | });
|
||||
61 |
|
||||
```
|
||||