accented 1.2.6 → 1.3.1
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.
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +32 -0
- package/dist/constants.js.map +1 -1
- package/dist/elements/accented-trigger.js +1 -1
- package/dist/elements/accented-trigger.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +79 -41
- package/dist/scanner.js.map +1 -1
- package/dist/utils/create-extended-element-with-issues.d.ts +3 -0
- package/dist/utils/create-extended-element-with-issues.d.ts.map +1 -0
- package/dist/utils/create-extended-element-with-issues.js +56 -0
- package/dist/utils/create-extended-element-with-issues.js.map +1 -0
- package/dist/utils/get-all-rules-from-axe-options.d.ts +3 -0
- package/dist/utils/get-all-rules-from-axe-options.d.ts.map +1 -0
- package/dist/utils/get-all-rules-from-axe-options.js +51 -0
- package/dist/utils/get-all-rules-from-axe-options.js.map +1 -0
- package/dist/utils/transform-violations.d.ts.map +1 -1
- package/dist/utils/transform-violations.js +2 -13
- package/dist/utils/transform-violations.js.map +1 -1
- package/dist/utils/update-elements-with-issues.d.ts +4 -3
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.js +36 -80
- package/dist/utils/update-elements-with-issues.js.map +1 -1
- package/package.json +5 -5
- package/src/constants.ts +34 -0
- package/src/elements/accented-trigger.ts +1 -1
- package/src/scanner.ts +91 -45
- package/src/utils/create-extended-element-with-issues.ts +67 -0
- package/src/utils/get-all-rules-from-axe-options.test.ts +169 -0
- package/src/utils/get-all-rules-from-axe-options.ts +54 -0
- package/src/utils/transform-violations.ts +2 -14
- package/src/utils/update-elements-with-issues.test.ts +223 -139
- package/src/utils/update-elements-with-issues.ts +76 -107
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tests in this test suite have e2e counterparts in axe-options.spec.ts.
|
|
3
|
+
* If we ever change this file, we should make sure axe-options.spec.ts
|
|
4
|
+
* doesn't diverge.
|
|
5
|
+
* That way, we can be sure that our reimplementation of runOnly / rules
|
|
6
|
+
* is in sync with that in axe-core.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { suite, test } from 'node:test';
|
|
11
|
+
import { getAllRulesFromAxeOptions } from './get-all-rules-from-axe-options';
|
|
12
|
+
|
|
13
|
+
suite('getAllRulesFromAxeOptions', () => {
|
|
14
|
+
test('with no options, returns all rules', () => {
|
|
15
|
+
const rules = getAllRulesFromAxeOptions({});
|
|
16
|
+
assert.ok(rules.size > 90);
|
|
17
|
+
assert.ok(rules.has('page-has-heading-one'));
|
|
18
|
+
assert.ok(rules.has('color-contrast'));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('with no options, excludes disabled-by-default rules', () => {
|
|
22
|
+
const rules = getAllRulesFromAxeOptions({});
|
|
23
|
+
// axe-core disables some rules by default
|
|
24
|
+
assert.equal(rules.has('color-contrast-enhanced'), false);
|
|
25
|
+
assert.equal(rules.has('target-size'), false);
|
|
26
|
+
assert.equal(rules.has('audio-caption'), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('with runOnly type rule, returns exactly the specified rules', () => {
|
|
30
|
+
const rules = getAllRulesFromAxeOptions({
|
|
31
|
+
runOnly: { type: 'rule', values: ['color-contrast', 'button-name'] },
|
|
32
|
+
});
|
|
33
|
+
assert.equal(rules.size, 2);
|
|
34
|
+
assert.ok(rules.has('color-contrast'));
|
|
35
|
+
assert.ok(rules.has('button-name'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('with runOnly type rule, ignores rules.enabled: true for a rule not in runOnly', () => {
|
|
39
|
+
const rules = getAllRulesFromAxeOptions({
|
|
40
|
+
runOnly: { type: 'rule', values: ['button-name'] },
|
|
41
|
+
rules: { 'color-contrast': { enabled: true } },
|
|
42
|
+
});
|
|
43
|
+
assert.equal(rules.size, 1);
|
|
44
|
+
assert.equal(rules.has('color-contrast'), false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('with runOnly type rule, ignores rules.enabled: false', () => {
|
|
48
|
+
const rules = getAllRulesFromAxeOptions({
|
|
49
|
+
runOnly: { type: 'rule', values: ['color-contrast'] },
|
|
50
|
+
rules: { 'color-contrast': { enabled: false } },
|
|
51
|
+
});
|
|
52
|
+
assert.equal(rules.size, 1);
|
|
53
|
+
assert.ok(rules.has('color-contrast'));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("with runOnly type 'rules' (plural), behaves the same as 'rule'", () => {
|
|
57
|
+
const rules = getAllRulesFromAxeOptions({
|
|
58
|
+
runOnly: { type: 'rules', values: ['button-name'] },
|
|
59
|
+
rules: { 'color-contrast': { enabled: true } },
|
|
60
|
+
});
|
|
61
|
+
assert.equal(rules.size, 1);
|
|
62
|
+
assert.ok(rules.has('button-name'));
|
|
63
|
+
assert.equal(rules.has('color-contrast'), false); // rules ignored, same as type 'rule'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('with runOnly type tag, returns rules matching those tags', () => {
|
|
67
|
+
const rules = getAllRulesFromAxeOptions({
|
|
68
|
+
runOnly: { type: 'tag', values: ['best-practice'] },
|
|
69
|
+
});
|
|
70
|
+
assert.ok(rules.has('page-has-heading-one'));
|
|
71
|
+
assert.equal(rules.has('color-contrast'), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('with runOnly type tag and multiple tags, returns rules from all specified tags', () => {
|
|
75
|
+
const rules = getAllRulesFromAxeOptions({
|
|
76
|
+
runOnly: { type: 'tag', values: ['best-practice', 'wcag2aa'] },
|
|
77
|
+
});
|
|
78
|
+
assert.ok(rules.has('page-has-heading-one'));
|
|
79
|
+
assert.ok(rules.has('color-contrast'));
|
|
80
|
+
assert.equal(rules.has('button-name'), false); // wcag2a only, not in either tag
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('with runOnly type tag, rules.enabled: false removes a matching rule', () => {
|
|
84
|
+
const rules = getAllRulesFromAxeOptions({
|
|
85
|
+
runOnly: { type: 'tag', values: ['wcag2aa'] },
|
|
86
|
+
rules: { 'color-contrast': { enabled: false } },
|
|
87
|
+
});
|
|
88
|
+
assert.ok(rules.has('meta-viewport')); // another wcag2aa rule, still included
|
|
89
|
+
assert.equal(rules.has('color-contrast'), false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('with runOnly type tag, rules.enabled: true adds a non-matching rule', () => {
|
|
93
|
+
const rules = getAllRulesFromAxeOptions({
|
|
94
|
+
runOnly: { type: 'tag', values: ['best-practice'] },
|
|
95
|
+
rules: { 'color-contrast': { enabled: true } },
|
|
96
|
+
});
|
|
97
|
+
assert.ok(rules.has('page-has-heading-one'));
|
|
98
|
+
assert.ok(rules.has('color-contrast'));
|
|
99
|
+
assert.equal(rules.has('button-name'), false); // not in best-practice and not explicitly enabled
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("with runOnly type 'tags' (plural), behaves the same as 'tag'", () => {
|
|
103
|
+
const rules = getAllRulesFromAxeOptions({
|
|
104
|
+
runOnly: { type: 'tags', values: ['best-practice'] },
|
|
105
|
+
rules: { 'color-contrast': { enabled: true } },
|
|
106
|
+
});
|
|
107
|
+
assert.ok(rules.has('page-has-heading-one'));
|
|
108
|
+
assert.ok(rules.has('color-contrast')); // rules applied, same as type 'tag'
|
|
109
|
+
assert.equal(rules.has('button-name'), false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('with no runOnly and rules overrides, still excludes disabled-by-default rules', () => {
|
|
113
|
+
const rules = getAllRulesFromAxeOptions({
|
|
114
|
+
rules: { 'color-contrast': { enabled: false }, 'button-name': { enabled: true } },
|
|
115
|
+
});
|
|
116
|
+
assert.equal(rules.has('color-contrast'), false); // explicitly disabled
|
|
117
|
+
assert.ok(rules.has('button-name')); // explicitly enabled
|
|
118
|
+
assert.equal(rules.has('color-contrast-enhanced'), false); // disabled by default, no override
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('with no runOnly, rules.enabled: false removes a rule', () => {
|
|
122
|
+
const rules = getAllRulesFromAxeOptions({
|
|
123
|
+
rules: { 'color-contrast': { enabled: false } },
|
|
124
|
+
});
|
|
125
|
+
assert.ok(rules.size > 90);
|
|
126
|
+
assert.equal(rules.has('color-contrast'), false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('with runOnly as a tag array shorthand, treats it as tag values', () => {
|
|
130
|
+
const rules = getAllRulesFromAxeOptions({ runOnly: ['best-practice'] });
|
|
131
|
+
assert.ok(rules.has('page-has-heading-one'));
|
|
132
|
+
assert.equal(rules.has('color-contrast'), false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('with runOnly as a single tag string, treats it as a tag value', () => {
|
|
136
|
+
const rules = getAllRulesFromAxeOptions({ runOnly: 'best-practice' });
|
|
137
|
+
assert.ok(rules.has('page-has-heading-one'));
|
|
138
|
+
assert.equal(rules.has('color-contrast'), false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('with runOnly as a rule ID string shorthand, returns that rule', () => {
|
|
142
|
+
const rules = getAllRulesFromAxeOptions({ runOnly: 'color-contrast' });
|
|
143
|
+
assert.equal(rules.size, 1);
|
|
144
|
+
assert.ok(rules.has('color-contrast'));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('with runOnly as a rule ID array shorthand, returns exactly those rules', () => {
|
|
148
|
+
const rules = getAllRulesFromAxeOptions({ runOnly: ['color-contrast', 'button-name'] });
|
|
149
|
+
assert.equal(rules.size, 2);
|
|
150
|
+
assert.ok(rules.has('color-contrast'));
|
|
151
|
+
assert.ok(rules.has('button-name'));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('with runOnly as a rule ID array shorthand, ignores rules overrides', () => {
|
|
155
|
+
const rules = getAllRulesFromAxeOptions({
|
|
156
|
+
runOnly: ['color-contrast', 'button-name'],
|
|
157
|
+
rules: { 'color-contrast': { enabled: false }, 'aria-label': { enabled: true } },
|
|
158
|
+
});
|
|
159
|
+
assert.equal(rules.size, 2);
|
|
160
|
+
assert.ok(rules.has('color-contrast'));
|
|
161
|
+
assert.ok(rules.has('button-name'));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('with runOnly as a mixed shorthand array (tag + rule ID), throws', () => {
|
|
165
|
+
assert.throws(() =>
|
|
166
|
+
getAllRulesFromAxeOptions({ runOnly: ['best-practice', 'color-contrast'] }),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* Adapts two axe-core behaviors:
|
|
2
|
+
- shorthand string/array runOnly normalization from normalizeOptions:
|
|
3
|
+
https://github.com/dequelabs/axe-core/blob/9261d074b60527a84f4dce5a64a6d5a5843a0772/lib/core/base/audit.js#L408-L428
|
|
4
|
+
- which rules to include for a given runOnly/rules combination, from ruleShouldRun (lines 65–80,
|
|
5
|
+
omitting the rule.pageLevel check — axe applies that itself when we call axe.run):
|
|
6
|
+
https://github.com/dequelabs/axe-core/blob/9261d074b60527a84f4dce5a64a6d5a5843a0772/lib/core/utils/rule-should-run.js */
|
|
7
|
+
import axe from 'axe-core';
|
|
8
|
+
import type { AxeOptions } from '../types.ts';
|
|
9
|
+
|
|
10
|
+
function getRuleIds(tags?: Array<string>): Set<string> {
|
|
11
|
+
return new Set(axe.getRules(tags).map((r) => r.ruleId));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function applyOverrides(ruleSet: Set<string>, rules: AxeOptions['rules']): Set<string> {
|
|
15
|
+
if (!rules) return ruleSet;
|
|
16
|
+
for (const [ruleId, ruleConfig] of Object.entries(rules)) {
|
|
17
|
+
if (ruleConfig.enabled === false) ruleSet.delete(ruleId);
|
|
18
|
+
else if (ruleConfig.enabled === true) ruleSet.add(ruleId);
|
|
19
|
+
}
|
|
20
|
+
return ruleSet;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Normalizes the string/array shorthands of runOnly into the { type, values } object form.
|
|
24
|
+
function normalizeRunOnly(
|
|
25
|
+
runOnly: Exclude<AxeOptions['runOnly'], undefined>,
|
|
26
|
+
allRuleIds: Set<string>,
|
|
27
|
+
): { type: string; values: string[] } {
|
|
28
|
+
if (typeof runOnly !== 'string' && !Array.isArray(runOnly)) return runOnly;
|
|
29
|
+
const values = typeof runOnly === 'string' ? [runOnly] : runOnly;
|
|
30
|
+
const isRulePath = values.every((v) => allRuleIds.has(v));
|
|
31
|
+
const isTagPath = values.every((v) => !allRuleIds.has(v));
|
|
32
|
+
if (!isRulePath && !isTagPath)
|
|
33
|
+
throw new Error(`runOnly mixes rule IDs and tag values: ${values.join(', ')}`);
|
|
34
|
+
return { type: isRulePath ? 'rule' : 'tag', values };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getAllRulesFromAxeOptions(axeOptions: AxeOptions): Set<string> {
|
|
38
|
+
const allRuleIds = getRuleIds();
|
|
39
|
+
const { rules, runOnly } = axeOptions;
|
|
40
|
+
|
|
41
|
+
if (runOnly === undefined) {
|
|
42
|
+
// axe.getRules() includes rules disabled by default; axe skips them via rule.enabled !== false.
|
|
43
|
+
// Replicate that here using the internal _audit.rules, which exposes the enabled flag.
|
|
44
|
+
// @ts-expect-error: _audit is an undocumented internal axe-core API not present in its type definitions
|
|
45
|
+
for (const rule of axe._audit.rules) {
|
|
46
|
+
if (rule.enabled === false) allRuleIds.delete(rule.id);
|
|
47
|
+
}
|
|
48
|
+
return applyOverrides(allRuleIds, rules);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { type, values } = normalizeRunOnly(runOnly, allRuleIds);
|
|
52
|
+
if (type === 'rule' || type === 'rules') return new Set(values);
|
|
53
|
+
return applyOverrides(getRuleIds(values), rules);
|
|
54
|
+
}
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import type { AxeResults } from 'axe-core';
|
|
2
|
-
import { issuesUrl, orderedImpacts } from '../constants.js';
|
|
2
|
+
import { issuesUrl, orderedImpacts, violationsAffectedByAccentedTriggers } from '../constants.js';
|
|
3
3
|
import type { ElementWithIssues, Issue } from '../types.ts';
|
|
4
4
|
|
|
5
|
-
// This is a list of axe-core violations (their ids) that may be flagged by axe-core
|
|
6
|
-
// as false positives if an Accented trigger is a descendant of the element with the issue.
|
|
7
|
-
const violationsAffectedByAccentedTriggers = [
|
|
8
|
-
'aria-hidden-focus',
|
|
9
|
-
'aria-text',
|
|
10
|
-
'definition-list',
|
|
11
|
-
'label-content-name-mismatch',
|
|
12
|
-
'list',
|
|
13
|
-
'nested-interactive',
|
|
14
|
-
'scrollable-region-focusable', // The Accented trigger might make the content grow such that scrolling is required.
|
|
15
|
-
];
|
|
16
|
-
|
|
17
5
|
function maybeCausedByAccented(violationId: string, element: HTMLElement, name: string) {
|
|
18
6
|
return (
|
|
19
|
-
violationsAffectedByAccentedTriggers.
|
|
7
|
+
violationsAffectedByAccentedTriggers.has(violationId) &&
|
|
20
8
|
Boolean(element.querySelector(`${name}-trigger`))
|
|
21
9
|
);
|
|
22
10
|
}
|
|
@@ -77,6 +77,11 @@ const node3: AxeNode = {
|
|
|
77
77
|
element: element3,
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
const htmlNode: AxeNode = {
|
|
81
|
+
...commonNodeProps,
|
|
82
|
+
element: document.documentElement,
|
|
83
|
+
};
|
|
84
|
+
|
|
80
85
|
const commonViolationProps = {
|
|
81
86
|
help: 'help',
|
|
82
87
|
helpUrl: 'http://example.com',
|
|
@@ -109,6 +114,18 @@ const violation4: Violation = {
|
|
|
109
114
|
nodes: [node3],
|
|
110
115
|
};
|
|
111
116
|
|
|
117
|
+
const headingViolation: Violation = {
|
|
118
|
+
...commonViolationProps,
|
|
119
|
+
id: 'page-has-heading-one',
|
|
120
|
+
nodes: [htmlNode],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const langViolation: Violation = {
|
|
124
|
+
...commonViolationProps,
|
|
125
|
+
id: 'html-has-lang',
|
|
126
|
+
nodes: [htmlNode],
|
|
127
|
+
};
|
|
128
|
+
|
|
112
129
|
const commonIssueProps = {
|
|
113
130
|
title: 'help',
|
|
114
131
|
description: 'description',
|
|
@@ -131,43 +148,56 @@ const issue3: Issue = {
|
|
|
131
148
|
...commonIssueProps,
|
|
132
149
|
};
|
|
133
150
|
|
|
134
|
-
const
|
|
151
|
+
const headingIssue: Issue = {
|
|
152
|
+
id: 'page-has-heading-one',
|
|
153
|
+
...commonIssueProps,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const langIssue: Issue = {
|
|
157
|
+
id: 'html-has-lang',
|
|
158
|
+
...commonIssueProps,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const fullDocumentContext = {
|
|
135
162
|
include: [document],
|
|
136
163
|
exclude: [],
|
|
137
164
|
};
|
|
138
165
|
|
|
166
|
+
const narrowContext = {
|
|
167
|
+
include: [element1],
|
|
168
|
+
exclude: [],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function createElementsWithIssues(
|
|
172
|
+
items: Array<{ id: number; element: HTMLElement; issues: Array<Issue> }>,
|
|
173
|
+
): Signal<Array<ExtendedElementWithIssues>> {
|
|
174
|
+
return signal(
|
|
175
|
+
items.map(({ id, element, issues }) => ({
|
|
176
|
+
id,
|
|
177
|
+
element,
|
|
178
|
+
rootNode,
|
|
179
|
+
skipRender: false,
|
|
180
|
+
position,
|
|
181
|
+
visible,
|
|
182
|
+
trigger,
|
|
183
|
+
anchorNameValue: 'none',
|
|
184
|
+
scrollableAncestors,
|
|
185
|
+
issues: signal(issues),
|
|
186
|
+
})),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
139
190
|
suite('updateElementsWithIssues', () => {
|
|
140
191
|
test('no changes', () => {
|
|
141
|
-
const extendedElementsWithIssues
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
element: element1,
|
|
145
|
-
rootNode,
|
|
146
|
-
skipRender: false,
|
|
147
|
-
position,
|
|
148
|
-
visible,
|
|
149
|
-
trigger,
|
|
150
|
-
anchorNameValue: 'none',
|
|
151
|
-
scrollableAncestors,
|
|
152
|
-
issues: signal([issue1]),
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
id: 2,
|
|
156
|
-
element: element2,
|
|
157
|
-
rootNode,
|
|
158
|
-
skipRender: false,
|
|
159
|
-
position,
|
|
160
|
-
visible,
|
|
161
|
-
trigger,
|
|
162
|
-
anchorNameValue: 'none',
|
|
163
|
-
scrollableAncestors,
|
|
164
|
-
issues: signal([issue2]),
|
|
165
|
-
},
|
|
192
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
193
|
+
{ id: 1, element: element1, issues: [issue1] },
|
|
194
|
+
{ id: 2, element: element2, issues: [issue2] },
|
|
166
195
|
]);
|
|
167
196
|
updateElementsWithIssues({
|
|
168
197
|
extendedElementsWithIssues,
|
|
169
|
-
|
|
170
|
-
|
|
198
|
+
limitedContext: fullDocumentContext,
|
|
199
|
+
limitedContextViolations: [violation1, violation2],
|
|
200
|
+
fullContextViolations: [],
|
|
171
201
|
name: 'accented',
|
|
172
202
|
});
|
|
173
203
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
@@ -178,36 +208,15 @@ suite('updateElementsWithIssues', () => {
|
|
|
178
208
|
});
|
|
179
209
|
|
|
180
210
|
test('one issue added', () => {
|
|
181
|
-
const extendedElementsWithIssues
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
element: element1,
|
|
185
|
-
rootNode,
|
|
186
|
-
skipRender: false,
|
|
187
|
-
position,
|
|
188
|
-
visible,
|
|
189
|
-
trigger,
|
|
190
|
-
anchorNameValue: 'none',
|
|
191
|
-
scrollableAncestors,
|
|
192
|
-
issues: signal([issue1]),
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
id: 2,
|
|
196
|
-
element: element2,
|
|
197
|
-
rootNode,
|
|
198
|
-
skipRender: false,
|
|
199
|
-
position,
|
|
200
|
-
visible,
|
|
201
|
-
trigger,
|
|
202
|
-
anchorNameValue: 'none',
|
|
203
|
-
scrollableAncestors,
|
|
204
|
-
issues: signal([issue2]),
|
|
205
|
-
},
|
|
211
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
212
|
+
{ id: 1, element: element1, issues: [issue1] },
|
|
213
|
+
{ id: 2, element: element2, issues: [issue2] },
|
|
206
214
|
]);
|
|
207
215
|
updateElementsWithIssues({
|
|
208
216
|
extendedElementsWithIssues,
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
limitedContext: fullDocumentContext,
|
|
218
|
+
limitedContextViolations: [violation1, violation2, violation3],
|
|
219
|
+
fullContextViolations: [],
|
|
211
220
|
name: 'accented',
|
|
212
221
|
});
|
|
213
222
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
@@ -218,36 +227,15 @@ suite('updateElementsWithIssues', () => {
|
|
|
218
227
|
});
|
|
219
228
|
|
|
220
229
|
test('one issue removed', () => {
|
|
221
|
-
const extendedElementsWithIssues
|
|
222
|
-
{
|
|
223
|
-
|
|
224
|
-
element: element1,
|
|
225
|
-
rootNode,
|
|
226
|
-
skipRender: false,
|
|
227
|
-
position,
|
|
228
|
-
visible,
|
|
229
|
-
trigger,
|
|
230
|
-
anchorNameValue: 'none',
|
|
231
|
-
scrollableAncestors,
|
|
232
|
-
issues: signal([issue1]),
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
id: 2,
|
|
236
|
-
element: element2,
|
|
237
|
-
rootNode,
|
|
238
|
-
skipRender: false,
|
|
239
|
-
position,
|
|
240
|
-
visible,
|
|
241
|
-
trigger,
|
|
242
|
-
anchorNameValue: 'none',
|
|
243
|
-
scrollableAncestors,
|
|
244
|
-
issues: signal([issue2, issue3]),
|
|
245
|
-
},
|
|
230
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
231
|
+
{ id: 1, element: element1, issues: [issue1] },
|
|
232
|
+
{ id: 2, element: element2, issues: [issue2, issue3] },
|
|
246
233
|
]);
|
|
247
234
|
updateElementsWithIssues({
|
|
248
235
|
extendedElementsWithIssues,
|
|
249
|
-
|
|
250
|
-
|
|
236
|
+
limitedContext: fullDocumentContext,
|
|
237
|
+
limitedContextViolations: [violation1, violation2],
|
|
238
|
+
fullContextViolations: [],
|
|
251
239
|
name: 'accented',
|
|
252
240
|
});
|
|
253
241
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
@@ -258,24 +246,14 @@ suite('updateElementsWithIssues', () => {
|
|
|
258
246
|
});
|
|
259
247
|
|
|
260
248
|
test('one element added', () => {
|
|
261
|
-
const extendedElementsWithIssues
|
|
262
|
-
{
|
|
263
|
-
id: 1,
|
|
264
|
-
element: element1,
|
|
265
|
-
rootNode,
|
|
266
|
-
skipRender: false,
|
|
267
|
-
position,
|
|
268
|
-
visible,
|
|
269
|
-
trigger,
|
|
270
|
-
anchorNameValue: 'none',
|
|
271
|
-
scrollableAncestors,
|
|
272
|
-
issues: signal([issue1]),
|
|
273
|
-
},
|
|
249
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
250
|
+
{ id: 1, element: element1, issues: [issue1] },
|
|
274
251
|
]);
|
|
275
252
|
updateElementsWithIssues({
|
|
276
253
|
extendedElementsWithIssues,
|
|
277
|
-
|
|
278
|
-
|
|
254
|
+
limitedContext: fullDocumentContext,
|
|
255
|
+
limitedContextViolations: [violation1, violation2],
|
|
256
|
+
fullContextViolations: [],
|
|
279
257
|
name: 'accented',
|
|
280
258
|
});
|
|
281
259
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
@@ -286,24 +264,14 @@ suite('updateElementsWithIssues', () => {
|
|
|
286
264
|
});
|
|
287
265
|
|
|
288
266
|
test('one disconnected element added', () => {
|
|
289
|
-
const extendedElementsWithIssues
|
|
290
|
-
{
|
|
291
|
-
id: 1,
|
|
292
|
-
element: element1,
|
|
293
|
-
rootNode,
|
|
294
|
-
skipRender: false,
|
|
295
|
-
position,
|
|
296
|
-
visible,
|
|
297
|
-
trigger,
|
|
298
|
-
anchorNameValue: 'none',
|
|
299
|
-
scrollableAncestors,
|
|
300
|
-
issues: signal([issue1]),
|
|
301
|
-
},
|
|
267
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
268
|
+
{ id: 1, element: element1, issues: [issue1] },
|
|
302
269
|
]);
|
|
303
270
|
updateElementsWithIssues({
|
|
304
271
|
extendedElementsWithIssues,
|
|
305
|
-
|
|
306
|
-
|
|
272
|
+
limitedContext: fullDocumentContext,
|
|
273
|
+
limitedContextViolations: [violation1, violation4],
|
|
274
|
+
fullContextViolations: [],
|
|
307
275
|
name: 'accented',
|
|
308
276
|
});
|
|
309
277
|
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
@@ -311,40 +279,156 @@ suite('updateElementsWithIssues', () => {
|
|
|
311
279
|
});
|
|
312
280
|
|
|
313
281
|
test('one element removed', () => {
|
|
314
|
-
const extendedElementsWithIssues
|
|
315
|
-
{
|
|
316
|
-
|
|
317
|
-
element: element1,
|
|
318
|
-
rootNode,
|
|
319
|
-
skipRender: false,
|
|
320
|
-
position,
|
|
321
|
-
visible,
|
|
322
|
-
trigger,
|
|
323
|
-
anchorNameValue: 'none',
|
|
324
|
-
scrollableAncestors,
|
|
325
|
-
issues: signal([issue1]),
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
id: 2,
|
|
329
|
-
element: element2,
|
|
330
|
-
rootNode,
|
|
331
|
-
skipRender: false,
|
|
332
|
-
position,
|
|
333
|
-
visible,
|
|
334
|
-
trigger,
|
|
335
|
-
anchorNameValue: 'none',
|
|
336
|
-
scrollableAncestors,
|
|
337
|
-
issues: signal([issue2]),
|
|
338
|
-
},
|
|
282
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
283
|
+
{ id: 1, element: element1, issues: [issue1] },
|
|
284
|
+
{ id: 2, element: element2, issues: [issue2] },
|
|
339
285
|
]);
|
|
340
286
|
updateElementsWithIssues({
|
|
341
287
|
extendedElementsWithIssues,
|
|
342
|
-
|
|
343
|
-
|
|
288
|
+
limitedContext: fullDocumentContext,
|
|
289
|
+
limitedContextViolations: [violation1],
|
|
290
|
+
fullContextViolations: [],
|
|
344
291
|
name: 'accented',
|
|
345
292
|
});
|
|
346
293
|
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
347
294
|
assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
|
|
348
295
|
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
349
296
|
});
|
|
297
|
+
|
|
298
|
+
test('strips descendant-dependent issue from element outside limited context when full context no longer reports it', () => {
|
|
299
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
300
|
+
{ id: 1, element: document.documentElement, issues: [headingIssue] },
|
|
301
|
+
]);
|
|
302
|
+
updateElementsWithIssues({
|
|
303
|
+
extendedElementsWithIssues,
|
|
304
|
+
limitedContext: narrowContext,
|
|
305
|
+
limitedContextViolations: [],
|
|
306
|
+
fullContextViolations: [],
|
|
307
|
+
name: 'accented',
|
|
308
|
+
});
|
|
309
|
+
assert.equal(extendedElementsWithIssues.value.length, 0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('keeps descendant-dependent issue on element outside limited context when full context still reports it', () => {
|
|
313
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
314
|
+
{ id: 1, element: document.documentElement, issues: [headingIssue] },
|
|
315
|
+
]);
|
|
316
|
+
updateElementsWithIssues({
|
|
317
|
+
extendedElementsWithIssues,
|
|
318
|
+
limitedContext: narrowContext,
|
|
319
|
+
limitedContextViolations: [],
|
|
320
|
+
fullContextViolations: [headingViolation],
|
|
321
|
+
name: 'accented',
|
|
322
|
+
});
|
|
323
|
+
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
324
|
+
assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
|
|
325
|
+
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
326
|
+
assert.equal(extendedElementsWithIssues.value[0]?.issues.value[0]?.id, 'page-has-heading-one');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('keeps non-descendant-dependent issue on element outside limited context', () => {
|
|
330
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
331
|
+
{ id: 1, element: document.documentElement, issues: [langIssue] },
|
|
332
|
+
]);
|
|
333
|
+
updateElementsWithIssues({
|
|
334
|
+
extendedElementsWithIssues,
|
|
335
|
+
limitedContext: narrowContext,
|
|
336
|
+
limitedContextViolations: [],
|
|
337
|
+
fullContextViolations: [],
|
|
338
|
+
name: 'accented',
|
|
339
|
+
});
|
|
340
|
+
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
341
|
+
assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
|
|
342
|
+
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
343
|
+
assert.equal(extendedElementsWithIssues.value[0]?.issues.value[0]?.id, 'html-has-lang');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('adds a new element reported only by full context violations', () => {
|
|
347
|
+
const extendedElementsWithIssues = createElementsWithIssues([]);
|
|
348
|
+
updateElementsWithIssues({
|
|
349
|
+
extendedElementsWithIssues,
|
|
350
|
+
limitedContext: narrowContext,
|
|
351
|
+
limitedContextViolations: [],
|
|
352
|
+
fullContextViolations: [headingViolation],
|
|
353
|
+
name: 'accented',
|
|
354
|
+
});
|
|
355
|
+
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
356
|
+
assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
|
|
357
|
+
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
358
|
+
assert.equal(extendedElementsWithIssues.value[0]?.issues.value[0]?.id, 'page-has-heading-one');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('adds a new element with merged issues from limited and full context violations', () => {
|
|
362
|
+
const extendedElementsWithIssues = createElementsWithIssues([]);
|
|
363
|
+
updateElementsWithIssues({
|
|
364
|
+
extendedElementsWithIssues,
|
|
365
|
+
limitedContext: fullDocumentContext,
|
|
366
|
+
limitedContextViolations: [langViolation],
|
|
367
|
+
fullContextViolations: [headingViolation],
|
|
368
|
+
name: 'accented',
|
|
369
|
+
});
|
|
370
|
+
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
371
|
+
assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
|
|
372
|
+
assert.deepEqual(
|
|
373
|
+
extendedElementsWithIssues.value[0]?.issues.value.map((issue) => issue.id),
|
|
374
|
+
['html-has-lang', 'page-has-heading-one'],
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('merges limited and full context violations onto an existing element', () => {
|
|
379
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
380
|
+
{ id: 1, element: document.documentElement, issues: [langIssue] },
|
|
381
|
+
]);
|
|
382
|
+
updateElementsWithIssues({
|
|
383
|
+
extendedElementsWithIssues,
|
|
384
|
+
limitedContext: fullDocumentContext,
|
|
385
|
+
limitedContextViolations: [langViolation],
|
|
386
|
+
fullContextViolations: [headingViolation],
|
|
387
|
+
name: 'accented',
|
|
388
|
+
});
|
|
389
|
+
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
390
|
+
assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
|
|
391
|
+
assert.deepEqual(
|
|
392
|
+
extendedElementsWithIssues.value[0]?.issues.value.map((issue) => issue.id),
|
|
393
|
+
['html-has-lang', 'page-has-heading-one'],
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('strips only descendant-dependent issues from an element outside limited context with mixed issues', () => {
|
|
398
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
399
|
+
{ id: 1, element: document.documentElement, issues: [langIssue, headingIssue] },
|
|
400
|
+
]);
|
|
401
|
+
updateElementsWithIssues({
|
|
402
|
+
extendedElementsWithIssues,
|
|
403
|
+
limitedContext: narrowContext,
|
|
404
|
+
limitedContextViolations: [],
|
|
405
|
+
fullContextViolations: [],
|
|
406
|
+
name: 'accented',
|
|
407
|
+
});
|
|
408
|
+
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
409
|
+
assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
|
|
410
|
+
assert.deepEqual(
|
|
411
|
+
extendedElementsWithIssues.value[0]?.issues.value.map((issue) => issue.id),
|
|
412
|
+
['html-has-lang'],
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('keeps existing issue on element outside limited context and adds a new full context issue', () => {
|
|
417
|
+
const extendedElementsWithIssues = createElementsWithIssues([
|
|
418
|
+
{ id: 1, element: document.documentElement, issues: [langIssue] },
|
|
419
|
+
]);
|
|
420
|
+
updateElementsWithIssues({
|
|
421
|
+
extendedElementsWithIssues,
|
|
422
|
+
limitedContext: narrowContext,
|
|
423
|
+
limitedContextViolations: [],
|
|
424
|
+
fullContextViolations: [headingViolation],
|
|
425
|
+
name: 'accented',
|
|
426
|
+
});
|
|
427
|
+
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
428
|
+
assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
|
|
429
|
+
assert.deepEqual(
|
|
430
|
+
extendedElementsWithIssues.value[0]?.issues.value.map((issue) => issue.id),
|
|
431
|
+
['html-has-lang', 'page-has-heading-one'],
|
|
432
|
+
);
|
|
433
|
+
});
|
|
350
434
|
});
|