-
Notifications
You must be signed in to change notification settings - Fork 779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(preload): cssom assets #958
Changes from 57 commits
321f4f1
33205ec
5e4f463
a3ea7c1
7119f2d
3782006
508c795
b78ac30
4e590a5
18b87e3
771bd88
eac06ac
cad3bb6
d9ab58f
858710f
678da39
f763dec
1b847ea
50229a0
8426fab
46ae47c
b03981b
0ba0ef9
ba00d6b
4fa9d6a
2074cc9
9f57cdb
f18db7f
1434751
b64309c
5ea88d5
28bdcdd
87820da
ae59586
15d71f1
3a10d63
222f05b
7f061ae
0fca2d1
2155df8
0923c36
29e3c61
9ecba9e
0b9d62d
f06701b
20e6f7d
06f6dfc
9e28b8a
483bcd8
a3ecee7
3436cc0
12008e7
8da721e
30a0819
027b3a1
567ef3c
b5c3f4f
a8c74ad
63cc5b4
247a7cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} | ||
} | ||
} | ||
} |
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; | ||
}; |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We cannot move .cssRules to here, as we need the catch to trigger for external stylesheets. Trying to read a .cssRules on external resource throws a SecurityError, which flows into the catch block. This is documented below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although I have refactored slightly |
||
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am taking each Appreciate the comment. |
||
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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since shadowId can be undefined, might be safer if we test that explicitly, so There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the thinking here, but node.shadowId is undefined in many cases, and this returns false correctly as expected. Adding a checked if null or undefined, is not essential here in my opinion. Can chat about this if necessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fine, but you owe me a beer when we get this reported. 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is cool by me 🍺 |
||
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could have done
This comment was marked as resolved.
Sorry, something went wrong. |
||
return out.concat(cssomSheets); | ||
}, []) | ||
); | ||
}) | ||
.catch(reject); | ||
}); | ||
|
||
return q; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add a warning here when this method is called? Just aliasing it doesn't seem to give users any reason not to use it.
Something like:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this.
Reason I did not do this is right away is, we are bypassing the function to
axe.utils
for the time being.The
jsdoc
@deprecated
notice is a flag for the dev's (us) to take some action on a later date.There are multiple places with in the code base still referring to
axe.commons.dom.getRootNode
, until we change all these references internally first, toaxe.utils.getRootNode
believe we should not warn on the console.And before all the internal changes come be made, this PR needs to be merged.
Also reckon a deprecation notice should dictate when an API is going to be deprecated, so we need to agree on that, something like
this function has been deprecated from v3.2.0, use newFunction
.Happy to add this, once we reach that consensus.
@marcysutton @WilcoFiers - thoughts?
Thanks @stephenmathieson