Skip to content
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

Merged
merged 60 commits into from
Aug 8, 2018
Merged

feat(preload): cssom assets #958

merged 60 commits into from
Aug 8, 2018

Conversation

jeeyyy
Copy link
Contributor

@jeeyyy jeeyyy commented Jun 20, 2018

  • This PR tackles the functionality of preloading assets, currently only cssom.
  • Only the boilerplate code to preload is built, this has not been injected into current run methods or checks.
  • The philosophy is such that, once this configuration is set, the audit.run calls a axe.utils.preload queue to retrieve any external assets, and parses the results and passes it back to audit.run.

Closes issue:

Reviewer checks

Required fields, to be filled out by PR reviewer(s)

  • Follows the commit message policy, appropriate for next version
  • Has documentation updated, a DU ticket, or requires no documentation change
  • Includes new tests, or was unnecessary
  • Code is reviewed for security by: <Stephen

@jeeyyy jeeyyy changed the title [WIP] feat: preloading assets - cssom feat: cssom - preloading assets Jun 25, 2018
@jeeyyy jeeyyy changed the title feat: cssom - preloading assets feat(preload): preloading cssom assets Jun 26, 2018
function start() {
// Stop messing with my tests Mocha!
document.querySelector('#mocha h1').outerHTML =
'<h2>preload cssom integration test</h2>';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does that mean "otherwise there is no title generated"?

timeout
})
.then(({ data }) => {
const sheet = getSheetFromTextFn(data, true); //second argument acts as > isExternal - true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That second argument is useful. It's always external. More importantly, isExternal shouldn't be set in getSheetFromTextFn at all. It belongs in the same function that you set isExternal = false (this one).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solved!

const q = axe.utils.queue();

Array.from(ownerDocument.styleSheets).forEach(sheet => {
if (sheet.disabled) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add && sheet.cssRules.length <= 0, rather than add an if statement in the try/catch block. Currently the code looks like it's missing an "else".

Copy link
Contributor Author

@jeeyyy jeeyyy Jul 31, 2018

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I have refactored slightly

* @return {Object}
*/
function preloadCssom({ timeout, treeRoot = axe._tree[0] }) {
const documents = getDocumentsFromTreeRoot(treeRoot);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're not doing shadow DOM in this PR, than we need to replace this expression. Otherwise we're just committing buggy shadow DOM code to develop, rather than not having it supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just solved for shadowDOM, and it would make sense to fold all of that into the same PR. So yes, shadowDOM is in the same PR.

*/
function isValidPreloadObject(preload) {
if (typeof preload !== 'object') {
return preload;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preload=false shouldn't return false in isValidPreloadObject. This is misleading.

return axe.utils.preloadCssom(config);
}

function commonTestsForRootAndFrame(root) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing we forgot to test is if cross domain sheets loaded through @import can be accessed. I suspect they might not, so there's still some work in there I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, you are right. I have come up with a solution for the same. Which is why shadowDOM work is going into this PR as against a new one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note:
Basic one-level deep nested @import is tackled here.
Have created an issue to tackle further enhancements - #1054

@jeeyyy jeeyyy dismissed WilcoFiers’s stale review July 31, 2018 17:52

Changes done. Also supports shadowDOM


// if type is object - ensure an array of assets to load is specified
if (!isValidPreloadObject(options.preload)) {
throw new Error(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will never run because if it isn't, your shouldPreload function causes utils.preload to exit early.

// await loading all such documents assets, and concat results into one object
.then(assets => {
resolve(
assets.reduce((out, cssomSheets) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could have done return out.concat(cssomSheets).

This comment was marked as resolved.

if (typeof options.preload === 'boolean') {
return options.preload;
}
return isValidPreloadObject(options.preload);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest we run this first and throw if its invalid, rather than do it in getPreloadConfig.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is what we are doing.

const shouldPreload = axe.utils.shouldPreload(options);
if (!shouldPreload) {
	return q;
}

const preloadConfig = axe.utils.getPreloadConfig(options);

function start() {
// Stop messing with my tests Mocha!
document.querySelector('#mocha h1').outerHTML =
'<h2>preload cssom integration test</h2>';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't. I put this in the page-has-heading-one tests to make sure I can test that page without mocha getting in the way. That's the only place its needed.

}
);

shouldIt('should reject external stylesheets', function(done) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean, "should reject if external sheet fails to load"?


// there are external rules, best to filter non-external rules and create a new stylesheet to avoid duplication
const nonExternalRules = rules.filter(rule => !rule.href);
// concat all cssText into a string for non external rules
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a very hard time understanding what's happening here. As best I can tell you're trying to turn same origin sheets and style elements with import statements into one long stylesheet... that doesn't seem like a good idea to me. This means we lose all context of how why and where a stylesheet got included. It also ignores that @import can have its own media value, and that they can be recursive. We also shouldn't ignore @import on cross-origin sheets.

Please be sure to include tests that covers all of these things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Paired on the above, refactored as discussed.

* @param {Boolean} isExternal flag to specify if the stylesheet was fetched via xhr
* @param {String} shadowId (optional) string representing shadowId if style/ sheet was constructed from shadowDOM assets
*/
function getCssomSheet(sheet, isExternal, shadowId) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a 1-liner: const getCssomSheet = (sheet, isExternal, shadowId) => ({ sheet, isExternal, shadowId }).

Also, this function seems unnecessary.

* @private
*/
function getRootNode(node) {
var doc = (node.getRootNode && node.getRootNode()) || document; // this is for backwards compatibility

This comment was marked as resolved.

This comment was marked as resolved.

function isValidPreloadObject(preload) {
return (
typeof preload === 'object' &&
preload.hasOwnProperty('assets') &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove the .hasOwnProperty check, as Array.isArray(undefined) returns false.

* @param {Object} options run configuration options (or defaults) passed via axe.run
* @return {boolean}
*/
axe.utils.shouldPreload = function shouldPreload(options) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could simplify this:

axe.utils.shouldPreload = options => options && isValidPreloadObject(options.preload)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was my comment marked as resolved? You didn't change anything or respond.....?

Copy link
Contributor Author

@jeeyyy jeeyyy Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply because, I do not want to check if preload is a valid object until I am sure that it is an object. Hence a boolean check and an undefined check earlier.

Preload can take 3 values: true, false or { assets: ['assetKey'] } as defined in the docs.

Doing this options && isValidPreloadObject(options.preload) ignores and overrides the boolean values due to &&.

Scenarios (with your suggestion):

Preload Value options isValidPreloadObject output expected
options: undefined or null false false false false
options: { preload : true } true false false true (BOOM!)
options: { preload : false } true false false false
options: { preload : { assets: ['cssom'] } } true true true true

I am sure there is a clever way to shorten things further, but this keeps it easy to follow.
Prefer to keep this for that reason.

Appreciate the comments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed on Slack. Disregard my comment. This is good as-is!


if (!areRequestedAssetsValid) {
throw new Error(
`Requested assets, not supported by aXe.` +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC we're not capitalizing the "x" anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to avoid i18n concerns removed aXe.

function getAllRootsInTree(tree) {
/**
* Clone of axe.commons.dom.getRootNode
* Note:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like something that needs to be fixed first. Duplicating these functions will lead to maintenance nightmares.

This comment was marked as resolved.

@jeeyyy jeeyyy changed the title feat(preload): cssom assets [WIP] feat(preload): cssom assets Aug 5, 2018
@jeeyyy jeeyyy changed the title [WIP] feat(preload): cssom assets feat(preload): cssom assets Aug 6, 2018
@jeeyyy jeeyyy dismissed WilcoFiers’s stale review August 6, 2018 15:55

Updated. Review again.

}
return doc;
};
dom.getRootNode = axe.utils.getRootNode;
Copy link
Member

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:

let didWarn = false
dom.getRootNode = (...args) => {
  if (!didWarn) {
    console.warn('axe-core: dom.getRootNode has been deprecated; use utils.getRootNode instead.')
    didWarn = true
  }
  axe.utils.getRootNode(...args)
}

Copy link
Contributor Author

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, to axe.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


// concat all cssText into a string for inline rules
const inlineRulesCssText = inlineRules
.reduce((out, rule) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .reduce() seems to be performing the function of .map(). Should we refactor to use the intended native method here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am taking each CSSRule and appending its cssText in to an array - out. I intend to keep this as reduce, as is, because I believe there will be logic going into this iteration as I tackle for @import and media rules.

Appreciate the comment.

Copy link
Member

@stephenmathieson stephenmathieson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jeeyyy jeeyyy merged commit 5d6c1fa into develop Aug 8, 2018
@jeeyyy jeeyyy deleted the preload-cssom branch August 8, 2018 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants