From 305a0a6cd625b649a447a529d9e07754ecd2881a Mon Sep 17 00:00:00 2001 From: yuta Date: Sun, 26 Jul 2026 13:55:24 +0900 Subject: [PATCH] Add Axe accessibility E2E tests --- README.md | 6 +++-- e2e/a11y/homePage.test.ts | 12 +++++++++ e2e/a11y/listsPage.test.ts | 24 ++++++++++++++++++ e2e/a11y/searchPage.test.ts | 24 ++++++++++++++++++ e2e/a11y/threadPage.test.ts | 12 +++++++++ e2e/a11y/userPage.test.ts | 12 +++++++++ {tests/e2e => e2e}/fixtures.ts | 10 ++++++++ .../integrations/reader.test.ts | 2 +- .../mist-thread-desktop-linux.png | Bin .../mist-thread-mobile-linux.png | Bin .../mist-user-desktop-linux.png | Bin .../mist-user-mobile-linux.png | Bin {tests/e2e => e2e}/mock-relay.mjs | 0 {tests/e2e => e2e}/models/Base.ts | 0 {tests/e2e => e2e}/models/HomePage.ts | 0 {tests/e2e => e2e}/models/ListsPage.ts | 0 {tests/e2e => e2e}/models/SearchPage.ts | 0 {tests/e2e => e2e}/models/ThreadPage.ts | 0 {tests/e2e => e2e}/models/UserPage.ts | 0 knip.ts | 6 +---- package.json | 1 + playwright.config.ts | 4 +-- pnpm-lock.yaml | 19 ++++++++++++++ tsconfig.json | 2 +- vitest.config.ts | 2 +- 25 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 e2e/a11y/homePage.test.ts create mode 100644 e2e/a11y/listsPage.test.ts create mode 100644 e2e/a11y/searchPage.test.ts create mode 100644 e2e/a11y/threadPage.test.ts create mode 100644 e2e/a11y/userPage.test.ts rename {tests/e2e => e2e}/fixtures.ts (72%) rename tests/e2e/reader.spec.ts => e2e/integrations/reader.test.ts (99%) rename {tests/e2e/reader.spec.ts-snapshots => e2e/integrations/reader.test.ts-snapshots}/mist-thread-desktop-linux.png (100%) rename {tests/e2e/reader.spec.ts-snapshots => e2e/integrations/reader.test.ts-snapshots}/mist-thread-mobile-linux.png (100%) rename {tests/e2e/reader.spec.ts-snapshots => e2e/integrations/reader.test.ts-snapshots}/mist-user-desktop-linux.png (100%) rename {tests/e2e/reader.spec.ts-snapshots => e2e/integrations/reader.test.ts-snapshots}/mist-user-mobile-linux.png (100%) rename {tests/e2e => e2e}/mock-relay.mjs (100%) rename {tests/e2e => e2e}/models/Base.ts (100%) rename {tests/e2e => e2e}/models/HomePage.ts (100%) rename {tests/e2e => e2e}/models/ListsPage.ts (100%) rename {tests/e2e => e2e}/models/SearchPage.ts (100%) rename {tests/e2e => e2e}/models/ThreadPage.ts (100%) rename {tests/e2e => e2e}/models/UserPage.ts (100%) diff --git a/README.md b/README.md index 0012250..45e1456 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,10 @@ nix develop -c pnpm start ``` `test:e2e` is intentionally Nix-only and uses the system Chromium supplied by -the dev shell. `test:live` is an explicit, opt-in smoke test that performs -exactly one read-only Top search using the configured relay. +the dev shell. Browser tests are organized under `e2e/`: page object models +and fixtures are shared by integration flows and Axe checks for WCAG 2.0/2.1 +A and AA. `test:live` is an explicit, opt-in smoke test that performs exactly +one read-only Top search using the configured relay. Development binds to `127.0.0.1` by default. `dev:tailscale` binds to `0.0.0.0`, so it exposes the app on LAN interfaces as well as Tailscale. Use it diff --git a/e2e/a11y/homePage.test.ts b/e2e/a11y/homePage.test.ts new file mode 100644 index 0000000..5ae7ee6 --- /dev/null +++ b/e2e/a11y/homePage.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from '../fixtures' + +test('home page has no accessibility violations', async ({ + a11y, + homePage, +}) => { + await homePage.goTo() + await expect(homePage.articlesLocator).toHaveCount(0) + + const result = await a11y().analyze() + expect(result.violations).toEqual([]) +}) diff --git a/e2e/a11y/listsPage.test.ts b/e2e/a11y/listsPage.test.ts new file mode 100644 index 0000000..889fd04 --- /dev/null +++ b/e2e/a11y/listsPage.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '../fixtures' + +test('list picker has no accessibility violations', async ({ + a11y, + listsPage, +}) => { + await listsPage.goTo() + await expect(listsPage.articlesLocator).toHaveCount(0) + + const result = await a11y().analyze() + expect(result.violations).toEqual([]) +}) + +test('list timeline has no accessibility violations', async ({ + a11y, + listsPage, +}) => { + await listsPage.goTo() + await listsPage.openList(/My E2E List.*10 メンバー/) + await expect(listsPage.endOfFeedLocator).toBeVisible() + + const result = await a11y().analyze() + expect(result.violations).toEqual([]) +}) diff --git a/e2e/a11y/searchPage.test.ts b/e2e/a11y/searchPage.test.ts new file mode 100644 index 0000000..8a440f4 --- /dev/null +++ b/e2e/a11y/searchPage.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '../fixtures' + +test('empty search page has no accessibility violations', async ({ + a11y, + searchPage, +}) => { + await searchPage.goTo() + await expect(searchPage.articlesLocator).toHaveCount(0) + + const result = await a11y().analyze() + expect(result.violations).toEqual([]) +}) + +test('search results have no accessibility violations', async ({ + a11y, + searchPage, +}) => { + await searchPage.goTo() + await searchPage.search('AI lang:ja') + await expect(searchPage.endOfFeedLocator).toBeVisible() + + const result = await a11y().analyze() + expect(result.violations).toEqual([]) +}) diff --git a/e2e/a11y/threadPage.test.ts b/e2e/a11y/threadPage.test.ts new file mode 100644 index 0000000..1990055 --- /dev/null +++ b/e2e/a11y/threadPage.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from '../fixtures' + +test('conversation has no accessibility violations', async ({ + a11y, + threadPage, +}) => { + await threadPage.goTo('yuta', '1001') + await expect(threadPage.endOfFeedLocator).toBeVisible() + + const result = await a11y().analyze() + expect(result.violations).toEqual([]) +}) diff --git a/e2e/a11y/userPage.test.ts b/e2e/a11y/userPage.test.ts new file mode 100644 index 0000000..9c12605 --- /dev/null +++ b/e2e/a11y/userPage.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from '../fixtures' + +test('user timeline has no accessibility violations', async ({ + a11y, + userPage, +}) => { + await userPage.goTo('yuta') + await expect(userPage.endOfFeedLocator).toBeVisible() + + const result = await a11y().analyze() + expect(result.violations).toEqual([]) +}) diff --git a/tests/e2e/fixtures.ts b/e2e/fixtures.ts similarity index 72% rename from tests/e2e/fixtures.ts rename to e2e/fixtures.ts index c8700e3..301675e 100644 --- a/tests/e2e/fixtures.ts +++ b/e2e/fixtures.ts @@ -1,3 +1,4 @@ +import AxeBuilder from '@axe-core/playwright' import { test as base } from '@playwright/test' import { HomePage } from './models/HomePage' import { ListsPage } from './models/ListsPage' @@ -6,6 +7,7 @@ import { ThreadPage } from './models/ThreadPage' import { UserPage } from './models/UserPage' type Fixtures = { + a11y: () => AxeBuilder homePage: HomePage listsPage: ListsPage searchPage: SearchPage @@ -14,6 +16,14 @@ type Fixtures = { } export const test = base.extend({ + a11y: async ({ page }, use) => { + const makeAxeBuilder = () => + new AxeBuilder({ page }) + .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) + .disableRules(['meta-viewport']) + + await use(makeAxeBuilder) + }, homePage: ({ page }, use) => use(new HomePage(page)), listsPage: ({ page }, use) => use(new ListsPage(page)), searchPage: ({ page }, use) => use(new SearchPage(page)), diff --git a/tests/e2e/reader.spec.ts b/e2e/integrations/reader.test.ts similarity index 99% rename from tests/e2e/reader.spec.ts rename to e2e/integrations/reader.test.ts index 2e369cd..81d4aa3 100644 --- a/tests/e2e/reader.spec.ts +++ b/e2e/integrations/reader.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from './fixtures' +import { expect, test } from '../fixtures' let consoleErrors: string[] let pageErrors: string[] diff --git a/tests/e2e/reader.spec.ts-snapshots/mist-thread-desktop-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png similarity index 100% rename from tests/e2e/reader.spec.ts-snapshots/mist-thread-desktop-linux.png rename to e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png diff --git a/tests/e2e/reader.spec.ts-snapshots/mist-thread-mobile-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png similarity index 100% rename from tests/e2e/reader.spec.ts-snapshots/mist-thread-mobile-linux.png rename to e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png diff --git a/tests/e2e/reader.spec.ts-snapshots/mist-user-desktop-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png similarity index 100% rename from tests/e2e/reader.spec.ts-snapshots/mist-user-desktop-linux.png rename to e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png diff --git a/tests/e2e/reader.spec.ts-snapshots/mist-user-mobile-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png similarity index 100% rename from tests/e2e/reader.spec.ts-snapshots/mist-user-mobile-linux.png rename to e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png diff --git a/tests/e2e/mock-relay.mjs b/e2e/mock-relay.mjs similarity index 100% rename from tests/e2e/mock-relay.mjs rename to e2e/mock-relay.mjs diff --git a/tests/e2e/models/Base.ts b/e2e/models/Base.ts similarity index 100% rename from tests/e2e/models/Base.ts rename to e2e/models/Base.ts diff --git a/tests/e2e/models/HomePage.ts b/e2e/models/HomePage.ts similarity index 100% rename from tests/e2e/models/HomePage.ts rename to e2e/models/HomePage.ts diff --git a/tests/e2e/models/ListsPage.ts b/e2e/models/ListsPage.ts similarity index 100% rename from tests/e2e/models/ListsPage.ts rename to e2e/models/ListsPage.ts diff --git a/tests/e2e/models/SearchPage.ts b/e2e/models/SearchPage.ts similarity index 100% rename from tests/e2e/models/SearchPage.ts rename to e2e/models/SearchPage.ts diff --git a/tests/e2e/models/ThreadPage.ts b/e2e/models/ThreadPage.ts similarity index 100% rename from tests/e2e/models/ThreadPage.ts rename to e2e/models/ThreadPage.ts diff --git a/tests/e2e/models/UserPage.ts b/e2e/models/UserPage.ts similarity index 100% rename from tests/e2e/models/UserPage.ts rename to e2e/models/UserPage.ts diff --git a/knip.ts b/knip.ts index bd716cb..6ab2dc2 100644 --- a/knip.ts +++ b/knip.ts @@ -1,10 +1,6 @@ import type { KnipConfig } from 'knip' export default { - entry: [ - 'playwright.config.ts', - 'tests/e2e/**/*.spec.ts', - 'tests/e2e/mock-relay.mjs', - ], + entry: ['playwright.config.ts', 'e2e/**/*.test.ts', 'e2e/mock-relay.mjs'], playwright: false, } satisfies KnipConfig diff --git a/package.json b/package.json index 5f5e4d6..14a5252 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "zod": "4.4.3" }, "devDependencies": { + "@axe-core/playwright": "4.11.1", "@biomejs/biome": "2.5.3", "@playwright/test": "1.61.1", "@testing-library/dom": "10.4.1", diff --git a/playwright.config.ts b/playwright.config.ts index a10fdeb..aef387f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,7 +11,7 @@ if (!chromiumExecutable) { } export default defineConfig({ - testDir: './tests/e2e', + testDir: './e2e', fullyParallel: false, workers: 1, use: { @@ -22,7 +22,7 @@ export default defineConfig({ }, webServer: [ { - command: `TWITTER_LITE_MOCK_RELAY_PORT=${relayPort} node tests/e2e/mock-relay.mjs`, + command: `TWITTER_LITE_MOCK_RELAY_PORT=${relayPort} node e2e/mock-relay.mjs`, port: relayPort, reuseExistingServer: false, timeout: 120_000, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4edaa0f..e808d78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: specifier: 4.4.3 version: 4.4.3 devDependencies: + '@axe-core/playwright': + specifier: 4.11.1 + version: 4.11.1(playwright-core@1.61.1) '@biomejs/biome': specifier: 2.5.3 version: 2.5.3 @@ -99,6 +102,11 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@axe-core/playwright@4.11.1': + resolution: {integrity: sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1074,6 +1082,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + axe-core@4.11.4: + resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} + engines: {node: '>=4'} + babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} @@ -1982,6 +1994,11 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@axe-core/playwright@4.11.1(playwright-core@1.61.1)': + dependencies: + axe-core: 4.11.4 + playwright-core: 1.61.1 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2843,6 +2860,8 @@ snapshots: assertion-error@2.0.1: {} + axe-core@4.11.4: {} + babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.7 diff --git a/tsconfig.json b/tsconfig.json index 4ccd5dc..9e69649 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "e2e/**/*.ts"], "compilerOptions": { "target": "ES2022", "jsx": "react-jsx", diff --git a/vitest.config.ts b/vitest.config.ts index 89abc01..807bd15 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,6 +6,6 @@ export default defineConfig({ test: { environment: 'jsdom', setupFiles: ['./tests/setup.ts'], - exclude: [...configDefaults.exclude, 'tests/e2e/**'], + exclude: [...configDefaults.exclude, 'e2e/**'], }, }) -- 2.55.0