Add Axe accessibility E2E tests
@@ -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
|
||||||
|
|||||||
@@ -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([])
|
||||||
|
})
|
||||||
@@ -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([])
|
||||||
|
})
|
||||||
@@ -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([])
|
||||||
|
})
|
||||||
@@ -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([])
|
||||||
|
})
|
||||||
@@ -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([])
|
||||||
|
})
|
||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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/**'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||