-
Notifications
You must be signed in to change notification settings - Fork 783
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
945 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,4 +8,4 @@ | |
"fail": "aria-hidden=true should not be present on the document body" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* global axe */ | ||
|
||
/** | ||
* Return the document or document fragment (shadow DOM) | ||
* @method getRootNode | ||
* @memberof axe.utils | ||
* @instance | ||
* @param {Element} node | ||
* @returns {DocumentFragment|Document} | ||
*/ | ||
axe.utils.getRootNode = function getRootNode(node) { | ||
var doc = (node.getRootNode && node.getRootNode()) || document; // this is for backwards compatibility | ||
if (doc === node) { | ||
// disconnected node | ||
doc = document; | ||
} | ||
return doc; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
/** | ||
* Returns a then(able) queue of CSSStyleSheet(s) | ||
* @param {Object} ownerDocument document object to be inspected for stylesheets | ||
* @param {number} timeout on network request for stylesheet that need to be externally fetched | ||
* @param {Function} convertTextToStylesheetFn a utility function to generate a style sheet from text | ||
* @return {Object} queue | ||
* @private | ||
*/ | ||
function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) { | ||
/** | ||
* Make an axios get request to fetch a given resource and resolve | ||
* @method getExternalStylesheet | ||
* @private | ||
* @param {Object} param an object with properties to configure the external XHR | ||
* @property {Object} param.resolve resolve callback on queue | ||
* @property {Object} param.reject reject callback on queue | ||
* @property {String} param.url string representing the url of the resource to load | ||
* @property {Number} param.timeout timeout to about network call | ||
*/ | ||
function getExternalStylesheet({ resolve, reject, url }) { | ||
axe.imports | ||
.axios({ | ||
method: 'get', | ||
url, | ||
timeout | ||
}) | ||
.then(({ data }) => { | ||
const sheet = convertTextToStylesheetFn({ | ||
data, | ||
isExternal: true, | ||
shadowId | ||
}); | ||
resolve(sheet); | ||
}) | ||
.catch(reject); | ||
} | ||
|
||
const q = axe.utils.queue(); | ||
|
||
// iterate to decipher multi-level nested sheets if any (this is essential to retrieve styles from shadowDOM) | ||
Array.from(root.styleSheets).forEach(sheet => { | ||
// ignore disabled sheets | ||
if (sheet.disabled) { | ||
return; | ||
} | ||
// attempt to retrieve cssRules, or for external sheets make a XMLHttpRequest | ||
try { | ||
// accessing .cssRules throws for external (cross-domain) sheets, which is handled in the catch | ||
const cssRules = sheet.cssRules; | ||
// read all css rules in the sheet | ||
const rules = Array.from(cssRules); | ||
|
||
// filter rules that are included by way of @import or nested link | ||
const importRules = rules.filter(r => r.href); | ||
|
||
// if no import or nested link rules, with in these cssRules | ||
// return current sheet | ||
if (!importRules.length) { | ||
q.defer(resolve => | ||
resolve({ | ||
sheet, | ||
isExternal: false, | ||
shadowId | ||
}) | ||
); | ||
return; | ||
} | ||
|
||
// if any import rules exists, fetch via `href` which eventually constructs a sheet with results from resource | ||
importRules.forEach(rule => { | ||
q.defer((resolve, reject) => { | ||
getExternalStylesheet({ resolve, reject, url: rule.href }); | ||
}); | ||
}); | ||
|
||
// in the same sheet - get inline rules in <style> tag or in a CSSStyleSheet excluding @import or nested link | ||
const inlineRules = rules.filter(rule => !rule.href); | ||
|
||
// concat all cssText into a string for inline rules | ||
const inlineRulesCssText = inlineRules | ||
.reduce((out, rule) => { | ||
out.push(rule.cssText); | ||
return out; | ||
}, []) | ||
.join(); | ||
// create and return a sheet with inline rules | ||
q.defer(resolve => | ||
resolve( | ||
convertTextToStylesheetFn({ | ||
data: inlineRulesCssText, | ||
shadowId, | ||
isExternal: false | ||
}) | ||
) | ||
); | ||
} catch (e) { | ||
// if no href, do not attempt to make an XHR, but this is preventive check | ||
// NOTE: as further enhancements to resolve nested @imports are done, a decision to throw an Error if necessary here will be made. | ||
if (!sheet.href) { | ||
return; | ||
} | ||
// external sheet -> make an xhr and q the response | ||
q.defer((resolve, reject) => { | ||
getExternalStylesheet({ resolve, reject, url: sheet.href }); | ||
}); | ||
} | ||
}, []); | ||
// return | ||
return q; | ||
} | ||
|
||
/** | ||
* Returns an array of objects with root(document) | ||
* @param {Object} treeRoot the DOM tree to be inspected | ||
* @return {Array<Object>} array of objects, which each object containing a root (document) and an optional shadowId | ||
* @private | ||
*/ | ||
function getAllRootsInTree(tree) { | ||
let ids = []; | ||
const documents = axe.utils | ||
.querySelectorAllFilter(tree, '*', node => { | ||
if (ids.includes(node.shadowId)) { | ||
return false; | ||
} | ||
ids.push(node.shadowId); | ||
return true; | ||
}) | ||
.map(node => { | ||
return { | ||
shadowId: node.shadowId, | ||
root: axe.utils.getRootNode(node.actualNode) | ||
}; | ||
}); | ||
return documents; | ||
} | ||
|
||
/** | ||
* @method preloadCssom | ||
* @memberof axe.utils | ||
* @instance | ||
* @param {Object} object argument which is a composite object, with attributes timeout, treeRoot(optional), resolve & reject | ||
* @property {Number} timeout timeout for any network calls made | ||
* @property {Object} treeRoot the DOM tree to be inspected | ||
* @return {Object} a queue with results of cssom assets | ||
*/ | ||
axe.utils.preloadCssom = function preloadCssom({ | ||
timeout, | ||
treeRoot = axe._tree[0] | ||
}) { | ||
const roots = axe.utils.uniqueArray(getAllRootsInTree(treeRoot), []); | ||
const q = axe.utils.queue(); | ||
|
||
if (!roots.length) { | ||
return q; | ||
} | ||
|
||
const dynamicDoc = document.implementation.createHTMLDocument(); | ||
|
||
/** | ||
* Convert text content to CSSStyleSheet | ||
* @method convertTextToStylesheet | ||
* @private | ||
* @param {Object} param an object with properties to construct stylesheet | ||
* @property {String} param.data text content of the stylesheet | ||
* @property {Boolean} param.isExternal flag to notify if the resource was fetched from the network | ||
* @property {Object} param.doc implementation document to create style elements | ||
* @property {String} param.shadowId (Optional) shadowId if shadowDOM | ||
*/ | ||
function convertTextToStylesheet({ data, isExternal, shadowId }) { | ||
const style = dynamicDoc.createElement('style'); | ||
style.type = 'text/css'; | ||
style.appendChild(dynamicDoc.createTextNode(data)); | ||
dynamicDoc.head.appendChild(style); | ||
return { | ||
sheet: style.sheet, | ||
isExternal, | ||
shadowId | ||
}; | ||
} | ||
|
||
q.defer((resolve, reject) => { | ||
// as there can be multiple documents (root document, shadow document fragments, and frame documents) | ||
// reduce these into a queue | ||
roots | ||
.reduce((out, root) => { | ||
out.defer((resolve, reject) => { | ||
loadCssom(root, timeout, convertTextToStylesheet) | ||
.then(resolve) | ||
.catch(reject); | ||
}); | ||
return out; | ||
}, axe.utils.queue()) | ||
// await loading all such documents assets, and concat results into one object | ||
.then(assets => { | ||
resolve( | ||
assets.reduce((out, cssomSheets) => { | ||
return out.concat(cssomSheets); | ||
}, []) | ||
); | ||
}) | ||
.catch(reject); | ||
}); | ||
|
||
return q; | ||
}; |
Oops, something went wrong.