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 = Number(process.env.E2E_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);
|