From 8aae7b58fc5654d23074bfaf2f29b79f50cab5bb Mon Sep 17 00:00:00 2001 From: Jaybe Date: Fri, 14 Nov 2025 20:29:54 +0900 Subject: [PATCH 1/2] test(e2e): add exported-page a11y smoke (focus outline, skip-link tab order) --- MAIN_PLAN.md | 7 ++++ tests/e2e/export-focus-outline.e2e.ts | 42 +++++++++++++++++++++++ tests/e2e/export-tab-order.e2e.ts | 49 +++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 tests/e2e/export-focus-outline.e2e.ts create mode 100644 tests/e2e/export-tab-order.e2e.ts diff --git a/MAIN_PLAN.md b/MAIN_PLAN.md index 5008ab1..47ea38c 100644 --- a/MAIN_PLAN.md +++ b/MAIN_PLAN.md @@ -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] diff --git a/tests/e2e/export-focus-outline.e2e.ts b/tests/e2e/export-focus-outline.e2e.ts new file mode 100644 index 0000000..7f611fa --- /dev/null +++ b/tests/e2e/export-focus-outline.e2e.ts @@ -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() +}) diff --git a/tests/e2e/export-tab-order.e2e.ts b/tests/e2e/export-tab-order.e2e.ts new file mode 100644 index 0000000..7a1e26b --- /dev/null +++ b/tests/e2e/export-tab-order.e2e.ts @@ -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
+// 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() +}) -- 2.52.0 From d2928501f6543b0eddd95edb29190b2f265bceee Mon Sep 17 00:00:00 2001 From: Jaybe Date: Fri, 14 Nov 2025 20:36:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(exporter):=20=EC=8A=A4=ED=82=B5?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=ED=99=9C=EC=84=B1=ED=99=94=20=EC=8B=9C=20?= =?UTF-8?q?main=EC=97=90=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/exporter/html.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/exporter/html.ts b/lib/exporter/html.ts index 05e4422..45ce4a0 100644 --- a/lib/exporter/html.ts +++ b/lib/exporter/html.ts @@ -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(); + }); + } })(); ` } -- 2.52.0