Skip to content

Commit 5f3bfbb

Browse files
committed
Move matchers into a separate entry, add details content implementation
1 parent c731954 commit 5f3bfbb

17 files changed

+218
-49
lines changed

README.md

+4-5
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ yarn add --dev accessibility-testing-toolkit
1818

1919
## Usage
2020

21-
Import `accessibility-testing-toolkit` in your project once. The best place is to do it in your test setup file:
21+
Import `accessibility-testing-toolkit/matchers` in your project once. The best place is to do it in your test setup file:
2222

2323
```js
2424
// In your own jest-setup.js
25-
import 'accessibility-testing-toolkit';
25+
import 'accessibility-testing-toolkit/matchers';
2626

2727
// In jest.config.js add
2828
setupFilesAfterEnv: ['<rootDir>/jest-setup.js'];
@@ -121,8 +121,8 @@ Container nodes in the DOM, such as non-semantic `<div>` and `<span>` elements,
121121
122122
When determining whether elements in the DOM are accessible, certain attributes and CSS properties signal that an element, along with its children, should not be considered visible:
123123
124-
- Elements with the `hidden` attribute or `aria-hidden="true"`.
125-
- Styles that set `display: none` or `visibility: hidden`.
124+
- Elements with the `hidden` attribute or `aria-hidden="true"`
125+
- Styles that set `display: none` or `visibility: hidden`
126126
127127
In testing environments, relying on attribute checks may be necessary since `getComputedStyle` may not reflect styles defined in external stylesheets.
128128
@@ -164,7 +164,6 @@ Specifically, the toolkit applies the following custom roles:
164164
- `abbr`: Mapped to `Abbr`
165165
- `audio`: Mapped to `Audio`
166166
- `canvas`: Mapped to `Canvas`
167-
- `details`: Mapped to `Details`
168167
- `dd`: Mapped to `DescriptionListDetails`
169168
- `dl`: Mapped to `DescriptionList`
170169
- `dt`: Mapped to `DescriptionListTerm`

jest-setup.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
import '@testing-library/jest-dom';
2-
import './src/index';
2+
import './src/matchers';

package.json

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "accessibility-testing-toolkit",
3-
"version": "1.0.3",
3+
"version": "1.1.0-beta.0",
44
"author": "Ivan Galiatin",
55
"license": "MIT",
66
"description": "A toolkit for testing accessibility",
@@ -9,15 +9,26 @@
99
"url": "git+https://github.com/trurl-master/accessibility-testing-toolkit.git"
1010
},
1111
"keywords": [
12-
"testing",
13-
"jsdom",
14-
"jest",
1512
"accessibility",
1613
"accessibility tree",
17-
"aria"
14+
"aria",
15+
"dom",
16+
"jest",
17+
"jsdom",
18+
"testing"
1819
],
1920
"main": "dist/index.js",
2021
"typings": "dist/index.d.ts",
22+
"exports": {
23+
".": {
24+
"import": "./dist/esm/index.js",
25+
"require": "./dist/index.js"
26+
},
27+
"./matchers": {
28+
"import": "./dist/esm/matchers.js",
29+
"require": "./dist/matchers.js"
30+
}
31+
},
2132
"files": [
2233
"/dist",
2334
"/src",
@@ -49,7 +60,8 @@
4960
"module": "dist/esm/index.js",
5061
"tsup": {
5162
"entry": [
52-
"src/index.ts"
63+
"src/index.ts",
64+
"src/matchers.ts"
5365
],
5466
"splitting": false,
5567
"sourcemap": true,

src/elements/code.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { render } from '@testing-library/react';
22
import { byRole } from '..';
33

4-
describe('', () => {
4+
describe('code', () => {
55
it('renders correct structure', () => {
66
const { container } = render(
77
<p>

src/elements/col-colgroup.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { render } from '@testing-library/react';
22
import { byRole } from '..';
33

4-
describe('', () => {
4+
describe('col-colgroup', () => {
55
it('renders correct structure', () => {
66
const { container } = render(
77
<table>

src/elements/datalist.test.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { render } from '@testing-library/react';
2+
import { byRole } from '..';
3+
4+
describe('datalist', () => {
5+
it('renders correct structure', () => {
6+
const { container } = render(
7+
<>
8+
<label htmlFor="ice-cream-choice">Choose a flavor:</label>
9+
<input
10+
list="ice-cream-flavors"
11+
id="ice-cream-choice"
12+
name="ice-cream-choice"
13+
/>
14+
<datalist id="ice-cream-flavors">
15+
<option value="Chocolate"></option>
16+
<option value="Coconut"></option>
17+
<option value="Mint"></option>
18+
<option value="Strawberry"></option>
19+
<option value="Vanilla"></option>
20+
</datalist>
21+
</>
22+
);
23+
24+
expect(container).toHaveA11yTree(
25+
byRole('generic', [
26+
byRole('LabelText', ['Choose a flavor:']),
27+
byRole('combobox', { name: 'Choose a flavor:' }),
28+
// datalist has display: none by default
29+
])
30+
);
31+
});
32+
});

src/elements/del-ins.test.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { render } from '@testing-library/react';
2+
import { byRole } from '..';
3+
4+
describe('del-ins', () => {
5+
it('renders correct structure', () => {
6+
const { container } = render(
7+
<blockquote>
8+
There is <del>nothing</del> <ins>no code</ins> either good or bad, but
9+
<del>thinking</del> <ins>running it</ins> makes it so.
10+
</blockquote>
11+
);
12+
13+
expect(container.firstChild).toHaveA11yTree(
14+
byRole('blockquote', [
15+
'There is ',
16+
byRole('deletion', ['nothing']),
17+
' ',
18+
byRole('insertion', ['no code']),
19+
' either good or bad, but',
20+
byRole('deletion', ['thinking']),
21+
' ',
22+
byRole('insertion', ['running it']),
23+
' makes it so.',
24+
])
25+
);
26+
});
27+
});

src/elements/details.test.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { render } from '@testing-library/react';
2+
import { byRole } from '..';
3+
4+
describe('details', () => {
5+
it('closed by default, content is hidden', () => {
6+
const { container } = render(
7+
<details>
8+
<summary>Details</summary>
9+
<p>Something small enough to escape casual notice.</p>
10+
some simple text
11+
</details>
12+
);
13+
14+
expect(container.firstChild).toHaveA11yTree(
15+
byRole('group', { expanded: false }, [
16+
byRole('DisclosureTriangle', ['Details']),
17+
// the details element is not expanded by default
18+
// so the content is not rendered
19+
])
20+
);
21+
});
22+
23+
it('open, content is visible', () => {
24+
const { container } = render(
25+
<details open>
26+
<summary>Details</summary>
27+
Something small enough to escape casual notice.
28+
</details>
29+
);
30+
31+
expect(container.firstChild).toHaveA11yTree(
32+
byRole('group', { expanded: true }, [
33+
byRole('DisclosureTriangle', ['Details']),
34+
'Something small enough to escape casual notice.',
35+
])
36+
);
37+
});
38+
});

src/elements/dl-dt-dd.test.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { render } from '@testing-library/react';
2+
import { byRole } from '..';
3+
4+
describe('dl-dt-dd', () => {
5+
it('renders correct structure', () => {
6+
const { container } = render(
7+
<>
8+
<p>Cryptids of Cornwall:</p>
9+
10+
<dl>
11+
<dt>Beast of Bodmin</dt>
12+
<dd>A large feline inhabiting Bodmin Moor.</dd>
13+
14+
<dt>Morgawr</dt>
15+
<dd>A sea serpent.</dd>
16+
17+
<dt>Owlman</dt>
18+
<dd>A giant owl-like creature.</dd>
19+
</dl>
20+
</>
21+
);
22+
23+
expect(container).toHaveA11yTree(
24+
byRole('generic', [
25+
byRole('paragraph', ['Cryptids of Cornwall:']),
26+
byRole('DescriptionList', [
27+
byRole('term', ['Beast of Bodmin']),
28+
byRole('definition', ['A large feline inhabiting Bodmin Moor.']),
29+
byRole('term', ['Morgawr']),
30+
byRole('definition', ['A sea serpent.']),
31+
byRole('term', ['Owlman']),
32+
byRole('definition', ['A giant owl-like creature.']),
33+
]),
34+
])
35+
);
36+
});
37+
});

src/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
export { MatcherOptions } from './types/matchers.d';
2-
import './matchers';
31
export * from './helpers/by-role';
42
export {
53
AsNonLandmarkRoles,
@@ -9,6 +7,7 @@ export {
97
A11yTreeNode,
108
A11yTreeNodeMatch,
119
} from './types/types';
10+
export { getPrettyTree } from './pretty-tree/pretty-tree';
1211
export { getAccessibilityTree } from './tree/accessibility-tree';
1312
export { pruneContainerNodes } from './tree/prune-container-nodes';
1413
export { isSubtreeInaccessible } from 'dom-accessibility-api';

src/matchers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { getPrettyTree } from './pretty-tree/pretty-tree';
77
import { MatcherOptions } from './types/matchers';
88
import { nodeTester } from './testers/node';
99

10+
export { MatcherOptions } from './types/matchers.d';
11+
1012
expect.extend({
1113
toHaveA11yTree(
1214
received: HTMLElement | A11yTreeNode,

src/pretty-tree/pretty-tree.ts

+1-21
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,9 @@
1-
// import { defaultState } from '../helpers';
21
import { defaultQueries, defaultState } from '../helpers';
32
import { StaticText } from '../tree/leafs';
4-
import {
5-
A11yTreeForDiff,
6-
// A11yTreeNodeState,
7-
// A11yTreeNodeStateForDiff,
8-
TextMatcher,
9-
} from '../types/types';
3+
import { A11yTreeForDiff, TextMatcher } from '../types/types';
104
import { omitDefaultValues } from './omit-default-values';
115
import { renderProperties } from './render-properties';
126

13-
// const getStateDetails = (
14-
// state: A11yTreeNodeState | A11yTreeNodeStateForDiff
15-
// ): string => {
16-
// const nonDefaultEntries = Object.entries(state).filter(
17-
// ([key, value]) => value !== defaultState[key as keyof A11yTreeNodeState]
18-
// );
19-
20-
// return nonDefaultEntries.length > 0
21-
// ? `${nonDefaultEntries
22-
// .map(([key, value]) => `${key}: ${value}`)
23-
// .join(', ')}`
24-
// : '';
25-
// };
26-
277
const renderStringMatcher = (textMatcher: TextMatcher): string => {
288
if (textMatcher instanceof RegExp || typeof textMatcher === 'number') {
299
return textMatcher.toString();

src/tree/accessibility-tree.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,21 @@ import { isDefined } from '../type-guards';
2424
import { StaticText } from './leafs';
2525
import { MatcherOptions } from '../types/matchers';
2626
import { getConfig } from '../config';
27-
28-
// if a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region
29-
const isNonLandmarkRole = (element: HTMLElement, role: string) =>
30-
['article', 'aside', 'main', 'nav', 'section'].includes(
31-
element.tagName.toLowerCase()
32-
) ||
33-
['aricle', 'complementary', 'main', 'navigation', 'region'].includes(role);
34-
35-
const isList = (role: HTMLElement['role']) => role === 'list';
27+
import { isClosedDetails, isList, isNonLandmarkRole } from './context';
28+
import { isInaccessibleOverride } from './overrides';
3629

3730
const defaultOptions = {
3831
isListSubtree: false,
32+
isClosedDetailsSubtree: false,
3933
isNonLandmarkSubtree: false,
4034
} satisfies MatcherOptions;
4135

4236
export const getAccessibilityTree = (
4337
element: HTMLElement,
4438
{
4539
isListSubtree: userListSubtree = defaultOptions.isListSubtree,
40+
isClosedDetailsSubtree:
41+
userIsClosedDetailsSubtree = defaultOptions.isClosedDetailsSubtree,
4642
isNonLandmarkSubtree:
4743
userNonLandmarkSubtree = defaultOptions.isNonLandmarkSubtree,
4844
isInaccessibleOptions = getConfig().isInaccessibleOptions,
@@ -52,7 +48,10 @@ export const getAccessibilityTree = (
5248
element: HTMLElement,
5349
context: A11yTreeNodeContext
5450
): A11yTreeNode | null {
55-
if (isInaccessible(element, context.isInaccessibleOptions)) {
51+
if (
52+
isInaccessible(element, context.isInaccessibleOptions) ||
53+
isInaccessibleOverride(element)
54+
) {
5655
return null;
5756
}
5857

@@ -88,6 +87,8 @@ export const getAccessibilityTree = (
8887
if (child instanceof HTMLElement) {
8988
return assembleTree(child, {
9089
isListSubtree: context.isListSubtree || isList(role),
90+
isClosedDetailsSubtree:
91+
context.isClosedDetailsSubtree || isClosedDetails(element),
9192
isNonLandmarkSubtree:
9293
context.isNonLandmarkSubtree ||
9394
isNonLandmarkRole(element, role),
@@ -96,7 +97,7 @@ export const getAccessibilityTree = (
9697
}
9798

9899
if (child instanceof Text) {
99-
if (child.textContent === null) {
100+
if (child.textContent === null || isInaccessibleOverride(child)) {
100101
return undefined;
101102
}
102103

@@ -111,6 +112,7 @@ export const getAccessibilityTree = (
111112

112113
return assembleTree(element, {
113114
isListSubtree: userListSubtree,
115+
isClosedDetailsSubtree: userIsClosedDetailsSubtree,
114116
isNonLandmarkSubtree: userNonLandmarkSubtree,
115117
isInaccessibleOptions,
116118
});

src/tree/context.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { isDetailsElement } from '../type-guards';
2+
3+
// if a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region
4+
export const isNonLandmarkRole = (element: HTMLElement, role: string) =>
5+
['article', 'aside', 'main', 'nav', 'section'].includes(
6+
element.tagName.toLowerCase()
7+
) ||
8+
['aricle', 'complementary', 'main', 'navigation', 'region'].includes(role);
9+
10+
export const isList = (role: HTMLElement['role']) => role === 'list';
11+
12+
export const isClosedDetails = (element: HTMLElement) =>
13+
isDetailsElement(element) && !element.open;

0 commit comments

Comments
 (0)