Skip to content

Commit

Permalink
fix: patch aXe to support namespaced element names
Browse files Browse the repository at this point in the history
aXe is creating CSS selectors for relevant elements in the checked
content. When faced with XML-namespaced element names (e.g. MathML
elements), it used the prefixed element name as a selector for the
element, which caused a syntax error when matching against this
selector.

To fix this, we copy part of aXe's code in our own file as a monkey
patch.
This must be removed when dequelabs/axe-core#563 is fixed.

Closes #57
  • Loading branch information
rdeltour committed Oct 9, 2017
1 parent d2ce78f commit 9e945b9
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 5 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"dist"
],
"dependencies": {
"axe-core": "^2.3.1",
"axe-core": "^2.4.2",
"env-paths": "^1.0.0",
"express": "^4.15.5",
"express-easy-zip": "^1.1.4",
Expand Down
7 changes: 7 additions & 0 deletions src/checker/checker-nightmare.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ if (!fs.existsSync(PATH_TO_H5O)) {
throw new Error('Can’t find h5o');
}

const PATH_TO_AXE_PATCH = path.join(__dirname, '../scripts/axe-patch.js');
if (!fs.existsSync(PATH_TO_AXE_PATCH)) {
winston.verbose(PATH_TO_AXE_PATCH);
throw new Error('Can’t find axe-patch script');
}

const PATH_TO_ACE_AXE = path.join(__dirname, '../scripts/ace-axe.js');
if (!fs.existsSync(PATH_TO_ACE_AXE)) {
winston.verbose(PATH_TO_ACE_AXE);
Expand Down Expand Up @@ -49,6 +55,7 @@ function checkSingle(spineItem, epub, nightmare) {
.goto(spineItem.url)
.inject('js', PATH_TO_AXE)
.inject('js', PATH_TO_H5O)
.inject('js', PATH_TO_AXE_PATCH)
.inject('js', PATH_TO_ACE_AXE)
.inject('js', PATH_TO_ACE_EXTRACTION)
.wait(50)
Expand Down
235 changes: 235 additions & 0 deletions src/scripts/axe-patch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/**
* Monkey-patch for aXe v2.4.2
*
* Copyright (c) 2017 Deque Systems, Inc.
*
* Your use of this Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* This entire copyright notice must appear in every copy of this file you
* distribute or in any file that contains substantial portions of this source
* code.
*/

(function axePatch(window) {
const axe = window.axe;
const escapeSelector = axe.utils.escapeSelector;

function isUncommonClassName (className) {
return ![
'focus', 'hover',
'hidden', 'visible',
'dirty', 'touched', 'valid', 'disable',
'enable', 'active', 'col-'
].find(str => className.includes(str));
}

function getDistinctClassList (elm) {
if (!elm.classList || elm.classList.length === 0) {
return [];
}

const siblings = elm.parentNode && Array.from(elm.parentNode.children || '') || [];
return siblings.reduce((classList, childElm) => {
if (elm === childElm) {
return classList;
} else {
return classList.filter(classItem => {
return !childElm.classList.contains(classItem);
});
}
}, Array.from(elm.classList).filter(isUncommonClassName));
}

const commonNodes = [
'div', 'span', 'p',
'b', 'i', 'u', 'strong', 'em',
'h2', 'h3'
];

function getNthChildString (elm, selector) {
const siblings = elm.parentNode && Array.from(elm.parentNode.children || '') || [];
const hasMatchingSiblings = siblings.find(sibling => (
sibling !== elm &&
axe.utils.matchesSelector(sibling, selector)
));
if (hasMatchingSiblings) {
const nthChild = 1 + siblings.indexOf(elm);
return ':nth-child(' + nthChild + ')';
} else {
return '';
}
}

const createSelector = {
// Get ID properties
getElmId (elm) {
if (!elm.getAttribute('id')) {
return;
}
const id = '#' + escapeSelector(elm.getAttribute('id') || '');
if (
// Don't include youtube's uid values, they change on reload
!id.match(/player_uid_/) &&
// Don't include IDs that occur more then once on the page
document.querySelectorAll(id).length === 1
) {
return id;
}
},
// Get custom element name
getCustomElm (elm, { isCustomElm, nodeName }) {
if (isCustomElm) {
return nodeName;
}
},

// Get ARIA role
getElmRoleProp (elm) {
if (elm.hasAttribute('role')) {
return '[role="' + escapeSelector(elm.getAttribute('role')) +'"]';
}
},
// Get uncommon node names
getUncommonElm (elm, { isCommonElm, isCustomElm, nodeName }) {
if (!isCommonElm && !isCustomElm) {
nodeName = escapeSelector(nodeName);
// Add [type] if nodeName is an input element
if (nodeName === 'input' && elm.hasAttribute('type')) {
nodeName += '[type="' + elm.type + '"]';
}
return nodeName;
}
},
// Has a name property, but no ID (Think input fields)
getElmNameProp (elm) {
if (!elm.hasAttribute('id') && elm.name) {
return '[name="' + escapeSelector(elm.name) + '"]';
}
},
// Get any distinct classes (as long as there aren't more then 3 of them)
getDistinctClass (elm, { distinctClassList }) {
if (distinctClassList.length > 0 && distinctClassList.length < 3) {
return '.' + distinctClassList.map(escapeSelector).join('.');
}
},
// Get a selector that uses src/href props
getFileRefProp (elm) {
let attr;
if (elm.hasAttribute('href')) {
attr = 'href';
} else if (elm.hasAttribute('src')) {
attr = 'src';
} else {
return;
}
const friendlyUriEnd = axe.utils.getFriendlyUriEnd(elm.getAttribute(attr));
if (friendlyUriEnd) {
return '[' + attr + '$="' + encodeURI(friendlyUriEnd) + '"]';
}
},
// Get common node names
getCommonName (elm, { nodeName, isCommonElm }) {
if (isCommonElm) {
return nodeName;
}
}
};

/**
* Get an array of features (as CSS selectors) that describe an element
*
* By going down the list of most to least prominent element features,
* we attempt to find those features that a dev is most likely to
* recognize the element by (IDs, aria roles, custom element names, etc.)
*/
function getElmFeatures (elm, featureCount) {
const nodeName = elm.localName.toLowerCase();
const classList = Array.from(elm.classList) || [];
// Collect some props we need to build the selector
const props = {
nodeName,
classList,
isCustomElm: nodeName.includes('-'),
isCommonElm: commonNodes.includes(nodeName),
distinctClassList: getDistinctClassList(elm)
};

return [
// go through feature selectors in order of priority
createSelector.getCustomElm,
createSelector.getElmRoleProp,
createSelector.getUncommonElm,
createSelector.getElmNameProp,
createSelector.getDistinctClass,
createSelector.getFileRefProp,
createSelector.getCommonName
].reduce((features, func) => {
// As long as we haven't met our count, keep looking for features
if (features.length === featureCount) {
return features;
}

const feature = func(elm, props);
if (feature) {
if (!feature[0].match(/[a-z]/)) {
features.push(feature);
} else {
features.unshift(feature);
}
}
return features;
}, []);
}

/**
* Gets a unique CSS selector
* @param {HTMLElement} node The element to get the selector for
* @return {String} Unique CSS selector for the node
*/
axe.utils.getSelector = function createUniqueSelector (elm, options = {}) {
//jshint maxstatements: 19
if (!elm) {
return '';
}
let selector, addParent;
let { isUnique = false } = options;
const idSelector = createSelector.getElmId(elm);
const {
featureCount = 2,
minDepth = 1,
toRoot = false,
childSelectors = []
} = options;

if (idSelector) {
selector = idSelector;
isUnique = true;

} else {
selector = getElmFeatures(elm, featureCount).join('');
selector += getNthChildString(elm, selector);
isUnique = options.isUnique || document.querySelectorAll(selector).length === 1;

// For the odd case that document doesn't have a unique selector
if (!isUnique && elm === document.documentElement) {
selector += ':root';
}
addParent = (minDepth !== 0 || !isUnique);
}

const selectorParts = [selector, ...childSelectors];

if (elm.parentElement && (toRoot || addParent)) {
return createUniqueSelector(elm.parentNode, {
toRoot, isUnique,
childSelectors: selectorParts,
featureCount: 1,
minDepth: minDepth -1
});
} else {
return selectorParts.join(' > ');
}
};
}(window));
7 changes: 6 additions & 1 deletion tests/__tests__/regression.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ test('issue #49: multiple \'dc:title\' elements', async () => {
expect(report['earl:result']['earl:outcome']).toEqual('pass');
});

test('issue #53: XXX', async () => {
test('issue #53: this.json.data.images.forEach is not a function', async () => {
const report = await ace('../data/issue-53');
expect(report['earl:result']['earl:outcome']).toEqual('pass');
});

test('issue #57: Failed to execute \'matches\' on \'Element\': \'m:annotation-xml\'', async () => {
const report = await ace('../data/issue-57');
expect(report['earl:result']['earl:outcome']).toEqual('pass');
});
17 changes: 17 additions & 0 deletions tests/data/issue-57/EPUB/content_001.xhtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en">
<head>
<title>Minimal EPUB</title>
</head>
<body>
<h1>Loomings</h1>
<p>Call me Ishmael.</p>
<m:math alttext="x" xmlns:m="http://www.w3.org/1998/Math/MathML">
<m:semantics>
<m:mi>x</m:mi>
<m:annotation-xml encoding="MathML-Content">
<m:ci>x</m:ci>
</m:annotation-xml>
</m:semantics>
</m:math>
</body>
</html>
12 changes: 12 additions & 0 deletions tests/data/issue-57/EPUB/nav.xhtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en">
<head>
<title>Minimal Nav</title>
</head>
<body>
<nav epub:type="toc">
<ol>
<li><a href="content_001.xhtml">content 001</a></li>
</ol>
</nav>
</body>
</html>
23 changes: 23 additions & 0 deletions tests/data/issue-57/EPUB/package.opf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" xml:lang="en" unique-identifier="uid">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title id="title">Minimal EPUB 3.0</dc:title>
<dc:language>en</dc:language>
<dc:identifier id="uid">NOID</dc:identifier>
<meta property="dcterms:modified">2017-01-01T00:00:01Z</meta>
<meta property="schema:accessibilityFeature">structuralNavigation</meta>
<meta property="schema:accessibilitySummary">everything OK!</meta>
<meta property="schema:accessibilityHazard">noFlashingHazard</meta>
<meta property="schema:accessibilityHazard">noSoundHazard</meta>
<meta property="schema:accessibilityHazard">noMotionSimulationHazard</meta>
<meta property="schema:accessMode">textual</meta>
<meta property="schema:accessModeSufficient">textual</meta>
</metadata>
<manifest>
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
<item id="content_001" href="content_001.xhtml" media-type="application/xhtml+xml" properties="mathml"/>
</manifest>
<spine>
<itemref idref="content_001" />
</spine>
</package>
6 changes: 6 additions & 0 deletions tests/data/issue-57/META-INF/container.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
1 change: 1 addition & 0 deletions tests/data/issue-57/mimetype
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
application/epub+zip
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,9 @@ aws4@^1.2.1, aws4@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"

axe-core@^2.3.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-2.4.1.tgz#55b6ceaa847cb1eaef5d559b6c41035dc3e07dbf"
axe-core@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-2.4.2.tgz#3156d914051ea32597e584071d9d653b66d1579b"

babel-cli@^6.26.0:
version "6.26.0"
Expand Down

0 comments on commit 9e945b9

Please sign in to comment.