testaro 18.1.0 → 18.2.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.
- package/aslint/LICENSE +362 -0
- package/aslint/README.md +260 -0
- package/aslint/app/rules/abstract-rule.ts +83 -0
- package/aslint/app/rules/aslint/aria-hidden-false/aria-hidden-false.test.ts +73 -0
- package/aslint/app/rules/aslint/aria-hidden-false/aria-hidden-false.ts +34 -0
- package/aslint/app/rules/aslint/aria-hidden-false/aria-hidden.documentation.md +32 -0
- package/aslint/app/rules/aslint/aria-role-dialog/aria-role-dialog.documentation.md +49 -0
- package/aslint/app/rules/aslint/aria-role-dialog/aria-role-dialog.test.ts +91 -0
- package/aslint/app/rules/aslint/aria-role-dialog/aria-role-dialog.ts +62 -0
- package/aslint/app/rules/aslint/capital-letters-words/capital-letters-words.documentation.md +44 -0
- package/aslint/app/rules/aslint/capital-letters-words/capital-letters-words.test.ts +111 -0
- package/aslint/app/rules/aslint/capital-letters-words/capital-letters-words.ts +120 -0
- package/aslint/app/rules/aslint/content-editable-missing-attributes/content-editable-missing-attributes.docmentation.md +48 -0
- package/aslint/app/rules/aslint/content-editable-missing-attributes/content-editable-missing-attributes.test.ts +67 -0
- package/aslint/app/rules/aslint/content-editable-missing-attributes/content-editable-missing-attributes.ts +63 -0
- package/aslint/app/rules/aslint/contentinfo-landmark-only-one/contentinfo-landmark-only-one.documentation.md +45 -0
- package/aslint/app/rules/aslint/contentinfo-landmark-only-one/contentinfo-landmark-only-one.test.ts +63 -0
- package/aslint/app/rules/aslint/contentinfo-landmark-only-one/contentinfo-landmark-only-one.ts +44 -0
- package/aslint/app/rules/aslint/elements-not-allowed-in-head/elements-not-allowed-in-head.documentation.md +65 -0
- package/aslint/app/rules/aslint/elements-not-allowed-in-head/elements-not-allowed-in-head.test.ts +53 -0
- package/aslint/app/rules/aslint/elements-not-allowed-in-head/elements-not-allowed-in-head.ts +47 -0
- package/aslint/app/rules/aslint/empty-title-attribute/empty-title-attribute.documentation.md +55 -0
- package/aslint/app/rules/aslint/empty-title-attribute/empty-title-attribute.test.ts +80 -0
- package/aslint/app/rules/aslint/empty-title-attribute/empty-title-attribute.ts +58 -0
- package/aslint/app/rules/aslint/flash-content/flash-content.documentation.md +48 -0
- package/aslint/app/rules/aslint/flash-content/flash-content.test.ts +52 -0
- package/aslint/app/rules/aslint/flash-content/flash-content.ts +32 -0
- package/aslint/app/rules/aslint/font-style-italic/font-style-italic.documentation.md +44 -0
- package/aslint/app/rules/aslint/font-style-italic/font-style-italic.test.ts +12 -0
- package/aslint/app/rules/aslint/font-style-italic/font-style-italic.ts +83 -0
- package/aslint/app/rules/aslint/h1-must-be/h1-must-be.documentation.md +46 -0
- package/aslint/app/rules/aslint/h1-must-be/h1-must-be.test.ts +46 -0
- package/aslint/app/rules/aslint/h1-must-be/h1-must-be.ts +36 -0
- package/aslint/app/rules/aslint/headings-sibling-unique/headings-sibling-unique.documentation.md +57 -0
- package/aslint/app/rules/aslint/headings-sibling-unique/headings-sibling-unique.test.ts +52 -0
- package/aslint/app/rules/aslint/headings-sibling-unique/headings-sibling-unique.ts +63 -0
- package/aslint/app/rules/aslint/horizontal-rule/horizontal-rule.documentation.md +39 -0
- package/aslint/app/rules/aslint/horizontal-rule/horizontal-rule.test.ts +66 -0
- package/aslint/app/rules/aslint/horizontal-rule/horizontal-rule.ts +37 -0
- package/aslint/app/rules/aslint/incorrect-technique-for-hiding-content/incorrect-technique-for-hiding-content.documentation.md +36 -0
- package/aslint/app/rules/aslint/incorrect-technique-for-hiding-content/incorrect-technique-for-hiding-content.test.ts +113 -0
- package/aslint/app/rules/aslint/incorrect-technique-for-hiding-content/incorrect-technique-for-hiding-content.ts +103 -0
- package/aslint/app/rules/aslint/invalid-attribute-dir-value/invalid-attribute-dir-value.documentation.md +34 -0
- package/aslint/app/rules/aslint/invalid-attribute-dir-value/invalid-attribute-dir-value.test.ts +82 -0
- package/aslint/app/rules/aslint/invalid-attribute-dir-value/invalid-attribute-dir-value.ts +44 -0
- package/aslint/app/rules/aslint/label-duplicated-content-title/label-duplicated-content-title.documentation.md +40 -0
- package/aslint/app/rules/aslint/label-duplicated-content-title/label-duplicated-content-title.test.ts +48 -0
- package/aslint/app/rules/aslint/label-duplicated-content-title/label-duplicated-content-title.ts +37 -0
- package/aslint/app/rules/aslint/links-language-destination/links-language-destination.test.ts +50 -0
- package/aslint/app/rules/aslint/links-language-destination/links-language-destination.ts +70 -0
- package/aslint/app/rules/aslint/main-element-only-one/main-element-only-one.test.ts +55 -0
- package/aslint/app/rules/aslint/main-element-only-one/main-element-only-one.ts +83 -0
- package/aslint/app/rules/aslint/main-landmark-must-be-top-level/main-landmark-must-be-top-level.test.ts +12 -0
- package/aslint/app/rules/aslint/main-landmark-must-be-top-level/main-landmark-must-be-top-level.ts +73 -0
- package/aslint/app/rules/aslint/minimum-font-size/minimum-font-size.test.ts +12 -0
- package/aslint/app/rules/aslint/minimum-font-size/minimum-font-size.ts +87 -0
- package/aslint/app/rules/aslint/missing-href-on-a/missing-href-on-a.test.ts +48 -0
- package/aslint/app/rules/aslint/missing-href-on-a/missing-href-on-a.ts +40 -0
- package/aslint/app/rules/aslint/misused-aria-on-focusable-element/misused-aria-on-focusable-element.test.ts +12 -0
- package/aslint/app/rules/aslint/misused-aria-on-focusable-element/misused-aria-on-focusable-element.ts +66 -0
- package/aslint/app/rules/aslint/misused-input-attribute/misused-input-attribute.test.ts +12 -0
- package/aslint/app/rules/aslint/misused-input-attribute/misused-input-attribute.ts +134 -0
- package/aslint/app/rules/aslint/misused-required-attribute/misused-required-attribute.test.ts +12 -0
- package/aslint/app/rules/aslint/misused-required-attribute/misused-required-attribute.ts +90 -0
- package/aslint/app/rules/aslint/navigation-landmark-restrictions/navigation-landmark-restrictions.test.ts +12 -0
- package/aslint/app/rules/aslint/navigation-landmark-restrictions/navigation-landmark-restrictions.ts +48 -0
- package/aslint/app/rules/aslint/obsolete-html-attributes/obsolete-html-attributes.test.ts +12 -0
- package/aslint/app/rules/aslint/obsolete-html-attributes/obsolete-html-attributes.ts +148 -0
- package/aslint/app/rules/aslint/obsolete-html-elements/obsolete-html-elements.test.ts +12 -0
- package/aslint/app/rules/aslint/obsolete-html-elements/obsolete-html-elements.ts +66 -0
- package/aslint/app/rules/aslint/outline-zero/outline-zero.test.ts +12 -0
- package/aslint/app/rules/aslint/outline-zero/outline-zero.ts +85 -0
- package/aslint/app/rules/aslint/overlay/overlay.test.ts +122 -0
- package/aslint/app/rules/aslint/overlay/overlay.ts +141 -0
- package/aslint/app/rules/aslint/role-application/role-application.test.ts +48 -0
- package/aslint/app/rules/aslint/role-application/role-application.ts +38 -0
- package/aslint/app/rules/aslint/rtl-content/rtl-content.test.ts +12 -0
- package/aslint/app/rules/aslint/rtl-content/rtl-content.ts +75 -0
- package/aslint/app/rules/aslint/unclear-uri-on-a/unclear-anchor-uri.test.ts +12 -0
- package/aslint/app/rules/aslint/unclear-uri-on-a/unclear-anchor-uri.ts +48 -0
- package/aslint/app/rules/aslint/unsupported-role-on-element/unsupported-role-on-element.test.ts +12 -0
- package/aslint/app/rules/aslint/unsupported-role-on-element/unsupported-role-on-element.ts +63 -0
- package/package.json +2 -2
- package/testaro/headingAmb.js +103 -0
- package/testaro/template.js +78 -0
- package/tests/testaro.js +1 -0
- package/validation/tests/jobs/headingAmb.json +129 -0
- package/validation/tests/targets/headingAmb/index.html +26 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { DomUtility } from '../../../utils/dom';
|
|
2
|
+
import { Css } from '../../../utils/css';
|
|
3
|
+
import { TextUtility } from '../../../utils/text';
|
|
4
|
+
import { CATEGORY_TYPE } from '../../../constants/categoryType';
|
|
5
|
+
import { IIssueReport } from '../../../interfaces/rule-issue.interface';
|
|
6
|
+
import { TranslateService } from '../../../services/translate';
|
|
7
|
+
import { $severity } from '../../../constants/accessibility';
|
|
8
|
+
import { $accessibilityAuditRules } from '../../../constants/accessibility';
|
|
9
|
+
import { AbstractRule, IAbstractRuleConfig } from '../../abstract-rule';
|
|
10
|
+
|
|
11
|
+
export class CapitalLettersWords extends AbstractRule {
|
|
12
|
+
protected selector: string = `*${[
|
|
13
|
+
':root',
|
|
14
|
+
'head',
|
|
15
|
+
'style',
|
|
16
|
+
'script',
|
|
17
|
+
'noscript',
|
|
18
|
+
'meta',
|
|
19
|
+
'link',
|
|
20
|
+
'br',
|
|
21
|
+
'hr',
|
|
22
|
+
'object',
|
|
23
|
+
'svg',
|
|
24
|
+
'path',
|
|
25
|
+
'defs',
|
|
26
|
+
'rect',
|
|
27
|
+
'clippath',
|
|
28
|
+
'use',
|
|
29
|
+
'g',
|
|
30
|
+
'b',
|
|
31
|
+
'filter',
|
|
32
|
+
'img',
|
|
33
|
+
'picture',
|
|
34
|
+
'input',
|
|
35
|
+
'iframe',
|
|
36
|
+
'code',
|
|
37
|
+
'metadata',
|
|
38
|
+
':empty'
|
|
39
|
+
].map((i: string): string => {
|
|
40
|
+
return `:not(${i})`;
|
|
41
|
+
}).join('')}`;
|
|
42
|
+
|
|
43
|
+
protected ruleConfig: IAbstractRuleConfig = {
|
|
44
|
+
id: TextUtility.convertUnderscoresToDashes($accessibilityAuditRules.capital_letters_words),
|
|
45
|
+
links: [
|
|
46
|
+
{
|
|
47
|
+
content: 'Dyslexia Font and Style Guide',
|
|
48
|
+
url: 'https://www.dyslexia-reading-well.com/dyslexia-font.html'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
content: 'Typefaces for dyslexia',
|
|
52
|
+
url: 'https://bdatech.org/what-technology/typefaces-for-dyslexia/'
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
recommendations: [],
|
|
56
|
+
severity: $severity.high,
|
|
57
|
+
type: CATEGORY_TYPE.BEST_PRACTICE
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
public validate(nodes: HTMLElement[]): void {
|
|
61
|
+
const onlyWordsWithUpperCases = (word: string): boolean => {
|
|
62
|
+
return TextUtility.isUpperCase(word);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const processNode = (element: HTMLElement): void => {
|
|
66
|
+
const report: IIssueReport = {
|
|
67
|
+
message: '',
|
|
68
|
+
node: element,
|
|
69
|
+
ruleId: this.ruleConfig.id
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let reportMessage: string = '';
|
|
73
|
+
|
|
74
|
+
let words: string[];
|
|
75
|
+
let onlyWordsWithUpperCase: string[] = [];
|
|
76
|
+
|
|
77
|
+
const text: string = DomUtility.getTextFromDescendantContent(element).trim();
|
|
78
|
+
|
|
79
|
+
words = text.split(' ');
|
|
80
|
+
|
|
81
|
+
if (text.length > 0 && words.length > 0) {
|
|
82
|
+
if (Css.isCssTextTransformUsed(element)) {
|
|
83
|
+
reportMessage = TranslateService.instant('capital_letters_words_report_message1', [text.toUpperCase()]);
|
|
84
|
+
} else {
|
|
85
|
+
|
|
86
|
+
onlyWordsWithUpperCase = words.filter(onlyWordsWithUpperCases);
|
|
87
|
+
|
|
88
|
+
if (onlyWordsWithUpperCase.length > 1) {
|
|
89
|
+
reportMessage = TranslateService.instant('capital_letters_words_report_message2');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const titleAttribute: string | null = element.getAttribute('title');
|
|
95
|
+
|
|
96
|
+
if (typeof titleAttribute === 'string' && titleAttribute.trim().length > 0) {
|
|
97
|
+
words = titleAttribute.split(' ');
|
|
98
|
+
onlyWordsWithUpperCase = words.filter(onlyWordsWithUpperCases);
|
|
99
|
+
|
|
100
|
+
if (onlyWordsWithUpperCase.length > 1) {
|
|
101
|
+
if (reportMessage.length === 0) {
|
|
102
|
+
reportMessage = TranslateService.instant('capital_letters_words_report_message3', [titleAttribute]);
|
|
103
|
+
} else {
|
|
104
|
+
reportMessage = TranslateService.instant('capital_letters_words_report_message4', [titleAttribute]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (reportMessage.length === 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
report.message = `${reportMessage} ${TranslateService.instant('capital_letters_words_report_explanation')}`;
|
|
114
|
+
|
|
115
|
+
this.validator.report(report);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
nodes.forEach(processNode);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# content-editable-missing-attributes
|
|
2
|
+
|
|
3
|
+
## Rule id
|
|
4
|
+
|
|
5
|
+
`content-editable-missing-attributes`
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
This rule verifies if element with attribute `contenteditable` have defined also following attributes:
|
|
10
|
+
|
|
11
|
+
1. `aria-multiline`
|
|
12
|
+
2. `aria-labelledby` or `aria-label`
|
|
13
|
+
|
|
14
|
+
## Purpose
|
|
15
|
+
|
|
16
|
+
When `aria-multiline="true"` is set then Assistive Technologies informs the user that the textbox supports multi-line input, with the expectation that <kbd>Enter</kbd> or <kbd>Return</kbd> will create a line break rather than submitting the form.
|
|
17
|
+
|
|
18
|
+
`aria-label` here is recommended to specify a string to be used as the accessible label.
|
|
19
|
+
|
|
20
|
+
Eventually `aria-labelledby` can be used to specify the `id` of another element in the DOM as an element's label.
|
|
21
|
+
|
|
22
|
+
## Test cases
|
|
23
|
+
|
|
24
|
+
### Passed
|
|
25
|
+
|
|
26
|
+
The rule passes when all of the following cases are fulfilled:
|
|
27
|
+
|
|
28
|
+
1. Attribute `contenteditable` is defined.
|
|
29
|
+
2. `aria-label` or `aria-labelledby` is defined. The rule does not validate `aria-labelledby` ids if the referenced elements exists.
|
|
30
|
+
|
|
31
|
+
## WCAG Success Criteria
|
|
32
|
+
|
|
33
|
+
Not Applicable
|
|
34
|
+
|
|
35
|
+
## Best Practice
|
|
36
|
+
|
|
37
|
+
Yes
|
|
38
|
+
|
|
39
|
+
## User Impact
|
|
40
|
+
|
|
41
|
+
* **Severity**: high
|
|
42
|
+
* **Disabilities Affected**:
|
|
43
|
+
* Visual:
|
|
44
|
+
* blindness
|
|
45
|
+
|
|
46
|
+
## Resources
|
|
47
|
+
|
|
48
|
+
Not available
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ContentEditableMissingAttributes } from './content-editable-missing-attributes';
|
|
2
|
+
import { DomUtility } from '../../../utils/dom';
|
|
3
|
+
import { Validator } from '../../../validator';
|
|
4
|
+
|
|
5
|
+
describe('Rules', () => {
|
|
6
|
+
|
|
7
|
+
describe('#content-editable-missing-attributes', () => {
|
|
8
|
+
|
|
9
|
+
let fakeDom;
|
|
10
|
+
|
|
11
|
+
new ContentEditableMissingAttributes().registerValidator();
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
fakeDom = document.createElement('div');
|
|
15
|
+
fakeDom.id = 'fakedom';
|
|
16
|
+
document.body.appendChild(fakeDom);
|
|
17
|
+
|
|
18
|
+
Validator.reset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
DomUtility.remove(document.getElementById('fakedom'));
|
|
23
|
+
fakeDom = undefined;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return 3 reports when there is an element with attribute [contenteditable] only', () => {
|
|
27
|
+
fakeDom.innerHTML = '<p contenteditable="true"></p>';
|
|
28
|
+
|
|
29
|
+
const nodes = DomUtility.querySelectorAllExclude('[contenteditable]', fakeDom);
|
|
30
|
+
|
|
31
|
+
new ContentEditableMissingAttributes().validate(nodes);
|
|
32
|
+
|
|
33
|
+
expect(Object.keys(Validator.getReports()).length).toBe(3);
|
|
34
|
+
|
|
35
|
+
expect(Validator.getReport('report_0').message).toBe('Missing attribute <code>role=\'textbox\'</code> on <code><p contenteditable="true"></p></code>');
|
|
36
|
+
expect(Validator.getReport('report_1').message).toBe('Missing attribute <code>aria-multiline=\'true\'</code> on <code><p contenteditable="true"></p></code>');
|
|
37
|
+
expect(Validator.getReport('report_2').message).toBe('Missing attribute <code>aria-labelledby</code> or <code>aria-label</code> on <code><p contenteditable="true"></p></code>');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return 2 reports when there is an element with attribute [contenteditable] and defined attribute aria-label', () => {
|
|
41
|
+
fakeDom.innerHTML = '<p contenteditable="true" aria-label="test"></p><span id="test">test</span>';
|
|
42
|
+
|
|
43
|
+
const nodes = DomUtility.querySelectorAllExclude('[contenteditable]', fakeDom);
|
|
44
|
+
|
|
45
|
+
new ContentEditableMissingAttributes().validate(nodes);
|
|
46
|
+
|
|
47
|
+
expect(Object.keys(Validator.getReports()).length).toBe(2);
|
|
48
|
+
|
|
49
|
+
expect(Validator.getReport('report_0').message).toBe('Missing attribute <code>role=\'textbox\'</code> on <code><p contenteditable="true" aria-label="test"></p></code>');
|
|
50
|
+
expect(Validator.getReport('report_1').message).toBe('Missing attribute <code>aria-multiline=\'true\'</code> on <code><p contenteditable="true" aria-label="test"></p></code>');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return 1 report for missing attribute role="textbox" when there is an element with attribute [contenteditable], defined attribute aria-label and aria-multiline', () => {
|
|
54
|
+
fakeDom.innerHTML = '<p contenteditable="true" aria-label="test" aria-multiline="true"></p><span id="test">test</span>';
|
|
55
|
+
|
|
56
|
+
const nodes = DomUtility.querySelectorAllExclude('[contenteditable]', fakeDom);
|
|
57
|
+
|
|
58
|
+
new ContentEditableMissingAttributes().validate(nodes);
|
|
59
|
+
|
|
60
|
+
expect(Object.keys(Validator.getReports()).length).toBe(1);
|
|
61
|
+
|
|
62
|
+
expect(Validator.getReport('report_0').message).toBe('Missing attribute <code>role=\'textbox\'</code> on <code><p contenteditable="true" aria-label="test" aria-multiline="true"></p></code>');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DomUtility } from '../../../utils/dom';
|
|
2
|
+
import { CATEGORY_TYPE } from '../../../constants/categoryType';
|
|
3
|
+
import { IIssueReport } from '../../../interfaces/rule-issue.interface';
|
|
4
|
+
import { TextUtility } from '../../../utils/text';
|
|
5
|
+
import { TranslateService } from '../../../services/translate';
|
|
6
|
+
import { $accessibilityAuditRules, $severity } from '../../../constants/accessibility';
|
|
7
|
+
import { AbstractRule, IAbstractRuleConfig } from '../../abstract-rule';
|
|
8
|
+
|
|
9
|
+
export class ContentEditableMissingAttributes extends AbstractRule {
|
|
10
|
+
protected selector: string = '[contenteditable]';
|
|
11
|
+
protected ruleConfig: IAbstractRuleConfig = {
|
|
12
|
+
id: TextUtility.convertUnderscoresToDashes($accessibilityAuditRules.content_editable_missing_attributes),
|
|
13
|
+
links: [],
|
|
14
|
+
recommendations: [],
|
|
15
|
+
severity: $severity.low,
|
|
16
|
+
type: CATEGORY_TYPE.BEST_PRACTICE
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
public validate(elements: Element[]): void {
|
|
20
|
+
const reportMissingAttributes = (element: Element): void => {
|
|
21
|
+
let problem: IIssueReport;
|
|
22
|
+
const roleValue: string | null = element.getAttribute('role');
|
|
23
|
+
|
|
24
|
+
if (roleValue !== 'textbox') {
|
|
25
|
+
const reportMessage: string = TranslateService.instant('content_editable_missing_attributes_report_message1', [DomUtility.getEscapedOuterHTML(element)]);
|
|
26
|
+
|
|
27
|
+
problem = {
|
|
28
|
+
message: reportMessage,
|
|
29
|
+
node: element,
|
|
30
|
+
ruleId: this.ruleConfig.id
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this.validator.report(problem);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (element.getAttribute('aria-multiline') === null) {
|
|
37
|
+
const reportMessage: string = TranslateService.instant('content_editable_missing_attributes_report_message2', [DomUtility.getEscapedOuterHTML(element)]);
|
|
38
|
+
|
|
39
|
+
problem = {
|
|
40
|
+
message: reportMessage,
|
|
41
|
+
node: element,
|
|
42
|
+
ruleId: this.ruleConfig.id
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.validator.report(problem);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (element.getAttribute('aria-labelledby') === null && element.getAttribute('aria-label') === null) {
|
|
49
|
+
const reportMessage: string = TranslateService.instant('content_editable_missing_attributes_report_message3', [DomUtility.getEscapedOuterHTML(element)]);
|
|
50
|
+
|
|
51
|
+
problem = {
|
|
52
|
+
message: reportMessage,
|
|
53
|
+
node: element,
|
|
54
|
+
ruleId: this.ruleConfig.id
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.validator.report(problem);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
elements.forEach(reportMissingAttributes);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# contentinfo-landmark-only-one
|
|
2
|
+
|
|
3
|
+
## Rule id
|
|
4
|
+
|
|
5
|
+
`contentinfo-landmark-only-one`
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
This rule verifies if there is no more than 1 element with an attribute `role="contentinfo"`.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
Based on https://www.w3.org/TR/wai-aria/#contentinfo
|
|
14
|
+
|
|
15
|
+
> Within any document or application, the author SHOULD mark no more than one element with the contentinfo role.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Test cases
|
|
19
|
+
|
|
20
|
+
### Passed
|
|
21
|
+
|
|
22
|
+
The rule passes when all of the following cases are fulfilled:
|
|
23
|
+
|
|
24
|
+
1. There is no more than 1 element with an attribute `role="contentinfo"`
|
|
25
|
+
|
|
26
|
+
## WCAG Success Criteria
|
|
27
|
+
|
|
28
|
+
Not Applicable
|
|
29
|
+
|
|
30
|
+
## Best Practice
|
|
31
|
+
|
|
32
|
+
Yes
|
|
33
|
+
|
|
34
|
+
## User Impact
|
|
35
|
+
|
|
36
|
+
* **Severity**: low
|
|
37
|
+
* **Disabilities Affected**:
|
|
38
|
+
* Visual:
|
|
39
|
+
* blindness
|
|
40
|
+
|
|
41
|
+
## Resources
|
|
42
|
+
|
|
43
|
+
* https://www.w3.org/TR/wai-aria/#contentinfo
|
|
44
|
+
* http://www.w3.org/TR/WCAG20-TECHS/ARIA11
|
|
45
|
+
|
package/aslint/app/rules/aslint/contentinfo-landmark-only-one/contentinfo-landmark-only-one.test.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ContentinfoLandmarkOnlyOne } from './contentinfo-landmark-only-one';
|
|
2
|
+
import { DomUtility } from '../../../utils/dom';
|
|
3
|
+
import { Validator } from '../../../validator';
|
|
4
|
+
|
|
5
|
+
describe('Rules', () => {
|
|
6
|
+
|
|
7
|
+
describe('ContentinfoLandmarkOnlyOne', () => {
|
|
8
|
+
|
|
9
|
+
let fakeDom;
|
|
10
|
+
|
|
11
|
+
new ContentinfoLandmarkOnlyOne().registerValidator();
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
fakeDom = document.createElement('div');
|
|
15
|
+
fakeDom.id = 'fakedom';
|
|
16
|
+
document.body.appendChild(fakeDom);
|
|
17
|
+
|
|
18
|
+
Validator.reset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
DomUtility.remove(document.getElementById('fakedom'));
|
|
23
|
+
fakeDom = undefined;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return one report when there is an element with more than one attribute role="contentinfo"', () => {
|
|
27
|
+
fakeDom.innerHTML = '<div role="contentinfo"></div><div role="contentinfo"></div>';
|
|
28
|
+
|
|
29
|
+
const nodes = DomUtility.querySelectorAllExclude('[role=contentinfo]', fakeDom);
|
|
30
|
+
|
|
31
|
+
new ContentinfoLandmarkOnlyOne().validate(nodes);
|
|
32
|
+
|
|
33
|
+
expect(Object.keys(Validator.getReports()).length).toBe(2);
|
|
34
|
+
expect(Validator.getReport('report_0').message).toBe('Expected attribute <code>role="contentinfo"</code> to be defined only once. You have 2 .');
|
|
35
|
+
expect(Validator.getReport('report_0').node.nodeName.toLowerCase()).toBe('div');
|
|
36
|
+
expect(Validator.getReport('report_0').ruleId).toBe('contentinfo-landmark-only-one');
|
|
37
|
+
expect(Validator.getReport('report_1').message).toBe('Expected attribute <code>role="contentinfo"</code> to be defined only once. You have 2 .');
|
|
38
|
+
expect(Validator.getReport('report_1').node.nodeName.toLowerCase()).toBe('div');
|
|
39
|
+
expect(Validator.getReport('report_1').ruleId).toBe('contentinfo-landmark-only-one');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return no reports when there is 1 element with attribute role="contentinfo"', () => {
|
|
43
|
+
fakeDom.innerHTML = '<div role="contentinfo">';
|
|
44
|
+
|
|
45
|
+
const nodes = DomUtility.querySelectorAllExclude('[role=contentinfo]', fakeDom);
|
|
46
|
+
|
|
47
|
+
new ContentinfoLandmarkOnlyOne().validate(nodes);
|
|
48
|
+
|
|
49
|
+
expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return no reports when there is no elements with attribute role="contentinfo"', () => {
|
|
53
|
+
fakeDom.innerHTML = '<div>contentinfo</div>';
|
|
54
|
+
const nodes = DomUtility.querySelectorAllExclude('[role=contentinfo]', fakeDom);
|
|
55
|
+
|
|
56
|
+
new ContentinfoLandmarkOnlyOne().validate(nodes);
|
|
57
|
+
|
|
58
|
+
expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
});
|
package/aslint/app/rules/aslint/contentinfo-landmark-only-one/contentinfo-landmark-only-one.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TextUtility } from '../../../utils/text';
|
|
2
|
+
import { CATEGORY_TYPE } from '../../../constants/categoryType';
|
|
3
|
+
import { IIssueReport } from '../../../interfaces/rule-issue.interface';
|
|
4
|
+
import { $accessibilityAuditRules, $severity } from '../../../constants/accessibility';
|
|
5
|
+
import { TranslateService } from '../../../services/translate';
|
|
6
|
+
import { AbstractRule, IAbstractRuleConfig } from '../../abstract-rule';
|
|
7
|
+
|
|
8
|
+
export class ContentinfoLandmarkOnlyOne extends AbstractRule {
|
|
9
|
+
protected selector: string = '[role="contentinfo"]';
|
|
10
|
+
protected ruleConfig: IAbstractRuleConfig = {
|
|
11
|
+
id: TextUtility.convertUnderscoresToDashes($accessibilityAuditRules.contentinfo_landmark_only_one),
|
|
12
|
+
links: [
|
|
13
|
+
{
|
|
14
|
+
content: 'ARIA11: Using ARIA landmarks to identify regions of a page',
|
|
15
|
+
url: 'http://www.w3.org/TR/WCAG20-TECHS/ARIA11'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
content: 'Accessible Rich Internet Applications (WAI-ARIA) 1.0 Specification: contentinfo role',
|
|
19
|
+
url: 'https://www.w3.org/TR/wai-aria/#contentinfo'
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
recommendations: [],
|
|
23
|
+
severity: $severity.low,
|
|
24
|
+
type: CATEGORY_TYPE.BEST_PRACTICE
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
public validate(nodes: Element[]): void {
|
|
28
|
+
const total: number = nodes.length;
|
|
29
|
+
|
|
30
|
+
if (total > 1) {
|
|
31
|
+
nodes.forEach((element: Element): void => {
|
|
32
|
+
const reportMessage: string = TranslateService.instant('contentinfo_landmark_only_one_report_message', [TextUtility.escape('role="contentinfo"'), total, '.']);
|
|
33
|
+
|
|
34
|
+
const report: IIssueReport = {
|
|
35
|
+
message: reportMessage,
|
|
36
|
+
node: element,
|
|
37
|
+
ruleId: this.ruleConfig.id
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this.validator.report(report);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# elements-not-allowed-in-head
|
|
2
|
+
|
|
3
|
+
## Rule id
|
|
4
|
+
|
|
5
|
+
`elements-not-allowed-in-head`
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
This rule verifies if there are elements that shouldn't be in the `<head>` section.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
`<head>` section contains elements that provides information of how the document should be perceived, and rendered, by web technologies. e.g. browsers, search engines, bots, screen readers, etc.
|
|
14
|
+
|
|
15
|
+
Non valid pages rely on the browser to auto-correct your code, and each browser does this differently. If your code is valid the browser has to do less processing as it doesn't have to correct any code, therefore the page will render faster and more predictable.
|
|
16
|
+
|
|
17
|
+
In order to make the code accurate processed and rendered it is recommended to have only valid HTML elements / tags in the `<head>` section.
|
|
18
|
+
|
|
19
|
+
Following elements are allowed to be in the `<head>` section:
|
|
20
|
+
|
|
21
|
+
<title>
|
|
22
|
+
<base>
|
|
23
|
+
<link>
|
|
24
|
+
<style>
|
|
25
|
+
<meta>
|
|
26
|
+
<script>
|
|
27
|
+
<noscript>
|
|
28
|
+
<template>
|
|
29
|
+
|
|
30
|
+
All other elements / tags are perceived as non-valid.
|
|
31
|
+
|
|
32
|
+
## Test cases
|
|
33
|
+
|
|
34
|
+
### Passed
|
|
35
|
+
|
|
36
|
+
The rule passes when only following elements / tags are specified in the `<head>` section:
|
|
37
|
+
|
|
38
|
+
<title>
|
|
39
|
+
<base>
|
|
40
|
+
<link>
|
|
41
|
+
<style>
|
|
42
|
+
<meta>
|
|
43
|
+
<script>
|
|
44
|
+
<noscript>
|
|
45
|
+
<template>
|
|
46
|
+
|
|
47
|
+
## WCAG Success Criteria
|
|
48
|
+
|
|
49
|
+
Not Applicable
|
|
50
|
+
|
|
51
|
+
## Best Practice
|
|
52
|
+
|
|
53
|
+
Yes
|
|
54
|
+
|
|
55
|
+
## User Impact
|
|
56
|
+
|
|
57
|
+
* **Severity**: low
|
|
58
|
+
* **Disabilities Affected**:
|
|
59
|
+
* Visual:
|
|
60
|
+
* blindness
|
|
61
|
+
|
|
62
|
+
## Resources
|
|
63
|
+
|
|
64
|
+
* https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/The_head_metadata_in_HTML
|
|
65
|
+
* https://html.spec.whatwg.org/multipage/semantics.html#the-head-element
|
package/aslint/app/rules/aslint/elements-not-allowed-in-head/elements-not-allowed-in-head.test.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ElementsNotAllowed } from './elements-not-allowed-in-head';
|
|
2
|
+
import { DomUtility } from '../../../utils/dom';
|
|
3
|
+
import { Validator } from '../../../validator';
|
|
4
|
+
|
|
5
|
+
describe('Rules', () => {
|
|
6
|
+
|
|
7
|
+
describe('ElementsNotAllowed#', () => {
|
|
8
|
+
|
|
9
|
+
let fakeDom;
|
|
10
|
+
const VALID_ELEMENTS: string[] = [':not(base)', ':not(link)', ':not(meta)', ':not(script)', ':not(style)', ':not(title)', ':not(noscript)', ':not(template)'];
|
|
11
|
+
const selector = `head *${VALID_ELEMENTS.join('')}`;
|
|
12
|
+
|
|
13
|
+
new ElementsNotAllowed().registerValidator();
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
Validator.reset();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
DomUtility.remove(document.getElementById('headfakedom'));
|
|
21
|
+
fakeDom = undefined;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return one report when there is an element not supposed to be in head block', () => {
|
|
25
|
+
fakeDom = document.createElement('div');
|
|
26
|
+
fakeDom.id = 'headfakedom';
|
|
27
|
+
document.head.appendChild(fakeDom);
|
|
28
|
+
|
|
29
|
+
const nodes = DomUtility.querySelectorAllExclude(selector);
|
|
30
|
+
|
|
31
|
+
new ElementsNotAllowed().validate(nodes);
|
|
32
|
+
|
|
33
|
+
expect(Object.keys(Validator.getReports()).length).toBe(1);
|
|
34
|
+
expect(Validator.getReport('report_0').message).toBe('Expected <code><div id="headfakedom"></div></code> not to be a child of <code><head></code>.');
|
|
35
|
+
expect(Validator.getReport('report_0').node.nodeName.toLowerCase()).toBe('div');
|
|
36
|
+
expect(Validator.getReport('report_0').ruleId).toBe('elements-not-allowed-in-head');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return no reports when there is no invalid elements in head section', () => {
|
|
40
|
+
fakeDom = document.createElement('noscript');
|
|
41
|
+
fakeDom.id = 'headfakedom';
|
|
42
|
+
document.head.appendChild(fakeDom);
|
|
43
|
+
|
|
44
|
+
const nodes = DomUtility.querySelectorAllExclude(selector);
|
|
45
|
+
|
|
46
|
+
new ElementsNotAllowed().validate(nodes);
|
|
47
|
+
|
|
48
|
+
expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DomUtility } from '../../../utils/dom';
|
|
2
|
+
import { TextUtility } from '../../../utils/text';
|
|
3
|
+
import { CATEGORY_TYPE } from '../../../constants/categoryType';
|
|
4
|
+
import { IIssueReport } from '../../../interfaces/rule-issue.interface';
|
|
5
|
+
import { TranslateService } from '../../../services/translate';
|
|
6
|
+
import { $accessibilityAuditRules, $severity } from '../../../constants/accessibility';
|
|
7
|
+
import { AbstractRule, IAbstractRuleConfig } from '../../abstract-rule';
|
|
8
|
+
|
|
9
|
+
export class ElementsNotAllowed extends AbstractRule {
|
|
10
|
+
protected selector: string = `head *${[
|
|
11
|
+
':not(base)',
|
|
12
|
+
':not(link)',
|
|
13
|
+
':not(meta)',
|
|
14
|
+
':not(script)',
|
|
15
|
+
':not(style)',
|
|
16
|
+
':not(title)',
|
|
17
|
+
':not(noscript)',
|
|
18
|
+
':not(template)'
|
|
19
|
+
].join('')}`;
|
|
20
|
+
|
|
21
|
+
protected ruleConfig: IAbstractRuleConfig = {
|
|
22
|
+
id: TextUtility.convertUnderscoresToDashes($accessibilityAuditRules.elements_not_allowed_in_head),
|
|
23
|
+
links: [],
|
|
24
|
+
recommendations: [],
|
|
25
|
+
severity: $severity.low,
|
|
26
|
+
type: CATEGORY_TYPE.BEST_PRACTICE
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
public validate(elements: Element[]): void {
|
|
30
|
+
const reportNode = (element: Element): void => {
|
|
31
|
+
const reportMessage: string = TranslateService.instant('elements_not_allowed_in_head_report_message', [
|
|
32
|
+
DomUtility.getEscapedOuterHTML(element),
|
|
33
|
+
TextUtility.escape('<head>')
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const report: IIssueReport = {
|
|
37
|
+
message: reportMessage,
|
|
38
|
+
node: element,
|
|
39
|
+
ruleId: this.ruleConfig.id
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.validator.report(report);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
elements.forEach(reportNode);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# empty-title-attribute
|
|
2
|
+
|
|
3
|
+
## Rule id
|
|
4
|
+
|
|
5
|
+
`empty-title-attribute`
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
This rule verifies if there are elements that have an attribute `title` with an empty value. This includes also whitespaces.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
`title` attribute should not be empty. This will ensure that the value of `title` will be processed in a predictable way. As a side effect removing an empty value of `title` attribute will make the code smaller, which means less data to transfer and faster processing by e.g. the browser.
|
|
14
|
+
|
|
15
|
+
Following elements are being skip while evaluating this rule:
|
|
16
|
+
|
|
17
|
+
<img/>,
|
|
18
|
+
<html>,
|
|
19
|
+
<head>,
|
|
20
|
+
<title>,
|
|
21
|
+
<body>,
|
|
22
|
+
<link/>,
|
|
23
|
+
<meta>,
|
|
24
|
+
<title>,
|
|
25
|
+
<style>,
|
|
26
|
+
<script>,
|
|
27
|
+
<noscript>,
|
|
28
|
+
<iframe>,
|
|
29
|
+
<br/>,
|
|
30
|
+
<hr/>
|
|
31
|
+
|
|
32
|
+
## Test cases
|
|
33
|
+
|
|
34
|
+
### Passed
|
|
35
|
+
|
|
36
|
+
The rule passes when element have an attribute `title` with non-empty value. This includes also whitespaces.
|
|
37
|
+
|
|
38
|
+
## WCAG Success Criteria
|
|
39
|
+
|
|
40
|
+
Not Applicable
|
|
41
|
+
|
|
42
|
+
## Best Practice
|
|
43
|
+
|
|
44
|
+
Yes
|
|
45
|
+
|
|
46
|
+
## User Impact
|
|
47
|
+
|
|
48
|
+
* **Severity**: low
|
|
49
|
+
* **Disabilities Affected**:
|
|
50
|
+
* Visual:
|
|
51
|
+
* blindness
|
|
52
|
+
|
|
53
|
+
## Resources
|
|
54
|
+
|
|
55
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title#accessibility_concerns
|