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(utils): Update CSSOM for nested @import computation #1339

Merged
merged 46 commits into from
May 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
66651c9
refactor: update preload cssom to handle nested import style resolution
jeeyyy Jan 29, 2019
9b6235c
fix: update preload cssom to cater for various import scenarios and a…
jeeyyy Jan 31, 2019
9978dd1
test: update preload related tests
jeeyyy Jan 31, 2019
d0f7b11
Merge branch 'develop' into core-preload-cssom-updates
jeeyyy Jan 31, 2019
8a411e7
Merge branch 'develop' into core-preload-cssom-updates
jeeyyy Feb 6, 2019
b8a3c47
refactor: code reviiew comments
jeeyyy Feb 8, 2019
fdcd4de
lint: fix linting error
jeeyyy Feb 8, 2019
fb0efdc
Merge branch 'develop' into core-preload-cssom-updates
jeeyyy Feb 12, 2019
cac974a
refactor: preload Cssom
jeeyyy Feb 12, 2019
6462f7c
Merge branch 'develop' into refactor-preload-cssom
jeeyyy Feb 12, 2019
f8ba160
refactor: updattes
jeeyyy Feb 13, 2019
08f928e
refactor: updattes
jeeyyy Feb 13, 2019
f24929e
Merge branch 'develop' into refactor-preload-cssom
jeeyyy Feb 19, 2019
56591d2
Merge branch 'develop' into refactor-preload-cssom
jeeyyy Feb 20, 2019
6d9e6aa
docs: remote @instance as method is static
jeeyyy Feb 21, 2019
e23af78
Merge branch 'develop' into refactor-preload-cssom
jeeyyy Feb 25, 2019
154e369
Merge branch 'develop' into refactor-preload-cssom
jeeyyy Feb 26, 2019
196294e
Merge branch 'develop' into refactor-preload-cssom
jeeyyy Mar 5, 2019
84bf9ba
Merge branch 'develop' into refactor-preload-cssom
stephenmathieson Mar 5, 2019
3e65860
Merge branch 'develop' into refactor-preload-cssom
jeeyyy Mar 5, 2019
4f0baa9
chore: merge from ddevelop
jeeyyy Mar 6, 2019
213e569
refactor: preload cssom for import url resolution
jeeyyy Mar 11, 2019
736eb3d
Merge branch 'develop' into core-preload-cssom-updates
jeeyyy Mar 11, 2019
40699b6
fix: tests and update preload config computation
jeeyyy Mar 11, 2019
a0ab521
fix: tests and refacttor
jeeyyy Mar 11, 2019
0424d03
fix: tests and refacttor
jeeyyy Mar 11, 2019
188d964
fix: update preload fn and relevant tests
jeeyyy Mar 11, 2019
87b2780
fix: reutilise fetching cross origin stylesheet function
jeeyyy Mar 11, 2019
f3991c1
tfix: failing tests in safari
jeeyyy Mar 11, 2019
7f2a758
ftest: update timeout test
jeeyyy Mar 14, 2019
b2a8953
fix: apass title when invoking createHTMLDocument
jeeyyy Mar 18, 2019
f8d73cb
chore: merge from develop
jeeyyy Mar 22, 2019
8aa9e76
Merge branch 'develop' into core-preload-cssom-updates
jeeyyy Apr 3, 2019
bb47753
chore: merge from develop
jeeyyy Apr 16, 2019
337b480
Merge branch 'develop' into core-preload-cssom-updates
jeeyyy Apr 25, 2019
1327def
remove comment
jeeyyy Apr 25, 2019
0bf4042
test: fixes
jeeyyy Apr 25, 2019
ea05a29
test: update
jeeyyy Apr 25, 2019
afb8a8b
test: update
jeeyyy Apr 25, 2019
ba6d4c3
update tests
jeeyyy Apr 25, 2019
c395e87
update test
jeeyyy Apr 25, 2019
f228798
respect preload timeout on a higher level
jeeyyy Apr 30, 2019
356f13d
merge
jeeyyy Apr 30, 2019
5ed7b4d
update tests for ttimeout
jeeyyy May 1, 2019
5ca679f
update tests
jeeyyy May 1, 2019
e9f5f0e
update and add tests
jeeyyy May 13, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions lib/core/base/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,21 +436,20 @@ Audit.prototype.run = function(context, options, resolve, reject) {
const preloaderQueue = axe.utils.queue();
// defer preload if preload dependent rules exist
if (runLaterRules.length) {
preloaderQueue.defer((res, rej) => {
preloaderQueue.defer(resolve => {
// handle both success and fail of preload
// and resolve, to allow to run all checks
axe.utils
.preload(options)
.then(preloadResults => {
// pluck first item in results (because it is a queue - meh!) and resolve
const assets = preloadResults[0];
res(assets);
})
.then(assets => resolve(assets))
.catch(err => {
// resolve as undefined, to allow rule.run to continue
/**
* Note:
* we do not reject, to allow other (non-preload) rules to `run`
* -> instead we resolve as `undefined`
*/
console.warn(`Couldn't load preload assets: `, err);
const assets = undefined;
res(assets);
resolve(undefined);
});
});
}
Expand Down
12 changes: 10 additions & 2 deletions lib/core/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,16 @@
resultGroups: [],
resultGroupMap: {},
impact: Object.freeze(['minor', 'moderate', 'serious', 'critical']),
preloadAssets: Object.freeze(['cssom']), // overtime this array will grow with other preload asset types, this constant is to verify if a requested preload type by the user via the configuration is supported by axe.
preloadAssetsTimeout: 10000
preload: Object.freeze({
/**
* array of supported & preload(able) asset types.
*/
assets: ['cssom'],
/**
* timeout value when resolving preload(able) assets
*/
timeout: 10000
})
};

definitions.forEach(function(definition) {
Expand Down
51 changes: 51 additions & 0 deletions lib/core/utils/get-stylesheet-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Function which converts given text to `CSSStyleSheet`
* - used in `CSSOM` computation.
* - factory (closure) function, initialized with `document.implementation.createHTMLDocument()`, which uses DOM API for creating `style` elements.
*
* @method axe.utils.getStyleSheetFactory
* @memberof axe.utils
* @param {Object} dynamicDoc `document.implementation.createHTMLDocument()
* @param {Object} options an object with properties to construct stylesheet
* @property {String} options.data text content of the stylesheet
* @property {Boolean} options.isCrossOrigin flag to notify if the resource was fetched from the network
* @property {String} options.shadowId (Optional) shadowId if shadowDOM
* @property {Object} options.root implementation document to create style elements
* @property {String} options.priority a number indicating the loaded priority of CSS, to denote specificity of styles contained in the sheet.
* @returns {Function}
*/
axe.utils.getStyleSheetFactory = function getStyleSheetFactory(dynamicDoc) {
if (!dynamicDoc) {
throw new Error(
'axe.utils.getStyleSheetFactory should be invoked with an argument'
);
}

return options => {
const {
data,
isCrossOrigin = false,
shadowId,
root,
priority,
isLink = false
} = options;
const style = dynamicDoc.createElement('style');
if (isLink) {
// as creating a stylesheet as link will need to be awaited
// till `onload`, it is wise to convert link href to @import statement
const text = dynamicDoc.createTextNode(`@import "${data.href}"`);
style.appendChild(text);
} else {
style.appendChild(dynamicDoc.createTextNode(data));
}
dynamicDoc.head.appendChild(style);
return {
sheet: style.sheet,
isCrossOrigin,
shadowId,
root,
priority
};
};
};
53 changes: 53 additions & 0 deletions lib/core/utils/parse-crossorigin-stylesheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Parse cross-origin stylesheets
*
* @method parseCrossOriginStylesheet
* @memberof axe.utils
* @param {String} url url from which to fetch stylesheet
* @param {Object} options options object from `axe.utils.parseStylesheet`
* @param {Array<Number>} priority sheet priority
* @param {Array<String>} importedUrls urls of already imported stylesheets
* @param {Boolean} isCrossOrigin boolean denoting if a stylesheet is `cross-origin`
* @returns {Promise}
*/
axe.utils.parseCrossOriginStylesheet = function parseCrossOriginStylesheet(
url,
options,
priority,
importedUrls,
isCrossOrigin
) {
const axiosOptions = {
method: 'get',
url
};

/**
* Add `url` to `importedUrls`
*/
importedUrls.push(url);

/**
* Fetch `cross-origin stylesheet` via axios
*/
return axe.imports.axios(axiosOptions).then(({ data }) => {
const result = options.convertDataToStylesheet({
data,
isCrossOrigin,
priority,
root: options.rootNode,
shadowId: options.shadowId
});

/**
* Parse resolved stylesheet further for any `@import` styles
*/
return axe.utils.parseStylesheet(
result.sheet,
options,
priority,
importedUrls,
result.isCrossOrigin
);
});
};
96 changes: 96 additions & 0 deletions lib/core/utils/parse-sameorigin-stylesheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Parse non cross-origin stylesheets
*
* @method parseSameOriginStylesheet
* @memberof axe.utils
* @param {Object} sheet CSSStylesheet object
* @param {Object} options options object from `axe.utils.parseStylesheet`
* @param {Array<Number>} priority sheet priority
* @param {Array<String>} importedUrls urls of already imported stylesheets
* @param {Boolean} isCrossOrigin boolean denoting if a stylesheet is `cross-origin`
* @returns {Promise}
*/
axe.utils.parseSameOriginStylesheet = function parseSameOriginStylesheet(
sheet,
options,
priority,
importedUrls,
isCrossOrigin = false
) {
const rules = Array.from(sheet.cssRules);

if (!rules) {
return Promise.resolve();
}

/**
* reference -> https://developer.mozilla.org/en-US/docs/Web/API/CSSRule#Type_constants
*/
const cssImportRules = rules.filter(r => r.type === 3); // type === 3 -> CSSRule.IMPORT_RULE

/**
* when no `@import` rules in given sheet -> resolve the current `sheet` & exit
*/
if (!cssImportRules.length) {
// exit
return Promise.resolve({
isCrossOrigin,
priority,
root: options.rootNode,
shadowId: options.shadowId,
sheet
});
}

/**
* filter rules that are not already fetched
*/
const cssImportUrlsNotAlreadyImported = cssImportRules
// ensure rule has a href
.filter(rule => rule.href)
// extract href from object
.map(rule => rule.href)
// only href that are not already imported
.filter(url => !importedUrls.includes(url));

/**
* iterate `@import` rules and fetch styles
*/
const promises = cssImportUrlsNotAlreadyImported.map(
(importUrl, cssRuleIndex) => {
const newPriority = [...priority, cssRuleIndex];
const isCrossOriginRequest = /^https?:\/\/|^\/\//i.test(importUrl);

return axe.utils.parseCrossOriginStylesheet(
importUrl,
options,
newPriority,
importedUrls,
isCrossOriginRequest
);
}
);

const nonImportCSSRules = rules.filter(r => r.type !== 3);

// no further rules to process in this sheet
if (!nonImportCSSRules.length) {
return Promise.all(promises);
}

// convert all `nonImportCSSRules` style rules into `text` and chain

promises.push(
Promise.resolve(
options.convertDataToStylesheet({
data: nonImportCSSRules.map(rule => rule.cssText).join(),
isCrossOrigin,
priority,
root: options.rootNode,
shadowId: options.shadowId
})
)
);

return Promise.all(promises);
};
70 changes: 70 additions & 0 deletions lib/core/utils/parse-stylesheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Parse a given stylesheet
*
* @method parseStylesheet
* @memberof axe.utils
* @param {Object} sheet stylesheet to parse
* @param {Object} options configuration options object from `axe.utils.parseStylesheets`
* @param {Array<Number>} priority priority of stylesheet
* @param {Array<String>} importedUrls list of resolved `@import` urls
* @param {Boolean} isCrossOrigin boolean denoting if a stylesheet is `cross-origin`, passed for re-parsing `cross-origin` sheets
* @returns {Promise}
*/
axe.utils.parseStylesheet = function parseStylesheet(
sheet,
options,
priority,
importedUrls,
isCrossOrigin = false
) {
const isSameOrigin = isSameOriginStylesheet(sheet);
if (isSameOrigin) {
/**
* resolve `same-origin` stylesheet
*/
return axe.utils.parseSameOriginStylesheet(
sheet,
options,
priority,
importedUrls,
isCrossOrigin
);
}

/**
* resolve `cross-origin` stylesheet
*/
return axe.utils.parseCrossOriginStylesheet(
sheet.href,
options,
priority,
importedUrls,
true // -> isCrossOrigin
);
};

/**
* Check if a given stylesheet is from the `same-origin`
* Note:
* `sheet.cssRules` throws an error on `cross-origin` stylesheets
*
* @param {Object} sheet CSS stylesheet
* @returns {Boolean}
*/
function isSameOriginStylesheet(sheet) {
try {
/*eslint no-unused-vars: 0*/
const rules = sheet.cssRules;

/**
* Safari, does not throw an error when accessing cssRules property,
*/
if (!rules && sheet.href) {
return false;
}

return true;
} catch (e) {
return false;
}
}
Loading