testaro 18.1.1 → 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 +1 -1
- 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,63 @@
|
|
|
1
|
+
import { TextUtility } from '../../../utils/text';
|
|
2
|
+
import { CATEGORY_TYPE } from '../../../constants/categoryType';
|
|
3
|
+
import { IIssueReport } from '../../../interfaces/rule-issue.interface';
|
|
4
|
+
import { TranslateService } from '../../../services/translate';
|
|
5
|
+
import { $accessibilityAuditRules, $severity } from '../../../constants/accessibility';
|
|
6
|
+
import { AbstractRule, IAbstractRuleConfig } from '../../abstract-rule';
|
|
7
|
+
|
|
8
|
+
export class HeadingsSiblingUnique extends AbstractRule {
|
|
9
|
+
protected selector: string = 'h1, h2, h3, h4, h5, h6';
|
|
10
|
+
|
|
11
|
+
protected ruleConfig: IAbstractRuleConfig = {
|
|
12
|
+
id: TextUtility.convertUnderscoresToDashes($accessibilityAuditRules.headings_sibling_unique),
|
|
13
|
+
links: [
|
|
14
|
+
{
|
|
15
|
+
content: '2.4.6 Headings and Labels (Level AA, Primary Success Criterion)',
|
|
16
|
+
url: 'http://www.w3.org/TR/WCAG20/#navigation-mechanisms-descriptive'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
content: 'G130: Providing descriptive headings',
|
|
20
|
+
url: 'http://www.w3.org/TR/WCAG20-TECHS/G130'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
content: 'G141: Organizing a page using headings',
|
|
24
|
+
url: 'https://www.w3.org/TR/WCAG20-TECHS/G141.html'
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
recommendations: [],
|
|
28
|
+
severity: $severity.high,
|
|
29
|
+
type: CATEGORY_TYPE.WCAG_AA
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
public validate(headings: HTMLHeadingElement[]): void {
|
|
33
|
+
|
|
34
|
+
const checkForSiblings = (headingElement: HTMLHeadingElement): void => {
|
|
35
|
+
const nextElementSibling: Element | null = headingElement.nextElementSibling;
|
|
36
|
+
|
|
37
|
+
if (nextElementSibling === null) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (nextElementSibling.nodeName.toLowerCase() !== headingElement.nodeName.toLowerCase()) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const headingText: string = typeof headingElement.textContent === 'string' ? TextUtility.safeTrim(headingElement.textContent) : '';
|
|
46
|
+
const nextElementSiblingText: string = typeof nextElementSibling.textContent === 'string' ? TextUtility.safeTrim(nextElementSibling.textContent) : '';
|
|
47
|
+
|
|
48
|
+
if (TextUtility.areStringsTheSame(headingText, nextElementSiblingText) === false) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const report: IIssueReport = {
|
|
53
|
+
message: TranslateService.instant('headings_sibling_unique_report_message', [headingElement.nodeName.toLowerCase(), nextElementSibling.textContent]),
|
|
54
|
+
node: headingElement,
|
|
55
|
+
ruleId: this.ruleConfig.id
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
this.validator.report(report);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
headings.forEach(checkForSiblings);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# horizontal-rule
|
|
2
|
+
|
|
3
|
+
## Rule id
|
|
4
|
+
|
|
5
|
+
`horizontal-rule`
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
This rule determines `<hr/>` elements and gives a tip.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
The `<hr/>` element adds extra "noise" and can be confusing. For example VoiceOver reads it as "dimmed collapsed on top, horizontal separator", Windows Narrator reads it as "end of line".
|
|
14
|
+
|
|
15
|
+
A better option is to replace `<hr/>` with `<div>` and use CSS for styling. Alternatively, `aria-hidden="true"` or `role="presentation"` can be applied to the `<hr/>` element.
|
|
16
|
+
|
|
17
|
+
## Test cases
|
|
18
|
+
|
|
19
|
+
### Passed
|
|
20
|
+
|
|
21
|
+
The rule passes when there are:
|
|
22
|
+
|
|
23
|
+
* no `<hr/>` elements or
|
|
24
|
+
* `<hr/>` use `aria-hidden="true"` or `role="presentation"`
|
|
25
|
+
|
|
26
|
+
## WCAG Success Criteria
|
|
27
|
+
|
|
28
|
+
Not Applicable
|
|
29
|
+
|
|
30
|
+
## Best Practice
|
|
31
|
+
|
|
32
|
+
Yes
|
|
33
|
+
|
|
34
|
+
## User Impact
|
|
35
|
+
|
|
36
|
+
* **Severity**: info
|
|
37
|
+
* **Disabilities Affected**:
|
|
38
|
+
* Visual:
|
|
39
|
+
* blindness
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { HorizontalRule } from './horizontal-rule';
|
|
2
|
+
import { Validator } from '../../../validator';
|
|
3
|
+
import { DomUtility } from '../../../utils/dom';
|
|
4
|
+
|
|
5
|
+
describe('Rules', () => {
|
|
6
|
+
|
|
7
|
+
describe('HorizontalRule', () => {
|
|
8
|
+
|
|
9
|
+
it('should indicate that class exists', () => {
|
|
10
|
+
expect(HorizontalRule).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const selector: string = `hr${[
|
|
14
|
+
':not([aria-hidden="true"])',
|
|
15
|
+
':not([role="presentation"])'
|
|
16
|
+
].join('')}`;
|
|
17
|
+
|
|
18
|
+
let fakeDom;
|
|
19
|
+
|
|
20
|
+
new HorizontalRule().registerValidator();
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
fakeDom = document.createElement('div');
|
|
24
|
+
fakeDom.id = 'fakedom';
|
|
25
|
+
document.body.appendChild(fakeDom);
|
|
26
|
+
|
|
27
|
+
Validator.reset();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
DomUtility.remove(document.getElementById('fakedom'));
|
|
32
|
+
fakeDom = undefined;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return one report when there is an element with hr', () => {
|
|
36
|
+
fakeDom.innerHTML = '<p>Text 1</p><hr/><p>Text 2</p>';
|
|
37
|
+
const nodes: HTMLHRElement[] = DomUtility.querySelectorAllExclude(selector, fakeDom) as HTMLHRElement[];
|
|
38
|
+
|
|
39
|
+
new HorizontalRule().validate(nodes);
|
|
40
|
+
|
|
41
|
+
expect(Object.keys(Validator.getReports()).length).toBe(1);
|
|
42
|
+
expect(Validator.getReport('report_0').message).toBe('The <code><hr></code> element adds extra <q>noise</q> and can be confusing. For example VoiceOver reads it as <q>dimmed collapsed on top, horizontal separator</q>, Windows Narrator reads it as <q>end of line</q>. A better option is to replace <code><hr></code> with <code><div></code> and use CSS for styling. Alternatively, <code>aria-hidden=\'true\'</code> or <code>role=\'presentation\'</code> can be applied to the <code><hr></code> element.');
|
|
43
|
+
expect(Validator.getReport('report_0').node.nodeName.toLowerCase()).toBe('hr');
|
|
44
|
+
expect(Validator.getReport('report_0').ruleId).toBe('horizontal-rule');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return no reports when there is no elements with hr', () => {
|
|
48
|
+
fakeDom.innerHTML = '<p>Text 1</p>hr<p>Text 2</p>';
|
|
49
|
+
const nodes: HTMLHRElement[] = DomUtility.querySelectorAllExclude(selector, fakeDom) as HTMLHRElement[];
|
|
50
|
+
|
|
51
|
+
new HorizontalRule().validate(nodes);
|
|
52
|
+
|
|
53
|
+
expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return no reports when there is an element hr, but with attribue aria-hidden="true" or role="presentation"', () => {
|
|
57
|
+
fakeDom.innerHTML = '<p>Text 1</p><hr aria-hidden="true"/><hr role="presentation"/><p>Text 2</p>';
|
|
58
|
+
const nodes: HTMLHRElement[] = DomUtility.querySelectorAllExclude(selector, fakeDom) as HTMLHRElement[];
|
|
59
|
+
|
|
60
|
+
new HorizontalRule().validate(nodes);
|
|
61
|
+
|
|
62
|
+
expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CATEGORY_TYPE } from '../../../constants/categoryType';
|
|
2
|
+
import { IIssueReport } from '../../../interfaces/rule-issue.interface';
|
|
3
|
+
import { TextUtility } from '../../../utils/text';
|
|
4
|
+
import { TranslateService } from '../../../services/translate';
|
|
5
|
+
import { $accessibilityAuditRules, $severity } from '../../../constants/accessibility';
|
|
6
|
+
import { AbstractRule, IAbstractRuleConfig } from '../../abstract-rule';
|
|
7
|
+
|
|
8
|
+
export class HorizontalRule extends AbstractRule {
|
|
9
|
+
protected selector: string = `hr${[
|
|
10
|
+
':not([aria-hidden="true"])',
|
|
11
|
+
':not([role="presentation"])'
|
|
12
|
+
].join('')}`;
|
|
13
|
+
|
|
14
|
+
protected ruleConfig: IAbstractRuleConfig = {
|
|
15
|
+
id: TextUtility.convertUnderscoresToDashes($accessibilityAuditRules.horizontal_rule),
|
|
16
|
+
links: [],
|
|
17
|
+
recommendations: [],
|
|
18
|
+
severity: $severity.info,
|
|
19
|
+
type: CATEGORY_TYPE.BEST_PRACTICE
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
public validate(elements: HTMLHRElement[]): void {
|
|
23
|
+
const reportHR = (element: HTMLHRElement): void => {
|
|
24
|
+
const reportMessage: string = TranslateService.instant('horizontal_rule_report_message', [TextUtility.escape('<hr>'), TextUtility.escape('<hr>'), TextUtility.escape('<div>'), TextUtility.escape('<hr>')]);
|
|
25
|
+
|
|
26
|
+
const report: IIssueReport = {
|
|
27
|
+
message: reportMessage,
|
|
28
|
+
node: element,
|
|
29
|
+
ruleId: this.ruleConfig.id
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
this.validator.report(report);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
elements.forEach(reportHR);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# incorrect-technique-for-hiding-content
|
|
2
|
+
|
|
3
|
+
## Rule id
|
|
4
|
+
|
|
5
|
+
`incorrect-technique-for-hiding-content`
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
This rule determines if there is defined style `text-indent` with the value so that the element is being positioned out of the viewport.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
The technique e.g. `text-indent: -10000px` is still being used to hide the content visually. However, the downside of that technique is that the screen reader focus follows that and it gets out of the visible area.
|
|
14
|
+
|
|
15
|
+
**Tip**: to avoid that use tecnique that hides the content visually, but does not move the element outside of the visible viewport. Example: https://github.com/twbs/bootstrap/blob/main/scss/mixins/_visually-hidden.scss
|
|
16
|
+
|
|
17
|
+
## Test cases
|
|
18
|
+
|
|
19
|
+
### Passed
|
|
20
|
+
|
|
21
|
+
The rule passes when there is no defined style `text-indent` that hides the content outside of viewport.
|
|
22
|
+
|
|
23
|
+
## WCAG Success Criteria
|
|
24
|
+
|
|
25
|
+
Not Applicable
|
|
26
|
+
|
|
27
|
+
## Best Practice
|
|
28
|
+
|
|
29
|
+
Yes
|
|
30
|
+
|
|
31
|
+
## User Impact
|
|
32
|
+
|
|
33
|
+
* **Severity**: info
|
|
34
|
+
* **Disabilities Affected**:
|
|
35
|
+
* Visual:
|
|
36
|
+
* keyboard users
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { DomUtility } from '../../../utils/dom';
|
|
2
|
+
import { Validator } from '../../../validator';
|
|
3
|
+
import { IncorrectTechniqueForHidingContent } from './incorrect-technique-for-hiding-content';
|
|
4
|
+
|
|
5
|
+
describe('Rules', () => {
|
|
6
|
+
|
|
7
|
+
describe('IncorrectTechniqueForHidingContent', () => {
|
|
8
|
+
|
|
9
|
+
it('should indicate that class exists', () => {
|
|
10
|
+
expect(IncorrectTechniqueForHidingContent).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let fakeDom;
|
|
14
|
+
|
|
15
|
+
new IncorrectTechniqueForHidingContent().registerValidator();
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
fakeDom = document.createElement('div');
|
|
19
|
+
fakeDom.id = 'fakedom';
|
|
20
|
+
document.body.appendChild(fakeDom);
|
|
21
|
+
|
|
22
|
+
Validator.reset();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
DomUtility.remove(document.getElementById('fakedom'));
|
|
27
|
+
fakeDom = undefined;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Note: untestable until https://github.com/facebook/jest/issues/11291 gets resolved
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
*
|
|
34
|
+
*it('should return 1 report when there is an element with defined style text-indent: -1000px;', () => {
|
|
35
|
+
* fakeDom.innerHTML = '<p style="text-indent: -1000px">Example text</p>';
|
|
36
|
+
* const nodes: HTMLParagraphElement[] = DomUtility.querySelectorAllExclude('p', fakeDom) as HTMLParagraphElement[];
|
|
37
|
+
*
|
|
38
|
+
* new IncorrectTechniqueForHidingContent().validate(nodes);
|
|
39
|
+
*
|
|
40
|
+
* expect(Object.keys(Validator.getReports()).length).toBe(1);
|
|
41
|
+
* expect(Validator.getReport('report_0').message).toBe('CSS technique <code>text-indent: -1000px </code> is used to hide text. However, it causes problems for right-to-left language and also keep focus for screen reader outside of visible area.');
|
|
42
|
+
* expect(Validator.getReport('report_0').node.nodeName.toLowerCase()).toBe('p');
|
|
43
|
+
* expect(Validator.getReport('report_0').ruleId).toBe('incorrect-technique-for-hiding-content');
|
|
44
|
+
*});
|
|
45
|
+
*
|
|
46
|
+
*it('should return 1 report when there is an element with defined style text-indent: 1000px;', () => {
|
|
47
|
+
* fakeDom.innerHTML = '<p style="text-indent: 1000px">Example text</p>';
|
|
48
|
+
* const nodes: HTMLParagraphElement[] = DomUtility.querySelectorAllExclude('p', fakeDom) as HTMLParagraphElement[];
|
|
49
|
+
*
|
|
50
|
+
* new IncorrectTechniqueForHidingContent().validate(nodes);
|
|
51
|
+
*
|
|
52
|
+
* expect(Object.keys(Validator.getReports()).length).toBe(1);
|
|
53
|
+
* expect(Validator.getReport('report_0').message).toBe('CSS technique <code>text-indent: 1000px </code> is used to hide text. However, it causes problems for right-to-left language and also keep focus for screen reader outside of visible area.');
|
|
54
|
+
* expect(Validator.getReport('report_0').node.nodeName.toLowerCase()).toBe('p');
|
|
55
|
+
* expect(Validator.getReport('report_0').ruleId).toBe('incorrect-technique-for-hiding-content');
|
|
56
|
+
*});
|
|
57
|
+
*
|
|
58
|
+
*it('should return no report when there is an element with no defined style text-indent', () => {
|
|
59
|
+
* fakeDom.innerHTML = '<p>Example text</p>';
|
|
60
|
+
* const nodes: HTMLParagraphElement[] = DomUtility.querySelectorAllExclude('p', fakeDom) as HTMLParagraphElement[];
|
|
61
|
+
*
|
|
62
|
+
* new IncorrectTechniqueForHidingContent().validate(nodes);
|
|
63
|
+
*
|
|
64
|
+
* expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
65
|
+
*});
|
|
66
|
+
*
|
|
67
|
+
*it('should return no report when there is an element with defined style text-indent that doesn\'t move content outside of visible area', () => {
|
|
68
|
+
* const createMockDiv = (width, height) => {
|
|
69
|
+
* const div = document.createElement('div');
|
|
70
|
+
*
|
|
71
|
+
* Object.assign(div.style, {
|
|
72
|
+
* height: `${height}px`,
|
|
73
|
+
* width: `${width}px`
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* div.getBoundingClientRect = () => {
|
|
77
|
+
* return {
|
|
78
|
+
* bottom: height,
|
|
79
|
+
* height,
|
|
80
|
+
* left: 0,
|
|
81
|
+
* right: width,
|
|
82
|
+
* toJSON: () => { },
|
|
83
|
+
* top: 0,
|
|
84
|
+
* width,
|
|
85
|
+
* x: 0,
|
|
86
|
+
* y: 0
|
|
87
|
+
* };
|
|
88
|
+
* };
|
|
89
|
+
*
|
|
90
|
+
* return div;
|
|
91
|
+
* };
|
|
92
|
+
*
|
|
93
|
+
* const div = createMockDiv(100, 100);
|
|
94
|
+
*
|
|
95
|
+
* div.innerText = 'Example text';
|
|
96
|
+
* div.style.textIndent = '-1rem';
|
|
97
|
+
*
|
|
98
|
+
* fakeDom.appendChild(div);
|
|
99
|
+
*
|
|
100
|
+
* const nodes: HTMLDivElement[] = DomUtility.querySelectorAllExclude('div', fakeDom) as HTMLDivElement[];
|
|
101
|
+
*
|
|
102
|
+
* global.innerHeight = 500;
|
|
103
|
+
* global.innerWidth = 500;
|
|
104
|
+
*
|
|
105
|
+
* new IncorrectTechniqueForHidingContent().validate(nodes);
|
|
106
|
+
*
|
|
107
|
+
* expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
108
|
+
*});
|
|
109
|
+
*
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Css } from '../../../utils/css';
|
|
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
|
+
import { DomUtility } from '../../../utils/dom';
|
|
9
|
+
import { ObjectUtility } from '../../../utils/object';
|
|
10
|
+
|
|
11
|
+
export class IncorrectTechniqueForHidingContent extends AbstractRule {
|
|
12
|
+
protected selector: string = `*${[
|
|
13
|
+
':root',
|
|
14
|
+
'head',
|
|
15
|
+
'title',
|
|
16
|
+
'style',
|
|
17
|
+
'script',
|
|
18
|
+
'noscript',
|
|
19
|
+
'meta',
|
|
20
|
+
'link',
|
|
21
|
+
'br',
|
|
22
|
+
'hr',
|
|
23
|
+
'object',
|
|
24
|
+
'path',
|
|
25
|
+
'g',
|
|
26
|
+
'desc',
|
|
27
|
+
'filter',
|
|
28
|
+
'img',
|
|
29
|
+
'input',
|
|
30
|
+
'iframe',
|
|
31
|
+
'code',
|
|
32
|
+
'defs',
|
|
33
|
+
':empty'
|
|
34
|
+
].map((i: string): string => {
|
|
35
|
+
return `:not(${i})`;
|
|
36
|
+
}).join('')}`;
|
|
37
|
+
|
|
38
|
+
protected ruleConfig: IAbstractRuleConfig = {
|
|
39
|
+
id: TextUtility.convertUnderscoresToDashes($accessibilityAuditRules.incorrect_technique_for_hiding_content),
|
|
40
|
+
links: [
|
|
41
|
+
{
|
|
42
|
+
content: 'Hiding Content for Accessibility',
|
|
43
|
+
url: 'https://snook.ca/archives/html_and_css/hiding-content-for-accessibility'
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
recommendations: [],
|
|
47
|
+
severity: $severity.high,
|
|
48
|
+
type: CATEGORY_TYPE.BEST_PRACTICE
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
public validate(elements: HTMLElement[]): void {
|
|
52
|
+
|
|
53
|
+
if (ObjectUtility.isHostMethod(document, 'getSelection') === false && ObjectUtility.isHostMethod(document, 'createRange') === false) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const reportNode = (element: HTMLElement): void => {
|
|
58
|
+
const styles: CSSStyleDeclaration | null = Css.getComputedStyle(element);
|
|
59
|
+
|
|
60
|
+
if (styles === null) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const textIndentValue: string = styles.getPropertyValue('text-indent');
|
|
65
|
+
const indent: number = Number(textIndentValue.replace(/[^0-9]/g, ''));
|
|
66
|
+
|
|
67
|
+
if (indent === 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const selection: Selection | null = document.getSelection();
|
|
72
|
+
|
|
73
|
+
if (selection === null) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const range: Range = document.createRange();
|
|
78
|
+
|
|
79
|
+
range.selectNodeContents(element);
|
|
80
|
+
|
|
81
|
+
selection.removeAllRanges();
|
|
82
|
+
selection.addRange(range);
|
|
83
|
+
|
|
84
|
+
for (let i: number = 0; i < selection.rangeCount; i += 1) {
|
|
85
|
+
if (DomUtility.isRangeOffPage(selection.getRangeAt(i))) {
|
|
86
|
+
const reportMessage: string = TranslateService.instant('incorrect_technique_for_hiding_content_report_message', [textIndentValue]);
|
|
87
|
+
|
|
88
|
+
const report: IIssueReport = {
|
|
89
|
+
message: reportMessage,
|
|
90
|
+
node: element,
|
|
91
|
+
ruleId: this.ruleConfig.id
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.validator.report(report);
|
|
95
|
+
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
elements.forEach(reportNode);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# invalid-attribute-dir-value
|
|
2
|
+
|
|
3
|
+
## Rule id
|
|
4
|
+
|
|
5
|
+
`invalid-attribute-dir-value`
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
This rule determines if a given attribute `dir` have the correct value: `rtl`, `ltr` or `auto`.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
The dir global attribute is an enumerated attribute that indicates the directionality of the element's text. To avoid unexpected behaviour the value of `dir` should be correct and should contains one of following string: `rtl`, `ltr` or `auto`.
|
|
14
|
+
|
|
15
|
+
## Test cases
|
|
16
|
+
|
|
17
|
+
### Passed
|
|
18
|
+
|
|
19
|
+
The rule passes when there is no `dir` attribute with value other than `rtl`, `ltr` or `auto`.
|
|
20
|
+
|
|
21
|
+
## WCAG Success Criteria
|
|
22
|
+
|
|
23
|
+
Not Applicable
|
|
24
|
+
|
|
25
|
+
## Best Practice
|
|
26
|
+
|
|
27
|
+
Yes
|
|
28
|
+
|
|
29
|
+
## User Impact
|
|
30
|
+
|
|
31
|
+
* **Severity**: info
|
|
32
|
+
* **Disabilities Affected**:
|
|
33
|
+
* Visual:
|
|
34
|
+
* blindness
|
package/aslint/app/rules/aslint/invalid-attribute-dir-value/invalid-attribute-dir-value.test.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { InvalidAttributeDirValue } from './invalid-attribute-dir-value';
|
|
2
|
+
import { Validator } from '../../../validator';
|
|
3
|
+
import { DomUtility } from '../../../utils/dom';
|
|
4
|
+
|
|
5
|
+
describe('Rules', () => {
|
|
6
|
+
|
|
7
|
+
describe('InvalidAttributeDirValue', () => {
|
|
8
|
+
|
|
9
|
+
it('should indicate that class exists', () => {
|
|
10
|
+
expect(InvalidAttributeDirValue).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let fakeDom;
|
|
14
|
+
|
|
15
|
+
new InvalidAttributeDirValue().registerValidator();
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
fakeDom = document.createElement('div');
|
|
19
|
+
fakeDom.id = 'fakedom';
|
|
20
|
+
document.body.appendChild(fakeDom);
|
|
21
|
+
|
|
22
|
+
Validator.reset();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
DomUtility.remove(document.getElementById('fakedom'));
|
|
27
|
+
fakeDom = undefined;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return correct data for dir with wrong attribute value', () => {
|
|
31
|
+
fakeDom.innerHTML = '<p dir="wrong">Wrong attribute</p>';
|
|
32
|
+
const nodes = DomUtility.querySelectorAllExclude('[dir]:not([dir="rtl"]):not([dir="ltr"]):not([dir="auto"])', fakeDom) as HTMLElement[];
|
|
33
|
+
|
|
34
|
+
new InvalidAttributeDirValue().validate(nodes);
|
|
35
|
+
|
|
36
|
+
expect(Object.keys(Validator.getReports()).length).toBe(1);
|
|
37
|
+
expect(Validator.getReport('report_0').message).toBe('Attribute <code>dir</code> has invalid value <code>wrong</code>.');
|
|
38
|
+
expect(Validator.getReport('report_0').node.nodeName.toLowerCase()).toBe('p');
|
|
39
|
+
expect(Validator.getReport('report_0').ruleId).toBe('invalid-attribute-dir-value');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return correct data for dir with empty attribute value', () => {
|
|
43
|
+
fakeDom.innerHTML = '<p dir>Wrong attribute</p>';
|
|
44
|
+
const nodes = DomUtility.querySelectorAllExclude('[dir]:not([dir="rtl"]):not([dir="ltr"]):not([dir="auto"])', fakeDom) as HTMLElement[];
|
|
45
|
+
|
|
46
|
+
new InvalidAttributeDirValue().validate(nodes);
|
|
47
|
+
|
|
48
|
+
expect(Object.keys(Validator.getReports()).length).toBe(1);
|
|
49
|
+
expect(Validator.getReport('report_0').message).toBe('Attribute <code>dir</code> has invalid value <code></code>.');
|
|
50
|
+
expect(Validator.getReport('report_0').node.nodeName.toLowerCase()).toBe('p');
|
|
51
|
+
expect(Validator.getReport('report_0').ruleId).toBe('invalid-attribute-dir-value');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return no reports in case of dir is ltr', () => {
|
|
55
|
+
fakeDom.innerHTML = '<p dir="ltr">Left to right(English)</p>';
|
|
56
|
+
const nodes = DomUtility.querySelectorAllExclude('[dir]:not([dir="rtl"]):not([dir="ltr"]):not([dir="auto"])', fakeDom) as HTMLElement[];
|
|
57
|
+
|
|
58
|
+
new InvalidAttributeDirValue().validate(nodes);
|
|
59
|
+
|
|
60
|
+
expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return no reports in case of dir is rtl', () => {
|
|
64
|
+
fakeDom.innerHTML = '<p dir="rtl">Right to left(Arabic)</p>';
|
|
65
|
+
const nodes = DomUtility.querySelectorAllExclude('[dir]:not([dir="rtl"]):not([dir="ltr"]):not([dir="auto"])', fakeDom) as HTMLElement[];
|
|
66
|
+
|
|
67
|
+
new InvalidAttributeDirValue().validate(nodes);
|
|
68
|
+
|
|
69
|
+
expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return no reports in case of dir is auto', () => {
|
|
73
|
+
fakeDom.innerHTML = '<p dir="auto">Auto</p>';
|
|
74
|
+
const nodes = DomUtility.querySelectorAllExclude('[dir]:not([dir="rtl"]):not([dir="ltr"]):not([dir="auto"])', fakeDom) as HTMLElement[];
|
|
75
|
+
|
|
76
|
+
new InvalidAttributeDirValue().validate(nodes);
|
|
77
|
+
|
|
78
|
+
expect(Object.keys(Validator.getReports()).length).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CATEGORY_TYPE } from '../../../constants/categoryType';
|
|
2
|
+
import { IIssueReport } from '../../../interfaces/rule-issue.interface';
|
|
3
|
+
import { TextUtility } from '../../../utils/text';
|
|
4
|
+
import { TranslateService } from '../../../services/translate';
|
|
5
|
+
import { $accessibilityAuditRules, $severity } from '../../../constants/accessibility';
|
|
6
|
+
import { AbstractRule, IAbstractRuleConfig } from '../../abstract-rule';
|
|
7
|
+
|
|
8
|
+
export class InvalidAttributeDirValue extends AbstractRule {
|
|
9
|
+
protected selector: string = '[dir]:not([dir="rtl"]):not([dir="ltr"]):not([dir="auto"])';
|
|
10
|
+
protected ruleConfig: IAbstractRuleConfig = {
|
|
11
|
+
id: TextUtility.convertUnderscoresToDashes($accessibilityAuditRules.invalid_attribute_dir_value),
|
|
12
|
+
links: [
|
|
13
|
+
{
|
|
14
|
+
content: 'H56: Using the dir attribute on an inline element to resolve problems with nested directional runs',
|
|
15
|
+
url: 'https://www.w3.org/TR/WCAG20-TECHS/H56.html'
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
recommendations: [],
|
|
19
|
+
severity: $severity.critical,
|
|
20
|
+
type: CATEGORY_TYPE.BEST_PRACTICE
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
public validate(elements: HTMLElement[]): void {
|
|
24
|
+
const checkValue = (element: HTMLElement): void => {
|
|
25
|
+
const dirAttribute: string | null = element.getAttribute('dir');
|
|
26
|
+
|
|
27
|
+
if (dirAttribute === null) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const reportMessage: string = TranslateService.instant('invalid_attribute_dir_value_report_message', [TextUtility.escape(dirAttribute)]);
|
|
32
|
+
|
|
33
|
+
const report: IIssueReport = {
|
|
34
|
+
message: reportMessage,
|
|
35
|
+
node: element,
|
|
36
|
+
ruleId: this.ruleConfig.id
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this.validator.report(report);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
elements.forEach(checkValue);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# label-duplicated-content-title
|
|
2
|
+
|
|
3
|
+
## Rule id
|
|
4
|
+
|
|
5
|
+
`label-duplicated-content-title`
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
This rule determines if a given element `<label>` contains attribute `title` with the same accessible name.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
Let's examine following example:
|
|
14
|
+
|
|
15
|
+
`<label for="example" title="This is an example">This is an example</label>`
|
|
16
|
+
|
|
17
|
+
Assuming screen reader can read `title`<sup>*</sup>. In above case then `This is an example` will be read twice. To avoid that this rule helps to find such cases.
|
|
18
|
+
|
|
19
|
+
`*` - by default, for example, VoiceOver doesn't read `title` until Hints are turned on.
|
|
20
|
+
|
|
21
|
+
## Test cases
|
|
22
|
+
|
|
23
|
+
### Passed
|
|
24
|
+
|
|
25
|
+
The rule passes when there is no `<label>` with an attribute `title` that contains same content as `<label>`'s accessible name.
|
|
26
|
+
|
|
27
|
+
## WCAG Success Criteria
|
|
28
|
+
|
|
29
|
+
Not Applicable
|
|
30
|
+
|
|
31
|
+
## Best Practice
|
|
32
|
+
|
|
33
|
+
Yes
|
|
34
|
+
|
|
35
|
+
## User Impact
|
|
36
|
+
|
|
37
|
+
* **Severity**: info
|
|
38
|
+
* **Disabilities Affected**:
|
|
39
|
+
* Visual:
|
|
40
|
+
* blindness
|