Skip to content

Commit e70fd04

Browse files
author
Mark Skelton
committed
feat: Initial release
1 parent 74b8d25 commit e70fd04

File tree

13 files changed

+226
-59
lines changed

13 files changed

+226
-59
lines changed

.eslintrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
{
22
"parser": "@typescript-eslint/parser",
3-
"plugins": ["@typescript-eslint"],
43
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
54
"env": {
65
"node": true
6+
},
7+
"rules": {
8+
"@typescript-eslint/no-non-null-assertion": "off",
9+
"@typescript-eslint/explicit-module-boundary-types": "off"
710
}
811
}

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
interface AxePlaywrightMatchers<R> {
22
toBeAccessible(): Promise<R>
3+
toBeAccessible(selector: string): Promise<R>
34
}
45

56
declare global {

playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ import { matchers } from './src'
44
expect.extend(matchers)
55

66
export default {
7+
globalSetup: require.resolve('./src/config/globalSetup'),
78
testDir: 'src',
89
} as PlaywrightTestConfig

src/config/globalSetup.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import http from 'http'
2+
import type { AddressInfo } from 'net'
3+
import { readFile } from '../utils/file'
4+
5+
function listener(req: http.IncomingMessage, res: http.ServerResponse) {
6+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
7+
const file = req.url!.replace('/', '')
8+
9+
readFile(file).then((html) => {
10+
res.writeHead(200)
11+
res.end(html)
12+
})
13+
}
14+
15+
export default async function globalSetup() {
16+
const server = http.createServer(listener)
17+
await new Promise((done) => server.listen(done))
18+
19+
// Expose port to the tests
20+
const address = server.address() as AddressInfo
21+
process.env.SERVER_PORT = String(address.port)
22+
23+
return async () => {
24+
await new Promise((done) => server.close(done))
25+
}
26+
}

src/config/templates/accessible.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>Foo</title>
5+
</head>
6+
<body>
7+
<main>
8+
<h1>Bar</h1>
9+
</main>
10+
</body>
11+
</html>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head> </head>
4+
<body>
5+
<h1>Bar</h1>
6+
</body>
7+
</html>

src/matchers/__tests__/toBeAccessible.spec.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/matchers/toBeAccessible.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { expect, test } from '@playwright/test'
2+
import { readFile } from '../../utils/file'
3+
4+
test.describe('toBeAccessible', () => {
5+
test.describe('page', () => {
6+
test('positive', async ({ page }) => {
7+
const content = await readFile('accessible.html')
8+
await page.setContent(content)
9+
await expect(page).toBeAccessible()
10+
})
11+
12+
test('negative', async ({ page }) => {
13+
test.fail()
14+
const content = await readFile('inaccessible.html')
15+
await page.setContent(content)
16+
await expect(page).toBeAccessible()
17+
})
18+
})
19+
20+
test.describe('selector', () => {
21+
test('positive', async ({ page }) => {
22+
await page.setContent('<button id="foo">Hello</button>')
23+
await expect(page).toBeAccessible('#foo')
24+
})
25+
26+
test('negative', async ({ page }) => {
27+
test.fail()
28+
await page.setContent('<button id="foo"></button>')
29+
await expect(page).toBeAccessible('#foo')
30+
})
31+
})
32+
33+
test.describe('element', () => {
34+
test('positive', async ({ page }) => {
35+
await page.setContent('<button id="foo">Hello</button>')
36+
const button = page.$('#foo')
37+
38+
await expect(button).toBeAccessible()
39+
await expect(await button).toBeAccessible()
40+
})
41+
42+
test('negative', async ({ page }) => {
43+
test.fail()
44+
await page.setContent('<button id="foo"></button>')
45+
const button = page.$('#foo')
46+
47+
await expect(button).toBeAccessible()
48+
await expect(await button).toBeAccessible()
49+
})
50+
})
51+
52+
test.describe('iframe', () => {
53+
test('positive', async ({ page }) => {
54+
const content = `<iframe src="http://localhost:${process.env.SERVER_PORT}/accessible.html">`
55+
await page.setContent(content)
56+
57+
const handle = page.$('iframe')
58+
await expect(handle).toBeAccessible()
59+
await expect(await handle).toBeAccessible()
60+
61+
const iframe = (await handle)?.contentFrame()
62+
await expect(iframe).toBeAccessible()
63+
await expect(await iframe).toBeAccessible()
64+
})
65+
66+
test('negative', async ({ page }) => {
67+
test.fail()
68+
const content = `<iframe src="http://localhost:${process.env.SERVER_PORT}/inaccessible.html">`
69+
await page.setContent(content)
70+
71+
const handle = page.$('iframe')
72+
await expect(handle).toBeAccessible()
73+
await expect(await handle).toBeAccessible()
74+
75+
const iframe = (await handle)?.contentFrame()
76+
await expect(iframe).toBeAccessible()
77+
await expect(await iframe).toBeAccessible()
78+
})
79+
})
80+
})

src/matchers/toBeAccessible/index.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Result } from 'axe-core'
2+
import type { MatcherState, SyncExpectationResult } from 'expect/build/types'
3+
import { injectAxe, runAxe } from '../../utils/axe'
4+
import { getElementHandle, InputArguments } from '../../utils/matcher'
5+
6+
const collectViolations = (violations: Result[]) =>
7+
violations
8+
.map((violation) => `${violation.id}(${violation.nodes.length})`)
9+
.join(', ')
10+
11+
export async function toBeAccessible(
12+
this: MatcherState,
13+
...args: InputArguments
14+
): Promise<SyncExpectationResult> {
15+
try {
16+
const [elementHandle] = await getElementHandle(args)
17+
const frame = (await elementHandle.ownerFrame())!
18+
19+
await injectAxe(frame)
20+
const results = await runAxe(elementHandle, {})
21+
const count = results.violations.length
22+
23+
return {
24+
pass: count === 0,
25+
message: () => {
26+
return (
27+
this.utils.matcherHint('toBeAccessible', undefined, undefined, this) +
28+
'\n\n' +
29+
'Expected: No violations\n' +
30+
`Received: ${count} violations\n\n` +
31+
`Violations: ${collectViolations(results.violations)}`
32+
)
33+
},
34+
}
35+
} catch (err) {
36+
return {
37+
pass: false,
38+
message: () => err.message,
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)