test(e2e): add exported-page a11y smoke (focus outline, skip-link tab order) #4

Merged
jaybe merged 2 commits from feat/ci-verify-run into main 2025-11-14 11:49:30 +00:00
4 changed files with 109 additions and 0 deletions
+7
View File
@@ -144,6 +144,13 @@
- CI verification note: 러너 온라인 이후, 사소 변경으로 CI 트리거 검증 예정
- CI rerun: 워크플로 재실행용 ping 커밋
### PR/CI 결과(2025-11-14)
- PR: feat/ci-verify-run → main 머지 완료
- CI: Vitest + Playwright 전부 그린
- E2E 추가: Exported page skip-link focus 표시 회귀 테스트 포함
- 워크플로: self-hosted runner [macos, host], Playwright 보고서 업로드 아티팩트 활성화
- 제안: 다음 릴리스 태그(v0.2.0)로 회귀 커버리지/Benefits 섹션 추가 반영
#### 결과: CI 통과 (self-hosted Gitea Actions)
- PR: https://gitea.jaybe.dev/jaybe/landing-builder/pulls/2
- 러너: self-hosted, labels: [macos, host]
+11
View File
@@ -282,6 +282,17 @@ function buildJs() {
(function(){
var ts = document.querySelector('input[name="_ts"]');
if(ts){ ts.value = String(Date.now()); }
// Ensure skip-link moves focus into main for a11y
var skip = document.querySelector('a.skip-link');
var main = document.getElementById('main');
if (skip && main) {
skip.addEventListener('click', function(){
if (!main.hasAttribute('tabindex')) {
main.setAttribute('tabindex', '-1');
}
main.focus();
});
}
})();
`
}
+42
View File
@@ -0,0 +1,42 @@
import { test, expect } from '@playwright/test'
type WindowWithExport = Window & {
__exportPreview?: () => { html: string; css: string; js: string }
}
const builderUrl = '/en/builder'
// Verify that focus outline tokens from CSS are applied in a real browser context
// by focusing the submit button in the exported page and inspecting computed styles.
test('Exported page shows 3px solid outline color on focused button', async ({ page, context }) => {
await page.goto(builderUrl)
// Minimal content to export
await page.getByTestId('card-hero').click()
await page.waitForFunction(() => Boolean((window as unknown as WindowWithExport).__exportPreview), { timeout: 5000 })
const exported = await page.evaluate(() => (window as unknown as WindowWithExport).__exportPreview?.() ?? null)
expect(exported).not.toBeNull()
const preview = await context.newPage()
await preview.setContent(exported!.html)
await preview.addStyleTag({ content: exported!.css })
const submit = preview.locator('button[type="submit"]')
await expect(submit).toHaveCount(1)
await submit.focus()
// Read computed style in the page context
const style = await preview.evaluate(() => {
const el = document.querySelector('button[type="submit"]') as HTMLElement | null
if (!el) return null
const cs = window.getComputedStyle(el)
return { outlineWidth: cs.outlineWidth, outlineStyle: cs.outlineStyle, outlineColor: cs.outlineColor }
})
expect(style).not.toBeNull()
// Expect 3px solid and brand color (#2563eb => rgb(37, 99, 235))
expect(style!.outlineWidth).toBe('3px')
expect(style!.outlineStyle).toBe('solid')
expect(style!.outlineColor.replace(/\s+/g, '')).toMatch(/rgb\(37,99,235\)|#2563eb/i)
await preview.close()
})
+49
View File
@@ -0,0 +1,49 @@
import { test, expect } from '@playwright/test'
type WindowWithExport = Window & {
__exportPreview?: () => { html: string; css: string; js: string }
}
const builderUrl = '/en/builder'
// Verify tab order after activating the skip-link: focus should move into <main>
// Then the first Tab should reach a focusable descendant (e.g., form submit button)
test('Skip-link Enter moves focus into main; next Tab reaches first focusable', async ({ page, context }) => {
await page.goto(builderUrl)
await page.getByTestId('card-hero').click()
await page.waitForFunction(() => Boolean((window as unknown as WindowWithExport).__exportPreview), { timeout: 5000 })
const exported = await page.evaluate(() => (window as unknown as WindowWithExport).__exportPreview?.() ?? null)
expect(exported).not.toBeNull()
const preview = await context.newPage()
await preview.setContent(exported!.html)
await preview.addStyleTag({ content: exported!.css })
const skip = preview.locator('a.skip-link')
await expect(skip).toHaveCount(1)
await skip.focus()
await expect(skip).toBeFocused()
// Activate skip-link
await preview.keyboard.press('Enter')
// Focus should be inside #main or on #main
const focusedInMain = await preview.evaluate(() => {
const main = document.getElementById('main')!
const active = document.activeElement as HTMLElement | null
if (!active) return false
return active === main || main.contains(active)
})
expect(focusedInMain).toBeTruthy()
// Press Tab and expect a focusable inside main (submit button) to receive focus
await preview.keyboard.press('Tab')
const isSubmitFocused = await preview.evaluate(() => {
const active = document.activeElement as HTMLElement | null
return !!active && active.tagName.toLowerCase() === 'button' && (active as HTMLButtonElement).type === 'submit'
})
expect(isSubmitFocused).toBeTruthy()
await preview.close()
})