Add Axe accessibility E2E tests #2

Merged
yuta merged 1 commits from agent/e2e-a11y into main 2026-07-26 13:55:56 +09:00
25 changed files with 124 additions and 12 deletions
+4 -2
View File
@@ -68,8 +68,10 @@ nix develop -c pnpm start
``` ```
`test:e2e` is intentionally Nix-only and uses the system Chromium supplied by `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 the dev shell. Browser tests are organized under `e2e/`: page object models
exactly one read-only Top search using the configured relay. 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 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 `0.0.0.0`, so it exposes the app on LAN interfaces as well as Tailscale. Use it
+12
View File
@@ -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([])
})
+24
View File
@@ -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([])
})
+24
View File
@@ -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([])
})
+12
View File
@@ -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([])
})
+12
View File
@@ -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([])
})
+10
View File
@@ -1,3 +1,4 @@
import AxeBuilder from '@axe-core/playwright'
import { test as base } from '@playwright/test' import { test as base } from '@playwright/test'
import { HomePage } from './models/HomePage' import { HomePage } from './models/HomePage'
import { ListsPage } from './models/ListsPage' import { ListsPage } from './models/ListsPage'
@@ -6,6 +7,7 @@ import { ThreadPage } from './models/ThreadPage'
import { UserPage } from './models/UserPage' import { UserPage } from './models/UserPage'
type Fixtures = { type Fixtures = {
a11y: () => AxeBuilder
homePage: HomePage homePage: HomePage
listsPage: ListsPage listsPage: ListsPage
searchPage: SearchPage searchPage: SearchPage
@@ -14,6 +16,14 @@ type Fixtures = {
} }
export const test = base.extend<Fixtures>({ export const test = base.extend<Fixtures>({
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)), homePage: ({ page }, use) => use(new HomePage(page)),
listsPage: ({ page }, use) => use(new ListsPage(page)), listsPage: ({ page }, use) => use(new ListsPage(page)),
searchPage: ({ page }, use) => use(new SearchPage(page)), searchPage: ({ page }, use) => use(new SearchPage(page)),
@@ -1,4 +1,4 @@
import { expect, test } from './fixtures' import { expect, test } from '../fixtures'
let consoleErrors: string[] let consoleErrors: string[]
let pageErrors: string[] let pageErrors: string[]

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

+1 -5
View File
@@ -1,10 +1,6 @@
import type { KnipConfig } from 'knip' import type { KnipConfig } from 'knip'
export default { export default {
entry: [ entry: ['playwright.config.ts', 'e2e/**/*.test.ts', 'e2e/mock-relay.mjs'],
'playwright.config.ts',
'tests/e2e/**/*.spec.ts',
'tests/e2e/mock-relay.mjs',
],
playwright: false, playwright: false,
} satisfies KnipConfig } satisfies KnipConfig
+1
View File
@@ -37,6 +37,7 @@
"zod": "4.4.3" "zod": "4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "4.11.1",
"@biomejs/biome": "2.5.3", "@biomejs/biome": "2.5.3",
"@playwright/test": "1.61.1", "@playwright/test": "1.61.1",
"@testing-library/dom": "10.4.1", "@testing-library/dom": "10.4.1",
+2 -2
View File
@@ -11,7 +11,7 @@ if (!chromiumExecutable) {
} }
export default defineConfig({ export default defineConfig({
testDir: './tests/e2e', testDir: './e2e',
fullyParallel: false, fullyParallel: false,
workers: 1, workers: 1,
use: { use: {
@@ -22,7 +22,7 @@ export default defineConfig({
}, },
webServer: [ 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, port: relayPort,
reuseExistingServer: false, reuseExistingServer: false,
timeout: 120_000, timeout: 120_000,
+19
View File
@@ -36,6 +36,9 @@ importers:
specifier: 4.4.3 specifier: 4.4.3
version: 4.4.3 version: 4.4.3
devDependencies: devDependencies:
'@axe-core/playwright':
specifier: 4.11.1
version: 4.11.1([email protected])
'@biomejs/biome': '@biomejs/biome':
specifier: 2.5.3 specifier: 2.5.3
version: 2.5.3 version: 2.5.3
@@ -99,6 +102,11 @@ packages:
'@asamuzakjp/[email protected]': '@asamuzakjp/[email protected]':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
'@axe-core/[email protected]':
resolution: {integrity: sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==}
peerDependencies:
playwright-core: '>= 1.0.0'
'@babel/[email protected]': '@babel/[email protected]':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1074,6 +1082,10 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
[email protected]:
resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==}
engines: {node: '>=4'}
[email protected]: [email protected]:
resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==}
@@ -1982,6 +1994,11 @@ snapshots:
'@asamuzakjp/[email protected]': {} '@asamuzakjp/[email protected]': {}
'@axe-core/[email protected]([email protected])':
dependencies:
axe-core: 4.11.4
playwright-core: 1.61.1
'@babel/[email protected]': '@babel/[email protected]':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.29.7 '@babel/helper-validator-identifier': 7.29.7
@@ -2843,6 +2860,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"], "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "e2e/**/*.ts"],
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"jsx": "react-jsx", "jsx": "react-jsx",
+1 -1
View File
@@ -6,6 +6,6 @@ export default defineConfig({
test: { test: {
environment: 'jsdom', environment: 'jsdom',
setupFiles: ['./tests/setup.ts'], setupFiles: ['./tests/setup.ts'],
exclude: [...configDefaults.exclude, 'tests/e2e/**'], exclude: [...configDefaults.exclude, 'e2e/**'],
}, },
}) })