- 오셀로: 8방향 뒤집기, 자동 패스, 종료·점수(빈칸 승자 몫 옵션), 한 수/피셔 시계·자동 착수, 무르기·무승부·기권, 놓을 곳 점·뒤집힐 돌 미리 보기·뒤집기 애니메이션 (테스트 31개) - 카탈로그에 오셀로·체스·장기·바둑 추가(엔진이 등록된 게임만 화면에 나옴) - e2e/games.e2e.ts: PC 방장 + 휴대폰 친구, 게임 소개→방 만들기→링크 입장→시작→차례대로 첫 수 - e2e/a11y.e2e.ts: 홈·게임 소개·대기실 × PC/휴대폰 × 밝게/어둡게, axe 위반 0건 (h1, 랜드마크, 제목 순서 정리) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.8 KiB
TypeScript
59 lines
2.8 KiB
TypeScript
/**
|
|
* Accessibility check with axe-core (docs/10 §2): home, game page, lobby, game screen (PC + phone).
|
|
* Fails on "serious"/"critical" violations. Run: bun e2e/a11y.e2e.ts
|
|
*/
|
|
import { chromium, type Page } from 'playwright-core';
|
|
import { mkdtempSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { loadConfig } from '../apps/server/src/config';
|
|
import { startServer } from '../apps/server/src/server';
|
|
|
|
const PORT = 4520;
|
|
const ORIGIN = `http://localhost:${PORT}`;
|
|
const AXE = readFileSync(require.resolve('axe-core/axe.min.js'), 'utf8');
|
|
const app = startServer({
|
|
config: loadConfig({ PORT: String(PORT), DB_PATH: join(mkdtempSync(join(process.env.TMPDIR ?? '/tmp', 'bg-a11y-')), 'app.db'), PUBLIC_ORIGIN: ORIGIN }),
|
|
log: () => {},
|
|
staticDir: join(import.meta.dir, '../apps/web/dist'),
|
|
});
|
|
const browser = await chromium.launch({ executablePath: process.env.CHROME ?? '/usr/bin/google-chrome', headless: true });
|
|
let bad = 0;
|
|
|
|
async function audit(page: Page, name: string) {
|
|
await page.addScriptTag({ content: AXE });
|
|
const res = await page.evaluate(async () => (window as any).axe.run(document, { resultTypes: ['violations'] }));
|
|
const serious = res.violations.filter((v: any) => v.impact === 'serious' || v.impact === 'critical');
|
|
const minor = res.violations.length - serious.length;
|
|
if (serious.length) {
|
|
bad += serious.length;
|
|
console.log(`✗ ${name}: ${serious.map((v: any) => `${v.id}(${v.nodes.length}) ${v.nodes[0]?.target?.join(' ')}`).join('; ')}`);
|
|
} else console.log(`✓ ${name}${minor ? ` (경미: ${res.violations.map((v: any) => v.id).join(', ')})` : ''}`);
|
|
}
|
|
|
|
for (const [label, ctxOpts] of [
|
|
['PC', { viewport: { width: 1440, height: 900 } }],
|
|
['phone', { viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true }],
|
|
] as const) {
|
|
for (const theme of ['light', 'dark'] as const) {
|
|
const ctx = await browser.newContext({ ...ctxOpts, locale: 'ko-KR', colorScheme: theme });
|
|
const p = await ctx.newPage();
|
|
await p.goto(ORIGIN + '/');
|
|
await p.getByText('게임 고르기').waitFor();
|
|
await audit(p, `${label}/${theme} 홈`);
|
|
await p.goto(ORIGIN + '/g/omok');
|
|
await p.getByRole('button', { name: /친구랑 하기/ }).waitFor();
|
|
await audit(p, `${label}/${theme} 게임 소개`);
|
|
await p.getByRole('button', { name: /친구랑 하기/ }).click();
|
|
await p.getByLabel('내 이름(닉네임)').fill(`검사${label}${theme}`.slice(0, 12));
|
|
await p.getByRole('button', { name: '이 이름으로 시작하기' }).click();
|
|
await p.waitForURL(/\/r\/\d{6}$/);
|
|
await p.getByText('친구에게 이 코드를 알려 주세요').waitFor();
|
|
await audit(p, `${label}/${theme} 대기실`);
|
|
await ctx.close();
|
|
}
|
|
}
|
|
await browser.close();
|
|
await app.stop();
|
|
console.log(bad ? `A11Y FAILED (${bad})` : 'A11Y PASSED');
|
|
process.exit(bad ? 1 : 0);
|