Main landmark 1 created with main tag
+diff --git a/Gruntfile.js b/Gruntfile.js index e11bcfa238..337f47f05a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -242,7 +242,10 @@ module.exports = function(grunt) { tasks: ['build', 'notify'] }, tests: { - files: ['test/**/*.js', 'test/integration/**/!(index).{html,json}'], + files: [ + 'test/**/*.js', + 'test/integration/**/!(index).{xhtml,html,json}' + ], tasks: ['clean:tests', 'testconfig', 'fixture'] } }, diff --git a/build/tasks/testconfig.js b/build/tasks/testconfig.js index 9834cd1a0c..08d24a6f0d 100644 --- a/build/tasks/testconfig.js +++ b/build/tasks/testconfig.js @@ -14,7 +14,13 @@ module.exports = function(grunt) { this.files.forEach(function(f) { f.src.forEach(function(filepath) { var config = grunt.file.readJSON(filepath); - config.content = grunt.file.read(filepath.replace(/json$/, 'html')); + try { + config.content = grunt.file.read(filepath.replace(/json$/, 'html')); + } catch (e) { + config.content = grunt.file.read( + filepath.replace(/json$/, 'xhtml') + ); + } result.tests[config.rule] = result.tests[config.rule] || []; result.tests[config.rule].push(config); }); diff --git a/build/test/config.js b/build/test/config.js index 54bf24ba85..eac1e4db9c 100644 --- a/build/test/config.js +++ b/build/test/config.js @@ -46,6 +46,7 @@ exports = module.exports = function(grunt, options) { log: true, urls: mapToUrl( [ + 'test/integration/full/**/*__.xhtml', 'test/integration/full/**/*.html', '!test/integration/full/**/frames/**/*.html' ], diff --git a/build/test/get-test-urls.js b/build/test/get-test-urls.js index d267895db8..6fb3649c84 100644 --- a/build/test/get-test-urls.js +++ b/build/test/get-test-urls.js @@ -27,6 +27,15 @@ const getTestUrls = async (host = `localhost`, port = `9876`) => { */ ...( await globby([ + // 'test/integration/full/landmark-one-main/**/*.html', + // '!test/integration/full/landmark-one-main/**/frames/**/*.html', + // 'test/integration/full/landmark-no-duplicate-main/**/*.html', + // '!test/integration/full/landmark-no-duplicate-main/**/frames/**/*.html' + + // 'test/integration/full/epub-type-has-matching-role/**/*__.xhtml', + // 'test/integration/full/pagebreak-label/**/*__.xhtml', + + 'test/integration/full/**/*__.xhtml', 'test/integration/full/**/*.html', '!test/integration/full/**/frames/**/*.html' ]) diff --git a/doc/examples/qunit/Gruntfile.js b/doc/examples/qunit/Gruntfile.js index b08c53082d..3f71195903 100644 --- a/doc/examples/qunit/Gruntfile.js +++ b/doc/examples/qunit/Gruntfile.js @@ -5,7 +5,7 @@ module.exports = function(grunt) { grunt.initConfig({ qunit: { - all: ['test/**/*.html'] + all: ['test/**/*.html', 'test/**/*__.xhtml'] } }); }; diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 4f143435d6..00e2f8209c 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -71,6 +71,7 @@ | :----------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------- | :------ | :-------------------------------- | :--------- | | [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.1/autocomplete-valid?application=RuleDescription) | Ensure the autocomplete attribute is correct and suitable for the form field | Serious | cat.forms, wcag21aa, wcag135 | failure | | [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.1/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412 | failure | +| [pagebreak-label](https://dequeuniversity.com/rules/axe/4.1/pagebreak-label?application=RuleDescription) | Ensure page markers have an accessible label | Serious | cat.epub | failure | ## Best Practices Rules @@ -83,6 +84,7 @@ Rules that do not necessarily conform to WCAG success criterion but are industry | [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.1/aria-dialog-name?application=RuleDescription) | Ensures every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.1/aria-treeitem-name?application=RuleDescription) | Ensures every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | [empty-heading](https://dequeuniversity.com/rules/axe/4.1/empty-heading?application=RuleDescription) | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | +| [epub-type-has-matching-role](https://dequeuniversity.com/rules/axe/4.1/epub-type-has-matching-role?application=RuleDescription) | Ensure the element has an ARIA role matching its epub:type | Minor | best-practice, cat.aria | failure | | [frame-tested](https://dequeuniversity.com/rules/axe/4.1/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item, best-practice | failure, needs review | | [frame-title-unique](https://dequeuniversity.com/rules/axe/4.1/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, best-practice | failure | | [heading-order](https://dequeuniversity.com/rules/axe/4.1/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure | @@ -96,7 +98,7 @@ Rules that do not necessarily conform to WCAG success criterion but are industry | [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.1/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.1/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.1/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | -| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.1/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | +| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.1/landmark-one-main?application=RuleDescription) | Ensures the document has a unique main landmark | Moderate | cat.semantics, best-practice | failure | | [landmark-unique](https://dequeuniversity.com/rules/axe/4.1/landmark-unique?application=RuleDescription) | Landmarks must have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | | [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.1/meta-viewport-large?application=RuleDescription) | Ensures <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | [meta-viewport](https://dequeuniversity.com/rules/axe/4.1/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, best-practice, ACT | failure | diff --git a/lib/checks/aria/aria-required-children-evaluate.js b/lib/checks/aria/aria-required-children-evaluate.js index 7b225d1a26..859f54c5b5 100644 --- a/lib/checks/aria/aria-required-children-evaluate.js +++ b/lib/checks/aria/aria-required-children-evaluate.js @@ -10,15 +10,28 @@ import { hasContentVirtual, idrefs } from '../../commons/dom'; * Get all owned roles of an element */ function getOwnedRoles(virtualNode) { + // DAISY-AXE + // const parentRole = getRole(virtualNode, { dpub: true }); + const ownedRoles = []; const ownedElements = getOwnedVirtual(virtualNode); for (let i = 0; i < ownedElements.length; i++) { let ownedElement = ownedElements[i]; + + // DAISY-AXE + // let role = getRole(ownedElement, { dpub: true }); let role = getRole(ownedElement); // if owned node has no role or is presentational we keep // parsing the descendant tree. this means intermediate roles // between a required parent and child will fail the check + + // DAISY-AXE + // if ( + // ['presentation', 'none', null].includes(role) || + // (['list'].includes(role) && + // ['doc-bibliography', 'doc-endnotes'].includes(parentRole)) + // ) { if (['presentation', 'none', null].includes(role)) { ownedElements.push(...ownedElement.children); } else if (role) { diff --git a/lib/checks/aria/matching-aria-role-evaluate.js b/lib/checks/aria/matching-aria-role-evaluate.js new file mode 100644 index 0000000000..65963155f1 --- /dev/null +++ b/lib/checks/aria/matching-aria-role-evaluate.js @@ -0,0 +1,185 @@ +import { tokenList } from '../../core/utils'; +import { getRole } from '../../commons/aria'; +import matchesSelector from '../../core/utils/element-matches'; + +function matchingAriaRoleEvaluate(node) { + // https://idpf.github.io/epub-guides/epub-aria-authoring/#sec-mappings + // https://www.w3.org/TR/dpub-aam-1.0/#mapping_role_table + // https://w3c.github.io/publ-cg/guides/aria-mapping.html#mapping-table + const mappings = new Map([ + ['abstract', 'doc-abstract'], + ['acknowledgments', 'doc-acknowledgments'], + ['afterword', 'doc-afterword'], + // ['answer', '??'], + // ['answers', '??'], + ['appendix', 'doc-appendix'], + // ['assessment', '??'], + // ['assessments', '??'], + // ['backmatter', '??'], + // ['balloon', '??'], + // ['backlink', 'doc-backlink'], // ?? + ['biblioentry', 'doc-biblioentry'], + ['bibliography', 'doc-bibliography'], + ['biblioref', 'doc-biblioref'], + // ['bodymatter', '??'], + // ['bridgehead', '??'], + // ['case-study', '??'], + ['chapter', 'doc-chapter'], + ['colophon', 'doc-colophon'], + // ['concluding-sentence', '??'], + ['conclusion', 'doc-conclusion'], + // ['contributors', '??'], + // ['copyright-page', '??'], + // ['cover', '??'], + // ['cover-image', 'doc-cover'], // ?? + // ['covertitle', '??'], + ['credit', 'doc-credit'], + ['credits', 'doc-credits'], + ['dedication', 'doc-dedication'], + // ['division', '??'], + ['endnote', 'doc-endnote'], + ['endnotes', 'doc-endnotes'], + ['epigraph', 'doc-epigraph'], + ['epilogue', 'doc-epilogue'], + ['errata', 'doc-errata'], + // ['example', 'doc-example'], + // ['feedback', '??'], + ['figure', 'figure'], // ARIA + // ['fill-in-the-blank-problem', '??'], + ['footnote', 'doc-footnote'], + // ['footnotes', '??'], + ['foreword', 'doc-foreword'], + // ['frontmatter', '??'], + // ['fulltitle', '??'], + // ['general-problem', '??'], + ['glossary', 'doc-glossary'], + ['glossdef', 'definition'], // ARIA + ['glossref', 'doc-glossref'], + ['glossterm', 'term'], // ARIA + // ['halftitle', '??'], + // ['halftitlepage', '??'], + // ['imprimatur', '??'], + // ['imprint', '??'], + ['help', 'doc-tip'], // ?? + ['index', 'doc-index'], + // ['index-editor-note', '??'], + // ['index-entry', '??'], + // ['index-entry-list', '??'], + // ['index-group', '??'], + // ['index-headnotes', '??'], + // ['index-legend', '??'], + // ['index-locator', '??'], + // ['index-locator-list', '??'], + // ['index-locator-range', '??'], + // ['index-term', '??'], + // ['index-term-categories', '??'], + // ['index-term-category', '??'], + // ['index-xref-preferred', '??'], + // ['index-xref-related', '??'], + ['introduction', 'doc-introduction'], + // ['keyword', '??'], + // ['keywords', '??'], + // ['label', '??'], + // ['landmarks', 'directory'], // ARIA (SKIPPED! NavDoc) + // ['learning-objective', '??'], + // ['learning-objectives', '??'], + // ['learning-outcome', '??'], + // ['learning-outcomes', '??'], + // ['learning-resource', '??'], + // ['learning-resources', '??'], + // ['learning-standard', '??'], + // ['learning-standards', '??'], + ['list', 'list'], // ARIA + ['list-item', 'listitem'], // ARIA + // ['loa', '??'], + // ['loi', '??'], + // ['lot', '??'], + // ['lov', '??'], + // ['match-problem', '??'], + // ['multiple-choice-problem', '??'], + ['noteref', 'doc-noteref'], + ['notice', 'doc-notice'], + // ['ordinal', '??'], + // ['other-credits', '??'], + ['page-list', 'doc-pagelist'], + ['pagebreak', 'doc-pagebreak'], + // ['panel', '??'], + // ['panel-group', '??'], + ['part', 'doc-part'], + // ['practice', '??'], + // ['practices', '??'], + // ['preamble', '??'], + ['preface', 'doc-preface'], + ['prologue', 'doc-prologue'], + ['pullquote', 'doc-pullquote'], + ['qna', 'doc-qna'], + // ['question', '??'], + ['referrer', 'doc-backlink'], + // ['revision-history', '??'], + // ['seriespage', '??'], + // ['sound-area', '??'], + // ['subchapter', '??'], + ['subtitle', 'doc-subtitle'], + ['table', 'table'], + ['table-cell', 'cell'], + ['table-row', 'row'], + // ['text-area', '??'], + ['tip', 'doc-tip'], + // ['title', '??'], + // ['titlepage', '??'], + ['toc', 'doc-toc'] + // ['toc-brief', '??'], + // ['topic-sentence', '??'], + // ['true-false-problem', '??'], + // ['volume', '??'], + ]); + + const hasXmlEpubType = node.hasAttributeNS( + 'http://www.idpf.org/2007/ops', + 'type' + ); + if ( + hasXmlEpubType || + node.hasAttribute('epub:type') // for unit tests that are not XML-aware due to fixture.innerHTML + ) { + // abort if descendant of landmarks nav (nav with epub:type=landmarks) + if ( + (hasXmlEpubType && matchesSelector(node, 'nav[*|type~="landmarks"] *')) || + matchesSelector(node, 'nav[epub\\:type~="landmarks"] *') + ) { + // console.log('BREAKPOINT'); + // throw new Error('BREAKPOINT'); + return true; + } + + // iterate for each epub:type value + var types = tokenList( + hasXmlEpubType + ? node.getAttributeNS('http://www.idpf.org/2007/ops', 'type') + : node.getAttribute('epub:type') + ); + for (const type of types) { + // If there is a 1-1 mapping, check that the role is set (best practice) + if (mappings.has(type)) { + // Note: using axe’s `getRole` util returns the effective role of the element + // (either explicitly set with the role attribute or implicit) + // So this works for types mapping to core ARIA roles (eg. glossref/glossterm). + const mappedRole = mappings.get(type); + const role = getRole(node, { dpub: true }); + // if (mappedRole !== role) { + // console.log('BREAKPOINT: ', type, mappedRole, role); + // // throw new Error('BREAKPOINT'); + // } + return mappedRole === role; + } else { + // e.g. cover, landmarks + // console.log('BREAKPOINT: ', type); + // throw new Error('BREAKPOINT'); + } + } + } + + return true; +} + +export default matchingAriaRoleEvaluate; diff --git a/lib/checks/aria/matching-aria-role.json b/lib/checks/aria/matching-aria-role.json new file mode 100644 index 0000000000..b8996c7e10 --- /dev/null +++ b/lib/checks/aria/matching-aria-role.json @@ -0,0 +1,11 @@ +{ + "id": "matching-aria-role", + "evaluate": "matching-aria-role-evaluate", + "metadata": { + "impact": "minor", + "messages": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + } + } +} diff --git a/lib/checks/lists/listitem-evaluate.js b/lib/checks/lists/listitem-evaluate.js index aebcbd8cc9..bcfdc3b5d1 100644 --- a/lib/checks/lists/listitem-evaluate.js +++ b/lib/checks/lists/listitem-evaluate.js @@ -1,4 +1,7 @@ import { getComposedParent } from '../../commons/dom'; + +// DAISY-AXE +// import { getRoleType, isValidRole } from '../../commons/aria'; import { isValidRole } from '../../commons/aria'; function listitemEvaluate(node) { @@ -16,6 +19,11 @@ function listitemEvaluate(node) { } if (parentRole && isValidRole(parentRole)) { + // DAISY-AXE + // if (getRoleType(parentRole) === 'list') { + // return true; + // } + this.data({ messageKey: 'roleNotValid' }); diff --git a/lib/checks/lists/only-listitems-evaluate.js b/lib/checks/lists/only-listitems-evaluate.js index d1ab863428..1906a2e637 100644 --- a/lib/checks/lists/only-listitems-evaluate.js +++ b/lib/checks/lists/only-listitems-evaluate.js @@ -1,4 +1,7 @@ import { isVisible } from '../../commons/dom'; + +// DAISY-AXE +// import { getRole, getRoleType } from '../../commons/aria'; import { getRole } from '../../commons/aria'; function onlyListitemsEvaluate(node, options, virtualNode) { @@ -24,6 +27,10 @@ function onlyListitemsEvaluate(node, options, virtualNode) { isEmpty = false; const isLi = actualNode.nodeName.toUpperCase() === 'LI'; const role = getRole(vNode); + + // DAISY-AXE + // const isListItemRole = + // role === 'listitem' || getRoleType(role) === 'listitem'; const isListItemRole = role === 'listitem'; if (!isLi && !isListItemRole) { diff --git a/lib/commons/aria/get-element-unallowed-roles.js b/lib/commons/aria/get-element-unallowed-roles.js index d76b6767bf..d15f75db05 100644 --- a/lib/commons/aria/get-element-unallowed-roles.js +++ b/lib/commons/aria/get-element-unallowed-roles.js @@ -37,13 +37,14 @@ function getRoleSegments(node) { roles = roles.concat(nodeRoles); } - if (node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type')) { - const epubRoles = tokenList( - node.getAttributeNS('http://www.idpf.org/2007/ops', 'type').toLowerCase() - ).map(role => `doc-${role}`); - - roles = roles.concat(epubRoles); - } + // DAISY-AXE (EPUB epub:type should be ignored) + // if (node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type')) { + // const epubRoles = tokenList( + // node.getAttributeNS('http://www.idpf.org/2007/ops', 'type').toLowerCase() + // ).map(role => `doc-${role}`); + + // roles = roles.concat(epubRoles); + // } // filter invalid roles roles = roles.filter(role => isValidRole(role)); diff --git a/lib/core/base/metadata-function-map.js b/lib/core/base/metadata-function-map.js index c197b0d776..69762cbc08 100644 --- a/lib/core/base/metadata-function-map.js +++ b/lib/core/base/metadata-function-map.js @@ -164,6 +164,10 @@ import svgNamespaceMatches from '../../rules/svg-namespace-matches'; import windowIsTopMatches from '../../rules/window-is-top-matches'; import xmlLangMismatchMatches from '../../rules/xml-lang-mismatch-matches'; +import epubTypeHasMatchingRoleMatches from '../../rules/epub-type-has-matching-role-matches'; +import pagebreakLabelMatches from '../../rules/pagebreak-label-matches'; +import matchingAriaRoleEvaluate from '../../checks/aria/matching-aria-role-evaluate'; + const metadataFunctionMap = { // aria 'abstractrole-evaluate': abstractroleEvaluate, @@ -331,7 +335,11 @@ const metadataFunctionMap = { 'skip-link-matches': skipLinkMatches, 'svg-namespace-matches': svgNamespaceMatches, 'window-is-top-matches': windowIsTopMatches, - 'xml-lang-mismatch-matches': xmlLangMismatchMatches + 'xml-lang-mismatch-matches': xmlLangMismatchMatches, + + 'epub-type-has-matching-role-matches': epubTypeHasMatchingRoleMatches, + 'pagebreak-label-matches': pagebreakLabelMatches, + 'matching-aria-role-evaluate': matchingAriaRoleEvaluate }; export default metadataFunctionMap; diff --git a/lib/core/public/configure.js b/lib/core/public/configure.js index be3eb58c28..03a2209c9f 100644 --- a/lib/core/public/configure.js +++ b/lib/core/public/configure.js @@ -1,7 +1,13 @@ import { hasReporter } from './reporter'; import { configureStandards } from '../../standards'; +// import matchesSelector from '../../core/utils/element-matches'; +// import { tokenList } from '../../core/utils'; +// import { getRole } from '../../commons/aria'; + function configure(spec) { + // throw new Error("DAISY ACE BREAKPOINT AXE CONFIGURE"); + var audit; audit = axe._audit; diff --git a/lib/rules/epub-type-has-matching-role-matches.js b/lib/rules/epub-type-has-matching-role-matches.js new file mode 100644 index 0000000000..f4ceca0ba0 --- /dev/null +++ b/lib/rules/epub-type-has-matching-role-matches.js @@ -0,0 +1,19 @@ +function epubTypeHasMatchingRoleMatches(node) { + // selector: '[*|type]', + return ( + node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type') || + node.hasAttribute('epub:type') // for unit tests that are not XML-aware due to fixture.innerHTML + ); + + // console.log('node.nodeName: ', node.nodeName); + // const attrs = Array.from(getNodeAttributes(node)); + // console.log(attrs.length); + // attrs.forEach((attr) => { + // console.log('\n====='); + // console.log(JSON.stringify(attr)); + // console.log('attr.nodeName: ', attr.nodeName); + // console.log('attr.namespaceURI: ', attr.namespaceURI); + // }); +} + +export default epubTypeHasMatchingRoleMatches; diff --git a/lib/rules/epub-type-has-matching-role.json b/lib/rules/epub-type-has-matching-role.json new file mode 100644 index 0000000000..f49753c50b --- /dev/null +++ b/lib/rules/epub-type-has-matching-role.json @@ -0,0 +1,12 @@ +{ + "id": "epub-type-has-matching-role", + "matches": "epub-type-has-matching-role-matches", + "tags": ["best-practice", "cat.aria"], + "metadata": { + "description": "Ensure the element has an ARIA role matching its epub:type", + "help": "ARIA role should be used in addition to epub:type" + }, + "all": [], + "any": ["matching-aria-role"], + "none": [] +} diff --git a/lib/rules/landmark-one-main.json b/lib/rules/landmark-one-main.json index 533cae06e9..22b9f486ba 100644 --- a/lib/rules/landmark-one-main.json +++ b/lib/rules/landmark-one-main.json @@ -3,10 +3,10 @@ "selector": "html", "tags": ["cat.semantics", "best-practice"], "metadata": { - "description": "Ensures the document has a main landmark", - "help": "Document must have one main landmark" + "description": "Ensures the document has a unique main landmark", + "help": "Document must have one unique main landmark" }, - "all": ["page-has-main"], + "all": ["page-has-main", "page-no-duplicate-main"], "any": [], "none": [] } diff --git a/lib/rules/pagebreak-label-matches.js b/lib/rules/pagebreak-label-matches.js new file mode 100644 index 0000000000..b4ec36e195 --- /dev/null +++ b/lib/rules/pagebreak-label-matches.js @@ -0,0 +1,19 @@ +function pagebreakLabelMatches(node) { + // selector: '[*|type~="pagebreak"], [role~="doc-pagebreak"]', + return ( + (node.hasAttribute('role') && + node + .getAttribute('role') + .match(/\S+/g) + .includes('doc-pagebreak')) || + (node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type') && + node + .getAttributeNS('http://www.idpf.org/2007/ops', 'type') + .match(/\S+/g) + .includes('pagebreak')) + ); + + return false; +} + +export default pagebreakLabelMatches; diff --git a/lib/rules/pagebreak-label.json b/lib/rules/pagebreak-label.json new file mode 100644 index 0000000000..1ff408ec22 --- /dev/null +++ b/lib/rules/pagebreak-label.json @@ -0,0 +1,12 @@ +{ + "id": "pagebreak-label", + "matches": "pagebreak-label-matches", + "tags": ["cat.epub"], + "metadata": { + "description": "Ensure page markers have an accessible label", + "help": "Page markers should have an accessible label" + }, + "all": [], + "any": ["aria-label", "non-empty-title"], + "none": [] +} diff --git a/locales/da.json b/locales/da.json index 7a448b218a..93bf26c535 100644 --- a/locales/da.json +++ b/locales/da.json @@ -1,6 +1,13 @@ { "lang": "da", "rules": { + "epub-type-has-matching-role": { + "desc": "Sikrer at elementet har en ARIA rolle, som matcher 'epub:type'", + "help": "ARIA rolle skal være til stede og matche den angivne 'epub:type'" + }, + "pagebreak-label": { + "desc": "Sikrer at sidemarkører har en tilgængelig etiket ('label')" + }, "accesskeys": { "description": "", "help": "Værdien for attributten 'accesskey' skal være unik" @@ -323,6 +330,10 @@ } }, "checks": { + "matching-aria-role": { + "fail": "Elementet har ingen ARIA rolle, som matcher 'epub:type'", + "pass": "Elementet har en ARIA rolle, som matcher 'epub:type'" + }, "abstractrole": { "pass": "Abstrakte roller er ikke brugt", "fail": "Abstrakte roller bør ikke bruges" diff --git a/locales/de.json b/locales/de.json index 1fc1101019..1b694ba730 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,6 +1,13 @@ { "lang": "de", "rules": { + "epub-type-has-matching-role": { + "desc": "Ensure the element has an ARIA role matching its epub:type", + "help": "ARIA role should be used in addition to epub:type" + }, + "pagebreak-label": { + "desc": "Ensure page markers have an accessible label" + }, "accesskeys": { "description": "", "help": "Der Wert des accesskey-Attributes muss einzigartig sein." @@ -251,6 +258,10 @@ } }, "checks": { + "matching-aria-role": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + }, "abstractrole": { "pass": "", "fail": "Abstrakte ARIA-Rollen dürfen nicht direkt verwendet werden." diff --git a/locales/es.json b/locales/es.json index a9ccac15c5..e4a6e27775 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,6 +1,13 @@ { "lang": "es", "rules": { + "epub-type-has-matching-role": { + "desc": "Asegurarse de que el elemento tiene un rol ARIA que corresponda a su epub:type", + "help": "Debería usarse ARIA role, además de epub:type" + }, + "pagebreak-label": { + "desc": "Garantizar que los marcadores de página tienen una etiqueta accesible" + }, "accesskeys": { "description": "Garantiza que cada valor para el atributo accesskey es único", "help": "El valor del atributo accesskey debe ser único" @@ -319,6 +326,10 @@ } }, "checks": { + "matching-aria-role": { + "fail": "El elemento no tiene un rol ARIA que corresponda a su epub:type", + "pass": "El elemento tiene un rol ARIA que corresponde a su epub:type" + }, "abstractrole": { "pass": "No se usan 'abstract roles'", "fail": "Los 'abstract roles' no se pueden usar directamente" diff --git a/locales/eu.json b/locales/eu.json index 6f56607816..e71a882a63 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -1,6 +1,13 @@ { "lang": "eu", "rules": { + "epub-type-has-matching-role": { + "desc": "Ensure the element has an ARIA role matching its epub:type", + "help": "ARIA role should be used in addition to epub:type" + }, + "pagebreak-label": { + "desc": "Ensure page markers have an accessible label" + }, "accesskeys": { "description": "Accesskey atributurako balio bakoitza bakarra dela bermatzen du", "help": "Accesskey atributuaren balioak bakarra izan behar du" @@ -319,6 +326,10 @@ } }, "checks": { + "matching-aria-role": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + }, "abstractrole": { "pass": "Ez dira 'abstract rolak' erabiltzen", "fail": "'abstract rolak 'ezin dira zuzenean erabili" diff --git a/locales/fr.json b/locales/fr.json index 1073752acd..63d116a036 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,6 +1,13 @@ { "lang": "fr", "rules": { + "epub-type-has-matching-role": { + "desc": "Vérifie qu’un élément a un rôle ARIA correspondant à son epub:type", + "help": "Un rôle ARIA devrait être spécifié en plus de l’epub:type" + }, + "pagebreak-label": { + "desc": "Vérifie que les sauts de page ont un label accessible" + }, "accesskeys": { "description": "Vérifier que chaque valeur de l’attribut accesskey est unique", "help": "La valeur de l’attribut accesskey doit être unique" @@ -327,6 +334,10 @@ } }, "checks": { + "matching-aria-role": { + "fail": "L’élément n’a pas de rôle ARIA correspondant à son epub:type", + "pass": "L’élément a un rôle ARIA correspondant à son epub:type" + }, "abstractrole": { "pass": "Les rôles abstraits ne sont pas utilisés", "fail": "Les rôles abstraits ne peuvent pas être utilisés directement" diff --git a/locales/ja.json b/locales/ja.json index b70492b99a..c41d43c445 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1,6 +1,13 @@ { "lang": "ja", "rules": { + "epub-type-has-matching-role": { + "desc": "Ensure the element has an ARIA role matching its epub:type", + "help": "ARIA role should be used in addition to epub:type" + }, + "pagebreak-label": { + "desc": "Ensure page markers have an accessible label" + }, "accesskeys": { "description": "すべてのaccesskey属性値が一意であることを確認します", "help": "accesskey属性値は一意でなければなりません" @@ -339,6 +346,10 @@ } }, "checks": { + "matching-aria-role": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + }, "abstractrole": { "pass": "抽象ロールは使用されていません", "fail": { diff --git a/locales/ko.json b/locales/ko.json index ee904e5619..08c16a2fd2 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1,6 +1,13 @@ { "lang": "ko", "rules": { + "epub-type-has-matching-role": { + "desc": "Ensure the element has an ARIA role matching its epub:type", + "help": "ARIA role should be used in addition to epub:type" + }, + "pagebreak-label": { + "desc": "Ensure page markers have an accessible label" + }, "accesskeys": { "description": "모든 accesskey 속성 값이 고유한지 확인합니다.", "help": "accesskey 속성 값은 고유해야 합니다." @@ -319,6 +326,10 @@ } }, "checks": { + "matching-aria-role": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + }, "abstractrole": { "pass": "추상 역할은 직접 사용하지 않습니다.", "fail": "추상 역할은 직접 사용할 수 없습니다." diff --git a/locales/nl.json b/locales/nl.json index 407ef62618..fb02c8054d 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,6 +1,10 @@ { "lang": "nl", "checks": { + "matching-aria-role": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + }, "abstractrole": { "pass": "Er zijn geen abstracte rollen (role) gebruikt", "fail": "Gebruik geen abstracte rollen (role)" @@ -19,6 +23,13 @@ } }, "rules": { + "epub-type-has-matching-role": { + "desc": "Ensure the element has an ARIA role matching its epub:type", + "help": "ARIA role should be used in addition to epub:type" + }, + "pagebreak-label": { + "desc": "Ensure page markers have an accessible label" + }, "aria-required-attr": { "description": "Zorg dat elementen met ARIA rollen (role) de vereiste ARIA attributen hebben", "help": "Voorzien de vereiste ARIA attributen" diff --git a/locales/pt_BR.json b/locales/pt_BR.json index 940536274c..3d965ab0c4 100644 --- a/locales/pt_BR.json +++ b/locales/pt_BR.json @@ -1,6 +1,13 @@ { "lang": "pt_BR", "rules": { + "epub-type-has-matching-role": { + "desc": "Certifique-se de que o elemento tem um ARIA 'role' correspondente ao seu epub:type", + "help": "Um ARIA 'role' deve ser usado em conjunto com o epub:type" + }, + "pagebreak-label": { + "desc": "Certifique-se de que os marcadores de páginas tenham um rótulo acessível" + }, "accesskeys": { "description": "Certifique-se de que cada valor do atributo 'acesskey' é único", "help": "O valor do atributo 'accesskey' deve ser único" @@ -339,6 +346,10 @@ } }, "checks": { + "matching-aria-role": { + "fail": "O elemento não tem um ARIA 'role' correspondente ao seu epub:type", + "pass": "O elemento tem um ARIA 'role' correspondente ao seu epub:type" + }, "abstractrole": { "pass": "As funções abstratas não são utilizadas", "fail": { diff --git a/package-lock.json b/package-lock.json index 8d33094ec2..f237534c2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "axe-core", - "version": "4.1.3", + "name": "@daisy/axe-core-for-ace", + "version": "4.1.3-daisy.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 62b11a99d8..177a586270 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "axe-core", + "name": "@daisy/axe-core-for-ace", "description": "Accessibility engine for automated Web UI testing", "version": "4.1.3", "license": "MPL-2.0", @@ -50,6 +50,15 @@ ], "main": "axe.js", "typings": "axe.d.ts", + "files": [ + "LICENSE", + "README.md", + "CHANGELOG.md", + "locales/**/*", + "axe.js", + "axe.min.js", + "axe.d.ts" + ], "standard-version": { "scripts": { "postbump": "npm ci && npm run sri-update" @@ -64,6 +73,7 @@ "eslint": "eslint --color --format stylish '{lib,test,build,doc}/**/*.js' 'Gruntfile.js'", "test:headless": "node ./build/test/headless", "test": "tsc && grunt test", + "test-fast": "tsc && grunt test-fast", "test:examples": "node ./doc/examples/test-examples", "test:locales": "mocha test/test-locales.js", "test:rule-help-version": "mocha test/test-rule-help-version.js", @@ -150,5 +160,8 @@ "prettier --write", "git add" ] + }, + "publishConfig": { + "access": "public" } } diff --git a/test/commons/aria/get-role.js b/test/commons/aria/get-role.js index 6a82302dc6..e76a1bfb90 100644 --- a/test/commons/aria/get-role.js +++ b/test/commons/aria/get-role.js @@ -315,11 +315,29 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node, { dpub: true }), 'doc-chapter'); }); - it('does not returns DPUB roles with `dpub: false`', function() { + it('returns DPUB roles with `dpub: true` whilst ignoring implicit roles', function() { + var node = document.createElement('li'); + node.setAttribute('role', 'doc-chapter'); + flatTreeSetup(node); + assert.equal(aria.getRole(node, { dpub: true }), 'doc-chapter'); + }); + + it('returns non-DPUB implicit roles with `dpub: false/undefined`', function() { + var node = document.createElement('li'); + node.setAttribute('role', 'doc-chapter'); + var parentNode = document.createElement('div'); + parentNode.appendChild(node); + flatTreeSetup(parentNode); + assert.equal(aria.getRole(node, { dpub: false }), 'listitem'); + assert.equal(aria.getRole(node, { dpub: undefined }), 'listitem'); + }); + + it('does not returns DPUB roles with `dpub: false/undefined`', function() { var node = document.createElement('section'); node.setAttribute('role', 'doc-chapter'); flatTreeSetup(node); assert.isNull(aria.getRole(node, { dpub: false })); + assert.isNull(aria.getRole(node, { dpub: undefined })); }); }); @@ -379,6 +397,54 @@ describe('aria.getRole', function() { 'doc-chapter' ); }); + + it('respect the `dpub: false/undefined` option, whilst skipping the implicit roles due to non-abstract explicit role', function() { + var node = document.createElement('li'); + node.setAttribute('role', 'doc-chapter region'); + var parentNode = document.createElement('div'); + parentNode.appendChild(node); + flatTreeSetup(parentNode); + assert.equal( + aria.getRole(node, { fallback: true, dpub: false }), + 'region' + ); + assert.equal( + aria.getRole(node, { fallback: true, dpub: undefined }), + 'region' + ); + }); + + it('respect the `dpub: false/undefined` option, whilst ignoring the implicit roles and abstract explicit role', function() { + var node = document.createElement('li'); + node.setAttribute('role', 'doc-chapter section'); + var parentNode = document.createElement('div'); + parentNode.appendChild(node); + flatTreeSetup(parentNode); + assert.isNull( + aria.getRole(node, { noImplicit: true, fallback: true, dpub: false }) + ); + assert.isNull( + aria.getRole(node, { + noImplicit: true, + fallback: true, + dpub: undefined + }) + ); + }); + + it('respect the `dpub: false/undefined` option', function() { + var node = document.createElement('div'); + node.setAttribute('role', 'doc-chapter region'); + flatTreeSetup(node); + assert.equal( + aria.getRole(node, { fallback: true, dpub: false }), + 'region' + ); + assert.equal( + aria.getRole(node, { fallback: true, dpub: undefined }), + 'region' + ); + }); }); describe('noPresentational is honored', function() { diff --git a/test/core/utils/get-selector.js b/test/core/utils/get-selector.js index c98a52cdec..5e9daca0c5 100644 --- a/test/core/utils/get-selector.js +++ b/test/core/utils/get-selector.js @@ -38,7 +38,7 @@ function makeNonuniqueLongAttributes(fixture) { return node; } -describe('axe.utils.getSelector', function() { +describe('axe.utils.getSelector (core)', function() { 'use strict'; var fixture = document.getElementById('fixture'); diff --git a/test/integration/full/epub-type-has-matching-role/content__.xhtml b/test/integration/full/epub-type-has-matching-role/content__.xhtml new file mode 100644 index 0000000000..2ca87358c0 --- /dev/null +++ b/test/integration/full/epub-type-has-matching-role/content__.xhtml @@ -0,0 +1,65 @@ + + +
+Call me Ishmael.
+ +
+No main landmarks
+ + diff --git a/test/integration/full/landmark-no-duplicate-main/landmark-no-duplicate-main-pass1.html b/test/integration/full/landmark-no-duplicate-main/landmark-no-duplicate-main-pass1.html new file mode 100644 index 0000000000..7659fe192c --- /dev/null +++ b/test/integration/full/landmark-no-duplicate-main/landmark-no-duplicate-main-pass1.html @@ -0,0 +1,29 @@ + + + + + + + + + + + +No main landmarks
+ + + + + + + diff --git a/test/integration/full/landmark-no-duplicate-main/landmark-no-duplicate-main-pass1.js b/test/integration/full/landmark-no-duplicate-main/landmark-no-duplicate-main-pass1.js new file mode 100644 index 0000000000..cdbeb398b3 --- /dev/null +++ b/test/integration/full/landmark-no-duplicate-main/landmark-no-duplicate-main-pass1.js @@ -0,0 +1,37 @@ +describe('landmark-no-duplicate-main test pass 1', function() { + 'use strict'; + var results; + before(function(done) { + axe.testUtils.awaitNestedLoad(function() { + axe.run( + { runOnly: { type: 'rule', values: ['landmark-no-duplicate-main'] } }, + function(err, r) { + assert.isNull(err); + results = r; + done(); + } + ); + }); + }); + + describe('violations', function() { + it('should find 0', function() { + assert.lengthOf(results.violations, 0); + }); + }); + + describe('passes', function() { + it('should find 0', function() { + assert.lengthOf(results.passes, 0); + }); + }); + + it('should find 1 inapplicable', function() { + assert.lengthOf(results.inapplicable, 1); + assert.lengthOf(results.inapplicable[0].nodes, 0); + }); + + it('should find 0 incomplete', function() { + assert.lengthOf(results.incomplete, 0); + }); +}); diff --git a/test/integration/full/landmark-one-main/frames/level1-fail1.html b/test/integration/full/landmark-one-main/frames/level1-fail1.html new file mode 100644 index 0000000000..1ad1fa5aff --- /dev/null +++ b/test/integration/full/landmark-one-main/frames/level1-fail1.html @@ -0,0 +1,15 @@ + + + + + + + +Main landmark 1 created with main tag
+Main landmark 2 created with main role
+No main content here
+ + + + + + + diff --git a/test/integration/full/landmark-one-main/landmark-one-main-fail1.js b/test/integration/full/landmark-one-main/landmark-one-main-fail1.js new file mode 100644 index 0000000000..d31bc95b7b --- /dev/null +++ b/test/integration/full/landmark-one-main/landmark-one-main-fail1.js @@ -0,0 +1,47 @@ +describe('landmark-one-main test failure 1', function() { + 'use strict'; + var results; + before(function(done) { + axe.testUtils.awaitNestedLoad(function() { + axe.run( + { runOnly: { type: 'rule', values: ['landmark-one-main'] } }, + function(err, r) { + assert.isNull(err); + results = r; + done(); + } + ); + }); + }); + + describe('violations', function() { + it('should find 1', function() { + assert.lengthOf(results.violations[0].nodes, 1); + }); + + it('should find #frame1, #violation2', function() { + assert.deepEqual(results.violations[0].nodes[0].target, [ + '#frame1', + '#violation2' + ]); + }); + }); + + describe('passes', function() { + it('should find 1', function() { + assert.lengthOf(results.passes[0].nodes, 1); + }); + + it('should find #pass1', function() { + assert.deepEqual(results.passes[0].nodes[0].target, ['#pass1']); + }); + }); + + it('should find 0 inapplicable', function() { + assert.lengthOf(results.inapplicable, 0); + }); + + it('should find 0 incomplete', function() { + assert.lengthOf(results.incomplete, 0); + }); +}); diff --git a/test/integration/full/landmark-one-main/landmark-one-main-fail2.html b/test/integration/full/landmark-one-main/landmark-one-main-fail2.html new file mode 100644 index 0000000000..d76ea3fd5e --- /dev/null +++ b/test/integration/full/landmark-one-main/landmark-one-main-fail2.html @@ -0,0 +1,34 @@ + + + + + + + + + + + +Main landmark 2 created with main role
+Main landmark 2 created with main tag
+Call me Ishmael.
+ + + + + + + + + + + + diff --git a/test/integration/full/pagebreak-label/pagebreak-label.js b/test/integration/full/pagebreak-label/pagebreak-label.js new file mode 100644 index 0000000000..ccb320857b --- /dev/null +++ b/test/integration/full/pagebreak-label/pagebreak-label.js @@ -0,0 +1,55 @@ +describe('pagebreak-label test fail', function() { + // Checks that `epub:type` have matching ARIA roles + // Ensure the element has an ARIA role matching its epub:type + // ARIA role should be used in addition to epub:type + + 'use strict'; + var results; + before(function(done) { + axe.testUtils.awaitNestedLoad(function() { + // axe.configure({}); // DAISY ACE BREAKPOINT AXE CONFIGURE + + axe.run( + { runOnly: { type: 'rule', values: ['pagebreak-label'] } }, + function(err, r) { + assert.isNull(err); + results = r; + done(); + } + ); + }); + }); + + describe('violations', function() { + it('should find 1', function() { + // console.log(JSON.stringify(results.violations, null, 4)); + assert.lengthOf(results.violations, 1); + }); + + it('should find #p3 #p4', function() { + assert.deepEqual(results.violations[0].nodes[0].target, ['#p3']); + assert.deepEqual(results.violations[0].nodes[1].target, ['#p4']); + }); + }); + + describe('passes', function() { + it('should find 1', function() { + // console.log(JSON.stringify(results.passes, null, 4)); + assert.lengthOf(results.passes, 1); + }); + + it('should find section #p1 #p2', function() { + assert.deepEqual(results.passes[0].nodes[0].target, ['#p1']); + assert.deepEqual(results.passes[0].nodes[1].target, ['#p2']); + }); + }); + + it('should find 0 inapplicable', function() { + assert.lengthOf(results.inapplicable, 0); + // assert.lengthOf(results.inapplicable[0].nodes, 0); + }); + + it('should find 0 incomplete', function() { + assert.lengthOf(results.incomplete, 0); + }); +}); diff --git a/test/integration/rules/epub-type-has-matching-role/epub-type-has-matching-role.json b/test/integration/rules/epub-type-has-matching-role/epub-type-has-matching-role.json new file mode 100644 index 0000000000..5a7e09deb6 --- /dev/null +++ b/test/integration/rules/epub-type-has-matching-role/epub-type-has-matching-role.json @@ -0,0 +1,15 @@ +{ + "description": "epub-type-has-matching-role test", + "rule": "epub-type-has-matching-role", + "violations": [["#fail1"], ["#fail2"], ["#fail3"]], + "passes": [ + ["section"], + ["#pass2"], + ["#pass3"], + ["#pass4"], + ["#pass6"], + ["#pass5"], + ["#id-landmarks"], + ["#pass0"] + ] +} diff --git a/test/integration/rules/epub-type-has-matching-role/epub-type-has-matching-role.xhtml b/test/integration/rules/epub-type-has-matching-role/epub-type-has-matching-role.xhtml new file mode 100644 index 0000000000..fa168ccb99 --- /dev/null +++ b/test/integration/rules/epub-type-has-matching-role/epub-type-has-matching-role.xhtml @@ -0,0 +1,50 @@ + + + +Call me Ishmael.
+ +
+