@markuplint/ml-spec 3.6.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/lib/constant/aria-version.d.ts +2 -0
  2. package/lib/constant/aria-version.js +5 -0
  3. package/lib/dom-traverse/get-computed-role.d.ts +6 -1
  4. package/lib/dom-traverse/get-computed-role.js +31 -6
  5. package/lib/dom-traverse/get-non-presentational-ancestor.js +7 -1
  6. package/lib/dom-traverse/matches-context-role.d.ts +7 -0
  7. package/lib/dom-traverse/matches-context-role.js +27 -0
  8. package/lib/index.d.ts +2 -0
  9. package/lib/index.js +2 -0
  10. package/lib/specs/resolve-version.js +1 -1
  11. package/lib/specs/schema-to-spec.js +5 -0
  12. package/lib/types/aria.d.ts +20 -0
  13. package/lib/types/index.d.ts +3 -1
  14. package/lib/utils/validateAriaVersion.d.ts +2 -0
  15. package/lib/utils/validateAriaVersion.js +8 -0
  16. package/package.json +7 -7
  17. package/test/dom-traverse/accname-computation.spec.js +69 -0
  18. package/test/dom-traverse/get-computed-aria-props.spec.js +758 -0
  19. package/test/dom-traverse/get-computed-role.spec.js +294 -0
  20. package/test/dom-traverse/get-implicit-role.spec.js +40 -0
  21. package/test/dom-traverse/has-required-owned-elements.spec.js +34 -0
  22. package/test/dom-traverse/is-exposed.spec.js +70 -0
  23. package/test/dom-traverse/matches-context-role.spec.js +60 -0
  24. package/test/specs/get-attr-specs.spec.js +41 -0
  25. package/test/specs/get-implicit-role.spec.js +79 -0
  26. package/test/specs/get-permitted-roles.spec.js +418 -0
  27. package/test/specs/get-role-spec.spec.js +67 -0
  28. package/test/specs/get-spec-by-tag-name.spec.js +68 -0
  29. package/test/specs/resolve-version.spec.js +34 -0
  30. package/test/specs/schema-to-spec.spec.js +61 -0
  31. package/test/utils/resolve-namespace.spec.js +37 -0
@@ -0,0 +1,294 @@
1
+ const specs = require('@markuplint/html-spec');
2
+ const { createSelector } = require('@markuplint/selector');
3
+ const { createJSDOMElement } = require('@markuplint/test-tools');
4
+
5
+ const { getComputedRole } = require('../../lib/dom-traverse/get-computed-role');
6
+
7
+ function _(html, selector) {
8
+ return createJSDOMElement(html, selector, function (selector) {
9
+ // JSDOM supports no level 4 selectors yet.
10
+ return createSelector(selector, specs).match(this) !== false;
11
+ });
12
+ }
13
+
14
+ function c(html, version, selector) {
15
+ return getComputedRole(specs, _(html, selector), version);
16
+ }
17
+
18
+ function tree(html, version) {
19
+ const el = _(html);
20
+ const tree = [];
21
+ let current = el;
22
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
23
+ while (current) {
24
+ const role = getComputedRole(specs, current, version);
25
+ const result = [current.localName, role.role?.name ?? null];
26
+ if (role.errorType) {
27
+ result.push(role.errorType);
28
+ }
29
+ tree.push(result);
30
+ current = current.children.item(0);
31
+ }
32
+ return tree;
33
+ }
34
+
35
+ describe('1.2', () => {
36
+ test('the a element', () => {
37
+ expect(c('<a></a>', '1.2').role?.name).toBe(undefined);
38
+ expect(c('<a href></a>', '1.2').role?.name).toBe('link');
39
+ expect(c('<a role="button"></a>', '1.2').role?.name).toBe('button');
40
+ expect(c('<a role="button" href></a>', '1.2').role?.name).toBe('button');
41
+ expect(c('<a role="foo" href></a>', '1.2').role?.name).toBe('link');
42
+ });
43
+
44
+ test('the heading role', () => {
45
+ expect(c('<h1></h1>', '1.2').role?.name).toBe('heading');
46
+ expect(c('<h1></h1>', '1.2').role?.isImplicit).toBe(true);
47
+ expect(c('<div role="heading"></div>', '1.2').role?.name).toBe('heading');
48
+ expect(c('<div role="heading"></div>', '1.2').role?.isImplicit).toBe(false);
49
+ });
50
+
51
+ test('multiple', () => {
52
+ expect(c('<div role="presentation heading"></div>', '1.2').role?.name).toBe('presentation');
53
+ expect(c('<div role="roletype heading"></div>', '1.2').role?.name).toBe('heading');
54
+ expect(c('<img alt="alt" role="banner button"/>', '1.2').role?.name).toBe('button');
55
+ expect(c('<img alt="alt" role="foo button"/>', '1.2').role?.name).toBe('button');
56
+ expect(c('<img alt="alt" role="graphics-symbol button"/>', '1.2').role?.name).toBe('button');
57
+ });
58
+
59
+ test('svg', () => {
60
+ expect(c('<svg><rect role="graphics-symbol img"></rect></svg>', '1.2', 'rect').role?.name).toBe(
61
+ 'graphics-symbol',
62
+ );
63
+ expect(c('<svg><rect role="roletype img"></rect></svg>', '1.2', 'rect').role?.name).toBe('img');
64
+ expect(c('<svg><rect role="roletype"></rect></svg>', '1.2', 'rect').role?.name).toBe('graphics-symbol');
65
+ });
66
+
67
+ test('landmark', () => {
68
+ expect(c('<div role="region"></div>', '1.2').role?.name).toBe('generic');
69
+ expect(c('<div role="region" aria-label="foo"></div>', '1.2').role?.name).toBe('region');
70
+ expect(c('<div role="form"></div>', '1.2').role?.name).toBe('generic');
71
+ expect(c('<div role="form" aria-label="foo"></div>', '1.2').role?.name).toBe('form');
72
+ expect(c('<div role="navigation"></div>', '1.2').role?.name).toBe('navigation');
73
+ expect(c('<div role="navigation" aria-label="foo"></div>', '1.2').role?.name).toBe('navigation');
74
+ });
75
+
76
+ test('Presentational Roles Conflict Resolution (1) Interactive Elements', () => {
77
+ expect(c('<a href="path/to"></a>', '1.2').role?.name).toBe('link');
78
+ expect(c('<a role="none" href="path/to"></a>', '1.2').role?.name).toBe('link'); // No permitted role
79
+ expect(c('<a></a>', '1.2').role?.name).toBe(undefined);
80
+ expect(c('<a role="none"></a>', '1.2').role?.name).toBe('none');
81
+ expect(c('<a role="none" href="path/to" disabled></a>', '1.2').role?.name).toBe('link'); // No permitted role
82
+ expect(c('<button></button>', '1.2').role?.name).toBe('button');
83
+ expect(c('<button disabled></button>', '1.2').role?.name).toBe('button');
84
+ expect(c('<button role="none"></button>', '1.2').role?.name).toBe('button'); // No permitted role
85
+ expect(c('<button role="none" disabled></button>', '1.2').role?.name).toBe('button'); // No permitted role
86
+ expect(c('<div></div>', '1.2').role?.name).toBe('generic');
87
+ expect(c('<div role="none"></div>', '1.2').role?.name).toBe('none');
88
+ expect(c('<div tabindex="0"></div>', '1.2').role?.name).toBe('generic');
89
+ expect(c('<div tabindex="0" role="none"></div>', '1.2').role?.name).toBe('generic');
90
+ expect(c('<div><span></span></div>', '1.2', 'span').role?.name).toBe('generic');
91
+ expect(c('<div><span role="none"></span></div>', '1.2', 'span').role?.name).toBe('none');
92
+ expect(c('<div><span tabindex="0"></span></div>', '1.2', 'span').role?.name).toBe('generic');
93
+ expect(c('<div><span tabindex="0" role="none"></span></div>', '1.2', 'span').role?.name).toBe('generic');
94
+ expect(c('<div hidden><span></span></div>', '1.2', 'span').role?.name).toBe('generic');
95
+ expect(c('<div hidden><span role="none"></span></div>', '1.2', 'span').role?.name).toBe('none');
96
+ expect(c('<div hidden><span tabindex="0"></span></div>', '1.2', 'span').role?.name).toBe('generic');
97
+ expect(c('<div hidden><span tabindex="0" role="none"></span></div>', '1.2', 'span').role?.name).toBe('none');
98
+ });
99
+
100
+ test('Presentational Roles Conflict Resolution (2-1) Required Owned Elements', () => {
101
+ expect(c('<table><tr><td>foo</td></tr></table>', '1.2', 'td').role?.name).toBe('cell');
102
+ expect(c('<table><tr><td role="none">foo</td></tr></table>', '1.2', 'td').role?.name).toBe('cell');
103
+ expect(c('<table><tbody role="none"><tr><td>foo</td></tr></tbody></table>', '1.2', 'tbody').role?.name).toBe(
104
+ 'rowgroup',
105
+ );
106
+ expect(c('<ul><li></li></ul>', '1.2', 'li').role?.name).toBe('listitem');
107
+ expect(c('<ul><li role="presentation"></li></ul>', '1.2', 'li').role?.name).toBe('listitem');
108
+ });
109
+
110
+ test('Presentational Roles Conflict Resolution (2-2) the accessibility tree to be malformed', () => {
111
+ expect(c('<table><tr><td>foo</td></tr></table>', '1.2', 'td').role?.name).toBe('cell');
112
+ expect(c('<table role="none"><tr><td>foo</td></tr></table>', '1.2', 'td').role?.name).toBe(undefined);
113
+ expect(tree('<table><tr><td>foo</td></tr></table>', '1.2')).toStrictEqual([
114
+ ['table', 'table'],
115
+ ['tbody', 'rowgroup'],
116
+ ['tr', 'row'],
117
+ ['td', 'cell'],
118
+ ]);
119
+ expect(tree('<table role="none"><tr><td>foo</td></tr></table>', '1.2')).toStrictEqual([
120
+ ['table', 'none'],
121
+ ['tbody', null, 'NO_OWNER'],
122
+ ['tr', null, 'NO_OWNER'],
123
+ ['td', null /* NOT MATCHING THE CONDITIONS */],
124
+ ]);
125
+ expect(
126
+ tree(
127
+ '<table role="table"><tbody role="rowgroup"><tr role="row"><td role="gridcell"></td></tr></tbody></table>',
128
+ '1.2',
129
+ ),
130
+ ).toStrictEqual([
131
+ ['table', 'table'],
132
+ ['tbody', 'rowgroup'],
133
+ ['tr', 'row'],
134
+ ['td', 'cell', 'NO_PERMITTED'],
135
+ ]);
136
+ expect(
137
+ tree(
138
+ '<table role="grid"><tbody role="rowgroup"><tr role="row"><td role="gridcell"></td></tr></tbody></table>',
139
+ '1.2',
140
+ ),
141
+ ).toStrictEqual([
142
+ ['table', 'grid'],
143
+ ['tbody', 'rowgroup'],
144
+ ['tr', 'row'],
145
+ ['td', 'gridcell'],
146
+ ]);
147
+ });
148
+
149
+ test('Presentational Roles Conflict Resolution (3) Global Props', () => {
150
+ /**
151
+ * @see https://w3c.github.io/aria/#example-41
152
+ */
153
+ expect(c('<h1 role="presentation" aria-describedby="comment-1"> Sample Content </h1>', '1.2').role?.name).toBe(
154
+ 'heading',
155
+ );
156
+ expect(c('<h1 role="presentation" aria-level="2"> Sample Content </h1>', '1.2').role?.name).toBe(
157
+ 'presentation',
158
+ );
159
+ });
160
+ });
161
+
162
+ describe('1.3', () => {
163
+ test('the a element', () => {
164
+ expect(c('<a></a>', '1.3').role?.name).toBe(undefined);
165
+ expect(c('<a href></a>', '1.3').role?.name).toBe('link');
166
+ expect(c('<a role="button"></a>', '1.3').role?.name).toBe('button');
167
+ expect(c('<a role="button" href></a>', '1.3').role?.name).toBe('button');
168
+ expect(c('<a role="foo" href></a>', '1.3').role?.name).toBe('link');
169
+ });
170
+
171
+ test('the heading role', () => {
172
+ expect(c('<h1></h1>', '1.3').role?.name).toBe('heading');
173
+ expect(c('<h1></h1>', '1.3').role?.isImplicit).toBe(true);
174
+ expect(c('<div role="heading"></div>', '1.3').role?.name).toBe('heading');
175
+ expect(c('<div role="heading"></div>', '1.3').role?.isImplicit).toBe(false);
176
+ });
177
+
178
+ test('multiple', () => {
179
+ expect(c('<div role="presentation heading"></div>', '1.3').role?.name).toBe('presentation');
180
+ expect(c('<div role="roletype heading"></div>', '1.3').role?.name).toBe('heading');
181
+ expect(c('<img alt="alt" role="banner button"/>', '1.3').role?.name).toBe('button');
182
+ expect(c('<img alt="alt" role="foo button"/>', '1.3').role?.name).toBe('button');
183
+ expect(c('<img alt="alt" role="graphics-symbol button"/>', '1.3').role?.name).toBe('button');
184
+ });
185
+
186
+ test('svg', () => {
187
+ expect(c('<svg><rect role="graphics-symbol img"></rect></svg>', '1.3', 'rect').role?.name).toBe(
188
+ 'graphics-symbol',
189
+ );
190
+ expect(c('<svg><rect role="roletype img"></rect></svg>', '1.3', 'rect').role?.name).toBe('img');
191
+ expect(c('<svg><rect role="roletype"></rect></svg>', '1.3', 'rect').role?.name).toBe('graphics-symbol');
192
+ });
193
+
194
+ test('landmark', () => {
195
+ expect(c('<div role="region"></div>', '1.3').role?.name).toBe('generic');
196
+ expect(c('<div role="region" aria-label="foo"></div>', '1.3').role?.name).toBe('region');
197
+ expect(c('<div role="form"></div>', '1.3').role?.name).toBe('generic');
198
+ expect(c('<div role="form" aria-label="foo"></div>', '1.3').role?.name).toBe('form');
199
+ expect(c('<div role="navigation"></div>', '1.3').role?.name).toBe('navigation');
200
+ expect(c('<div role="navigation" aria-label="foo"></div>', '1.3').role?.name).toBe('navigation');
201
+ });
202
+
203
+ test('Presentational Roles Conflict Resolution (1) Interactive Elements', () => {
204
+ expect(c('<a href="path/to"></a>', '1.3').role?.name).toBe('link');
205
+ expect(c('<a role="none" href="path/to"></a>', '1.3').role?.name).toBe('link'); // No permitted role
206
+ expect(c('<a></a>', '1.3').role?.name).toBe(undefined);
207
+ expect(c('<a role="none"></a>', '1.3').role?.name).toBe('none');
208
+ expect(c('<a role="none" href="path/to" disabled></a>', '1.3').role?.name).toBe('link'); // No permitted role
209
+ expect(c('<button></button>', '1.3').role?.name).toBe('button');
210
+ expect(c('<button disabled></button>', '1.3').role?.name).toBe('button');
211
+ expect(c('<button role="none"></button>', '1.3').role?.name).toBe('button'); // No permitted role
212
+ expect(c('<button role="none" disabled></button>', '1.3').role?.name).toBe('button'); // No permitted role
213
+ expect(c('<div></div>', '1.3').role?.name).toBe('generic');
214
+ expect(c('<div role="none"></div>', '1.3').role?.name).toBe('none');
215
+ expect(c('<div tabindex="0"></div>', '1.3').role?.name).toBe('generic');
216
+ expect(c('<div tabindex="0" role="none"></div>', '1.3').role?.name).toBe('generic');
217
+ expect(c('<div><span></span></div>', '1.3', 'span').role?.name).toBe('generic');
218
+ expect(c('<div><span role="none"></span></div>', '1.3', 'span').role?.name).toBe('none');
219
+ expect(c('<div><span tabindex="0"></span></div>', '1.3', 'span').role?.name).toBe('generic');
220
+ expect(c('<div><span tabindex="0" role="none"></span></div>', '1.3', 'span').role?.name).toBe('generic');
221
+ expect(c('<div hidden><span></span></div>', '1.3', 'span').role?.name).toBe('generic');
222
+ expect(c('<div hidden><span role="none"></span></div>', '1.3', 'span').role?.name).toBe('none');
223
+ expect(c('<div hidden><span tabindex="0"></span></div>', '1.3', 'span').role?.name).toBe('generic');
224
+ expect(c('<div hidden><span tabindex="0" role="none"></span></div>', '1.3', 'span').role?.name).toBe('none');
225
+ });
226
+
227
+ test('Presentational Roles Conflict Resolution (2-1) Required Owned Elements', () => {
228
+ expect(c('<table><tr><td>foo</td></tr></table>', '1.3', 'td').role?.name).toBe('cell');
229
+ expect(c('<table><tr><td role="none">foo</td></tr></table>', '1.3', 'td').role?.name).toBe('cell');
230
+ expect(c('<table><tbody role="none"><tr><td>foo</td></tr></tbody></table>', '1.3', 'tbody').role?.name).toBe(
231
+ 'rowgroup',
232
+ );
233
+ expect(c('<ul><li></li></ul>', '1.3', 'li').role?.name).toBe('listitem');
234
+ expect(c('<ul><li role="presentation"></li></ul>', '1.3', 'li').role?.name).toBe('listitem');
235
+ });
236
+
237
+ test('Presentational Roles Conflict Resolution (2-2) the accessibility tree to be malformed', () => {
238
+ expect(c('<table><tr><td>foo</td></tr></table>', '1.3', 'td').role?.name).toBe('cell');
239
+ expect(c('<table role="none"><tr><td>foo</td></tr></table>', '1.3', 'td').role?.name).toBe(undefined);
240
+ expect(tree('<table><tr><td>foo</td></tr></table>', '1.3')).toStrictEqual([
241
+ ['table', 'table'],
242
+ ['tbody', 'rowgroup'],
243
+ ['tr', 'row'],
244
+ ['td', 'cell'],
245
+ ]);
246
+ expect(tree('<table role="none"><tr><td>foo</td></tr></table>', '1.3')).toStrictEqual([
247
+ ['table', 'none'],
248
+ ['tbody', null, 'NO_OWNER'],
249
+ ['tr', null, 'INVALID_REQUIRED_CONTEXT_ROLE'],
250
+ ['td', null /* NOT MATCHING THE CONDITIONS */],
251
+ ]);
252
+ expect(
253
+ tree(
254
+ '<table role="table"><tbody role="rowgroup"><tr role="row"><td role="gridcell"></td></tr></tbody></table>',
255
+ '1.3',
256
+ ),
257
+ ).toStrictEqual([
258
+ ['table', 'table'],
259
+ ['tbody', 'rowgroup'],
260
+ ['tr', 'row'],
261
+ ['td', 'cell', 'NO_PERMITTED'],
262
+ ]);
263
+ expect(
264
+ tree(
265
+ '<table role="grid"><tbody role="rowgroup"><tr role="row"><td role="gridcell"></td></tr></tbody></table>',
266
+ '1.3',
267
+ ),
268
+ ).toStrictEqual([
269
+ ['table', 'grid'],
270
+ ['tbody', 'rowgroup'],
271
+ ['tr', 'row'],
272
+ ['td', 'gridcell'],
273
+ ]);
274
+ });
275
+
276
+ test('Presentational Roles Conflict Resolution (3) Global Props', () => {
277
+ /**
278
+ * @see https://w3c.github.io/aria/#example-41
279
+ */
280
+ expect(c('<h1 role="presentation" aria-describedby="comment-1"> Sample Content </h1>', '1.3').role?.name).toBe(
281
+ 'heading',
282
+ );
283
+ expect(c('<h1 role="presentation" aria-level="2"> Sample Content </h1>', '1.3').role?.name).toBe(
284
+ 'presentation',
285
+ );
286
+ });
287
+ });
288
+
289
+ describe('Issues', () => {
290
+ test('#778', () => {
291
+ expect(c('<td role="gridcell"></td>', '1.2').role?.name).toBe('gridcell');
292
+ expect(c('<td role="gridcell"></td>', '1.3').role?.name).toBe('gridcell');
293
+ });
294
+ });
@@ -0,0 +1,40 @@
1
+ const specs = require('@markuplint/html-spec');
2
+ const { createSelector } = require('@markuplint/selector');
3
+ const { createJSDOMElement } = require('@markuplint/test-tools');
4
+
5
+ const { getImplicitRole } = require('../../lib/dom-traverse/get-implicit-role');
6
+
7
+ function _(html, selector) {
8
+ return createJSDOMElement(html, selector, function (selector) {
9
+ // JSDOM supports no level 4 selectors yet.
10
+ return createSelector(selector, specs).match(this) !== false;
11
+ });
12
+ }
13
+
14
+ describe('1.2', () => {
15
+ test('<button>', () => {
16
+ const el = _('<button></button>');
17
+ const role = getImplicitRole(specs, el, '1.2');
18
+ expect(role.role?.name).toBe('button');
19
+ });
20
+
21
+ test('<td>', () => {
22
+ const el = _('<table><tr><td></td></tr></table>', 'td');
23
+ const role = getImplicitRole(specs, el, '1.2');
24
+ expect(role.role?.name).toBe('cell');
25
+ });
26
+ });
27
+
28
+ describe('1.3', () => {
29
+ test('<button>', () => {
30
+ const el = _('<button></button>');
31
+ const role = getImplicitRole(specs, el, '1.3');
32
+ expect(role.role?.name).toBe('button');
33
+ });
34
+
35
+ test('<td>', () => {
36
+ const el = _('<table><tr><td></td></tr></table>', 'td');
37
+ const role = getImplicitRole(specs, el, '1.3');
38
+ expect(role.role?.name).toBe('cell');
39
+ });
40
+ });
@@ -0,0 +1,34 @@
1
+ const specs = require('@markuplint/html-spec');
2
+ const { createSelector } = require('@markuplint/selector');
3
+ const { createJSDOMElement } = require('@markuplint/test-tools');
4
+
5
+ const { hasRequiredOwnedElement } = require('../../lib/dom-traverse/has-required-owned-elements');
6
+
7
+ function _(html, selector) {
8
+ return createJSDOMElement(html, selector, function (selector) {
9
+ // JSDOM supports no level 4 selectors yet.
10
+ return createSelector(selector, specs).match(this) !== false;
11
+ });
12
+ }
13
+
14
+ function m(html, version, selector) {
15
+ const el = _(html, selector);
16
+ return hasRequiredOwnedElement(el, specs, version);
17
+ }
18
+
19
+ describe('matchesOwnedRole', () => {
20
+ test('rowgroup > row', () => {
21
+ expect(m('<table><tbody><tr><td></td></tr></tbody></table>', '1.2')).toBe(true);
22
+ });
23
+ test('row', () => {
24
+ expect(m('<table><tr><td></td></tr></table>', '1.2', 'tr')).toBe(true);
25
+ });
26
+ test('list > listitem', () => {
27
+ expect(m('<ul><li></li></ul>', '1.2')).toBe(true);
28
+ expect(m('<ul><div><li></li></div></ul>', '1.2')).toBe(false);
29
+ expect(m('<ul><div role="none"><li></li></div></ul>', '1.2')).toBe(true);
30
+ expect(
31
+ m('<ul><div role="none"><div role="none"><div role="none"><li></li></div></div></div></ul>', '1.2'),
32
+ ).toBe(true);
33
+ });
34
+ });
@@ -0,0 +1,70 @@
1
+ const specs = require('@markuplint/html-spec');
2
+ const { createSelector } = require('@markuplint/selector');
3
+ const { createJSDOMElement } = require('@markuplint/test-tools');
4
+
5
+ const { isExposed } = require('../../lib/dom-traverse/is-exposed');
6
+
7
+ function _(html, selector) {
8
+ return createJSDOMElement(html, selector, function (selector) {
9
+ // JSDOM supports no level 4 selectors yet.
10
+ return createSelector(selector, specs).match(this) !== false;
11
+ });
12
+ }
13
+
14
+ function x(html, selector, version = '1.2') {
15
+ const el = _(html, selector);
16
+ return isExposed(el, specs, version);
17
+ }
18
+
19
+ describe('isExposed', () => {
20
+ test('Basic', () => {
21
+ expect(x('<div></div>')).toBe(true);
22
+ expect(x('<div aria-hidden="true"></div>')).toBe(false);
23
+ expect(x('<div role="presentation"></div>')).toBe(false);
24
+ expect(x('<div role="presentation" aria-hidden="true"></div>')).toBe(false);
25
+ expect(x('<div role="presentation"><span></span></div>', 'span')).toBe(true);
26
+ expect(x('<div role="presentation" aria-hidden="true"><span></span></div>', 'span')).toBe(false);
27
+ expect(x('<div style="display: none;"><span></span></div>', 'span')).toBe(false);
28
+ expect(x('<div hidden><span></span></div>', 'span')).toBe(false);
29
+ expect(x('<div hidden="until-found"><span></span></div>', 'span')).toBe(false);
30
+ expect(x('<html><body>content</body></html>', 'body')).toBe(true);
31
+ expect(x('<ul><li></li></ul>', 'li')).toBe(true);
32
+ });
33
+
34
+ test('Children Presentational: True', () => {
35
+ expect(x('<button></button>')).toBe(true);
36
+ expect(x('<button hidden></button>')).toBe(false);
37
+ expect(x('<button><span></span></button>', 'span')).toBe(false);
38
+ expect(x('<button hidden><span></span></button>', 'span')).toBe(false);
39
+ });
40
+
41
+ test('Hidden elements', () => {
42
+ expect(x('<input type="text">')).toBe(true);
43
+ expect(x('<input type="hidden">')).toBe(false);
44
+ expect(x('<meta>')).toBe(false);
45
+ expect(x('<style>a{}</style>')).toBe(false);
46
+ expect(x('<script>"";</script>')).toBe(false);
47
+ });
48
+
49
+ test('Recent element', () => {
50
+ expect(x('<canvas></canvas>')).toBe(true);
51
+ expect(x('<dialog></dialog>')).toBe(true);
52
+ expect(x('<details></details>')).toBe(true);
53
+ expect(x('<summary></summary>')).toBe(true);
54
+ expect(x('<meter></meter>')).toBe(true);
55
+ expect(x('<progress></progress>')).toBe(true);
56
+ expect(x('<picture></picture>')).toBe(true);
57
+ expect(x('<ruby></ruby>')).toBe(true);
58
+ expect(x('<pt></pt>')).toBe(true);
59
+ expect(x('<rp></rp>')).toBe(true);
60
+ expect(x('<time></time>')).toBe(true);
61
+ expect(x('<slot></slot>')).toBe(true);
62
+ expect(x('<portal></portal>')).toBe(true);
63
+ });
64
+
65
+ test('Unknown element', () => {
66
+ expect(x('<unknown></unknown>')).toBe(true);
67
+ expect(x('<font></font>')).toBe(true);
68
+ expect(x('<x-component></x-component>')).toBe(true);
69
+ });
70
+ });
@@ -0,0 +1,60 @@
1
+ const specs = require('@markuplint/html-spec');
2
+ const { createSelector } = require('@markuplint/selector');
3
+ const { createJSDOMElement } = require('@markuplint/test-tools');
4
+
5
+ const { matchesContextRole } = require('../../lib/dom-traverse/matches-context-role');
6
+
7
+ function _(html, selector) {
8
+ return createJSDOMElement(html, selector, function (selector) {
9
+ // JSDOM supports no level 4 selectors yet.
10
+ return createSelector(selector, specs).match(this) !== false;
11
+ });
12
+ }
13
+
14
+ describe('1.2', () => {
15
+ const version = '1.2';
16
+
17
+ test('rowgroup', () => {
18
+ const el = _('<table><tbody><tr><td></td></tr></tbody></table>', 'tr');
19
+ expect(matchesContextRole(['rowgroup'], el, specs, version)).toBe(true);
20
+ });
21
+
22
+ test('rowgroup owned by table', () => {
23
+ const el = _('<table><tbody><tr><td></td></tr></tbody></table>', 'tr');
24
+ expect(matchesContextRole(['table > rowgroup'], el, specs, version)).toBe(true);
25
+ });
26
+
27
+ test('no rowgroup', () => {
28
+ const el = _('<table><tbody><tr><td></td></tr></tbody></table>', 'tr');
29
+ expect(matchesContextRole(['group'], el, specs, version)).toBe(false);
30
+ });
31
+
32
+ test('rowgroup owned by no table', () => {
33
+ const el = _('<table><tbody><tr><td></td></tr></tbody></table>', 'tr');
34
+ expect(matchesContextRole(['grid > rowgroup'], el, specs, version)).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('1.3', () => {
39
+ const version = '1.3';
40
+
41
+ test('rowgroup', () => {
42
+ const el = _('<table><tbody><tr><td></td></tr></tbody></table>', 'tr');
43
+ expect(matchesContextRole(['rowgroup'], el, specs, version)).toBe(true);
44
+ });
45
+
46
+ test('rowgroup owned by table', () => {
47
+ const el = _('<table><tbody><tr><td></td></tr></tbody></table>', 'tr');
48
+ expect(matchesContextRole(['table > rowgroup'], el, specs, version)).toBe(true);
49
+ });
50
+
51
+ test('no rowgroup', () => {
52
+ const el = _('<table><tbody><tr><td></td></tr></tbody></table>', 'tr');
53
+ expect(matchesContextRole(['group'], el, specs, version)).toBe(false);
54
+ });
55
+
56
+ test('rowgroup owned by no table', () => {
57
+ const el = _('<table><tbody><tr><td></td></tr></tbody></table>', 'tr');
58
+ expect(matchesContextRole(['grid > rowgroup'], el, specs, version)).toBe(false);
59
+ });
60
+ });
@@ -0,0 +1,41 @@
1
+ const htmlSpec = require('@markuplint/html-spec');
2
+
3
+ const { getAttrSpecs } = require('../../lib/specs/get-attr-specs');
4
+
5
+ describe('getSpec', () => {
6
+ test('svg:image[x]', () => {
7
+ const specs = getAttrSpecs('image', 'http://www.w3.org/2000/svg', htmlSpec);
8
+ const x = specs?.find(spec => spec.name === 'x');
9
+ expect(x).toStrictEqual({
10
+ defaultValue: '0',
11
+ name: 'x',
12
+ type: ['<svg-length>', '<percentage>'],
13
+ animatable: true,
14
+ });
15
+ });
16
+
17
+ test('svg:foreignObject[x]', () => {
18
+ const specs = getAttrSpecs('foreignObject', 'http://www.w3.org/2000/svg', htmlSpec);
19
+ const x = specs?.find(spec => spec.name === 'x');
20
+ expect(x).toStrictEqual({
21
+ defaultValue: '0',
22
+ description:
23
+ 'The x coordinate of the foreignObject. Value type: <length>|<percentage> ; Default value: 0; Animatable: yes',
24
+ name: 'x',
25
+ type: ['<svg-length>', '<percentage>'],
26
+ animatable: true,
27
+ });
28
+ });
29
+
30
+ test('svg:linearGradient[xlink:href]', () => {
31
+ const specs = getAttrSpecs('linearGradient', 'http://www.w3.org/2000/svg', htmlSpec);
32
+ const x = specs?.find(spec => spec.name === 'xlink:href');
33
+ expect(x).toStrictEqual({
34
+ description:
35
+ 'Deprecated: This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.An <IRI> reference to another <linearGradient> element that will be used as a template. Value type: <IRI> ; Default value: none; Animatable: yes',
36
+ name: 'xlink:href',
37
+ type: 'URL',
38
+ deprecated: true,
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,79 @@
1
+ const specs = require('@markuplint/html-spec');
2
+ const { createSelector } = require('@markuplint/selector');
3
+ const { createJSDOMElement } = require('@markuplint/test-tools');
4
+
5
+ const { getImplicitRole } = require('../../lib/specs/get-implicit-role');
6
+
7
+ function c(html, version) {
8
+ const el = createJSDOMElement(html);
9
+ return getImplicitRole(
10
+ specs,
11
+ el.localName,
12
+ el.namespaceURI,
13
+ version,
14
+ selector => createSelector(selector, specs).match(el) !== false,
15
+ );
16
+ }
17
+
18
+ describe('getImplicitRole', () => {
19
+ test('the a element', () => {
20
+ expect(c('<a></a>', '1.2')).toBe(false);
21
+ expect(c('<a name="foo"></a>', '1.2')).toBe(false);
22
+ expect(c('<a href></a>', '1.2')).toBe('link');
23
+ expect(c('<a href=""></a>', '1.2')).toBe('link');
24
+ expect(c('<a href="path/to"></a>', '1.2')).toBe('link');
25
+ });
26
+
27
+ test('the area element', () => {
28
+ expect(c('<area />', '1.2')).toBe('generic');
29
+ expect(c('<area shape="rect" />', '1.2')).toBe('generic');
30
+ expect(c('<area href />', '1.2')).toBe('link');
31
+ expect(c('<area href="" />', '1.2')).toBe('link');
32
+ expect(c('<area href="path/to" />', '1.2')).toBe('link');
33
+ });
34
+
35
+ test('the article element', () => {
36
+ expect(c('<article></article>', '1.2')).toBe('article');
37
+ });
38
+
39
+ test('the aside element', () => {
40
+ expect(c('<aside></aside>', '1.2')).toBe('complementary');
41
+ });
42
+
43
+ test('the audio element', () => {
44
+ expect(c('<audio></audio>', '1.2')).toBe(false);
45
+ });
46
+
47
+ test('the h1 element', () => {
48
+ expect(c('<h1></h1>', '1.2')).toBe('heading');
49
+ });
50
+
51
+ test('the header element', () => {
52
+ expect(c('<header></header>', '1.2')).toBe('banner');
53
+ expect(c('<header><article></article></header>', '1.2')).toBe('generic');
54
+ expect(c('<header><article></article></header>', '1.1')).toBe(false);
55
+ expect(c('<header><div></div></header>', '1.2')).toBe('banner');
56
+ expect(c('<header><div role="article"></div></header>', '1.2')).toBe('generic');
57
+ expect(c('<header><div role="article"></div></header>', '1.1')).toBe(false);
58
+ });
59
+
60
+ test('the img element', () => {
61
+ expect(c('<img />', '1.2')).toBe('img');
62
+ expect(c('<img alt />', '1.2')).toBe('presentation');
63
+ expect(c('<img alt="" />', '1.2')).toBe('presentation');
64
+ expect(c('<img alt="foo" />', '1.2')).toBe('img');
65
+ expect(c('<img aria-label="foo" />', '1.2')).toBe('img');
66
+ });
67
+
68
+ test('the form element', () => {
69
+ expect(c('<form></form>', '1.2')).toBe(false);
70
+ expect(c('<form></form>', '1.1')).toBe(false);
71
+ expect(c('<form aria-label="foo"></form>', '1.2')).toBe('form');
72
+ expect(c('<form aria-label="foo"></form>', '1.1')).toBe('form');
73
+ });
74
+
75
+ test('the section element', () => {
76
+ expect(c('<section></section>', '1.2')).toBe(false);
77
+ expect(c('<section aria-label="foo"></section>', '1.2')).toBe('region');
78
+ });
79
+ });