Skip to content

Commit

Permalink
feat: add EN.301.549 tags to rules (#4063)
Browse files Browse the repository at this point in the history
* feat: add EN.301.549 tags to rules

* Add proper tag validation

* Docs

* Update tag validation

* Change to "EN-301-549"
  • Loading branch information
WilcoFiers committed Jun 29, 2023
1 parent 733c45e commit de3da89
Show file tree
Hide file tree
Showing 69 changed files with 584 additions and 178 deletions.
169 changes: 163 additions & 6 deletions build/tasks/validate.js
Expand Up @@ -198,12 +198,6 @@ function createSchemas() {
type: 'array',
items: {
type: 'string'
},
conform: function hasCategoryTag(tags) {
return tags.some(tag => tag.includes('cat.'));
},
messages: {
conform: 'must include a category tag'
}
},
actIds: {
Expand Down Expand Up @@ -307,5 +301,168 @@ function validateRule({ tags, metadata }) {
if (help.toLowerCase().includes(prohibitedWord)) {
issues.push(`metadata.help can not contain the word '${prohibitedWord}'.`);
}

issues.push(...findTagIssues(tags));
return issues;
}

const miscTags = ['ACT', 'experimental', 'review-item', 'deprecated'];

const categories = [
'aria',
'color',
'forms',
'keyboard',
'language',
'name-role-value',
'parsing',
'semantics',
'sensory-and-visual-cues',
'structure',
'tables',
'text-alternatives',
'time-and-media'
];

const standardsTags = [
{
// Has to be first, as others rely on the WCAG level getting picked up first
name: 'WCAG',
standardRegex: /^wcag2(1|2)?a{1,3}(-obsolete)?$/,
criterionRegex: /^wcag\d{3,4}$/
},
{
name: 'Section 508',
standardRegex: /^section508$/,
criterionRegex: /^section508\.\d{1,2}\.[a-z]$/,
wcagLevelRegex: /^wcag2aa?$/
},
{
name: 'Trusted Tester',
standardRegex: /^TTv5$/,
criterionRegex: /^TT\d{1,3}\.[a-z]$/,
wcagLevelRegex: /^wcag2aa?$/
},
{
name: 'EN 301 549',
standardRegex: /^EN-301-549$/,
criterionRegex: /^EN-9\.[1-4]\.[1-9]\.\d{1,2}$/,
wcagLevelRegex: /^wcag21?aa?$/
}
];

function findTagIssues(tags) {
const issues = [];
const catTags = tags.filter(tag => tag.startsWith('cat.'));
const bestPracticeTags = tags.filter(tag => tag === 'best-practice');

// Category
if (catTags.length !== 1) {
issues.push(`Must have exactly one cat. tag, got ${catTags.length}`);
}
if (catTags.length && !categories.includes(catTags[0].slice(4))) {
issues.push(`Invalid category tag: ${catTags[0]}`);
}
if (!startsWith(tags, catTags)) {
issues.push(`Tag ${catTags[0]} must be before ${tags[0]}`);
}
tags = removeTags(tags, catTags);

// Best practice
if (bestPracticeTags.length > 1) {
issues.push(
`Only one best-practice tag is allowed, got ${bestPracticeTags.length}`
);
}
if (!startsWith(tags, bestPracticeTags)) {
issues.push(`Tag ${bestPracticeTags[0]} must be before ${tags[0]}`);
}
tags = removeTags(tags, bestPracticeTags);

const standards = {};
// WCAG, Section 508, Trusted Tester, EN 301 549
for (const {
name,
standardRegex,
criterionRegex,
wcagLevelRegex
} of standardsTags) {
const standardTags = tags.filter(tag => tag.match(standardRegex));
const criterionTags = tags.filter(tag => tag.match(criterionRegex));
if (!standardTags.length && !criterionTags.length) {
continue;
}

standards[name] = {
name,
standardTag: standardTags[0] ?? null,
criterionTags
};
if (bestPracticeTags.length !== 0) {
issues.push(`${name} tags cannot be used along side best-practice tag`);
}
if (standardTags.length === 0) {
issues.push(`Expected one ${name} tag, got 0`);
} else if (standardTags.length > 1) {
issues.push(`Expected one ${name} tag, got: ${standardTags.join(', ')}`);
}
if (criterionTags.length === 0) {
issues.push(`Expected at least one ${name} criterion tag, got 0`);
}

if (wcagLevelRegex) {
const wcagLevel = standards.WCAG.standardTag;
if (!wcagLevel.match(wcagLevelRegex)) {
issues.push(`${name} rules not allowed on ${wcagLevel}`);
}
}

// Must have the same criteria listed
if (name === 'EN 301 549') {
const wcagCriteria = standards.WCAG.criterionTags.map(tag =>
tag.slice(4)
);
const enCriteria = criterionTags.map(tag =>
tag.slice(5).replaceAll('.', '')
);
if (
wcagCriteria.length !== enCriteria.length ||
!startsWith(wcagCriteria, enCriteria)
) {
issues.push(
`Expect WCAG and EN criteria numbers to match: ${wcagCriteria.join(
', '
)} vs ${enCriteria.join(', ')}}`
);
}
}
tags = removeTags(tags, [...standardTags, ...criterionTags]);
}

// Other tags
const usedMiscTags = miscTags.filter(tag => tags.includes(tag));
const unknownTags = removeTags(tags, usedMiscTags);
if (unknownTags.length) {
issues.push(`Invalid tags: ${unknownTags.join(', ')}`);
}

// At this point only misc tags are left:
tags = removeTags(tags, unknownTags);
if (!startsWith(tags, usedMiscTags)) {
issues.push(
`Tags [${tags.join(', ')}] should be sorted like [${usedMiscTags.join(
', '
)}]`
);
}

return issues;
}

function startsWith(arr1, arr2) {
return arr2.every((item, i) => item === arr1[i]);
}

function removeTags(tags, tagsToRemove) {
return tags.filter(tag => !tagsToRemove.includes(tag));
}
38 changes: 20 additions & 18 deletions doc/API.md
Expand Up @@ -76,24 +76,26 @@ Each rule in axe-core has a number of tags. These provide metadata about the rul

The `experimental`, `ACT`, `TT`, and `section508` tags are only added to some rules. Each rule with a `section508` tag also has a tag to indicate what requirement in old Section 508 the rule is required by. For example `section508.22.a`.

| Tag Name | Accessibility Standard / Purpose |
| ----------------- | ---------------------------------------------------- |
| `wcag2a` | WCAG 2.0 Level A |
| `wcag2aa` | WCAG 2.0 Level AA |
| `wcag2aaa` | WCAG 2.0 Level AAA |
| `wcag21a` | WCAG 2.1 Level A |
| `wcag21aa` | WCAG 2.1 Level AA |
| `wcag22aa` | WCAG 2.2 Level AA |
| `best-practice` | Common accessibility best practices |
| `wcag2a-obsolete` | WCAG 2.0 Level A, no longer required for conformance |
| `wcag***` | WCAG success criterion e.g. wcag111 maps to SC 1.1.1 |
| `ACT` | W3C approved Accessibility Conformance Testing rules |
| `section508` | Old Section 508 rules |
| `section508.*.*` | Requirement in old Section 508 |
| `TTv5` | Trusted Tester v5 rules |
| `TT*.*` | Test ID in Trusted Tester |
| `experimental` | Cutting-edge rules, disabled by default |
| `cat.*` | Category mappings used by Deque (see below) |
| Tag Name | Accessibility Standard / Purpose |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `wcag2a` | WCAG 2.0 Level A |
| `wcag2aa` | WCAG 2.0 Level AA |
| `wcag2aaa` | WCAG 2.0 Level AAA |
| `wcag21a` | WCAG 2.1 Level A |
| `wcag21aa` | WCAG 2.1 Level AA |
| `wcag22aa` | WCAG 2.2 Level AA |
| `best-practice` | Common accessibility best practices |
| `wcag2a-obsolete` | WCAG 2.0 Level A, no longer required for conformance |
| `wcag***` | WCAG success criterion e.g. wcag111 maps to SC 1.1.1 |
| `ACT` | W3C approved Accessibility Conformance Testing rules |
| `section508` | Old Section 508 rules |
| `section508.*.*` | Requirement in old Section 508 |
| `TTv5` | Trusted Tester v5 rules |
| `TT*.*` | Test ID in Trusted Tester |
| `EN-301-549` | Rule required under [EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf) |
| `EN-9.*` | Section in EN 301 549 listing the requirement |
| `experimental` | Cutting-edge rules, disabled by default |
| `cat.*` | Category mappings used by Deque (see below) |

All rules have a `cat.*` tag, which indicates what type of content it is part of. The following `cat.*` tags exist in axe-core:

Expand Down

0 comments on commit de3da89

Please sign in to comment.