diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index bad1ebb8b0..c46f6ffa69 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -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); }); }); } diff --git a/lib/core/constants.js b/lib/core/constants.js index af9897ba86..02d40619b8 100644 --- a/lib/core/constants.js +++ b/lib/core/constants.js @@ -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) { diff --git a/lib/core/utils/get-stylesheet-factory.js b/lib/core/utils/get-stylesheet-factory.js new file mode 100644 index 0000000000..6c8da2103b --- /dev/null +++ b/lib/core/utils/get-stylesheet-factory.js @@ -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 + }; + }; +}; diff --git a/lib/core/utils/parse-crossorigin-stylesheet.js b/lib/core/utils/parse-crossorigin-stylesheet.js new file mode 100644 index 0000000000..80b70aae86 --- /dev/null +++ b/lib/core/utils/parse-crossorigin-stylesheet.js @@ -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} priority sheet priority + * @param {Array} 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 + ); + }); +}; diff --git a/lib/core/utils/parse-sameorigin-stylesheet.js b/lib/core/utils/parse-sameorigin-stylesheet.js new file mode 100644 index 0000000000..d02e0cd661 --- /dev/null +++ b/lib/core/utils/parse-sameorigin-stylesheet.js @@ -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} priority sheet priority + * @param {Array} 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); +}; diff --git a/lib/core/utils/parse-stylesheet.js b/lib/core/utils/parse-stylesheet.js new file mode 100644 index 0000000000..1e9ab4cf10 --- /dev/null +++ b/lib/core/utils/parse-stylesheet.js @@ -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} priority priority of stylesheet + * @param {Array} 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; + } +} diff --git a/lib/core/utils/preload-cssom.js b/lib/core/utils/preload-cssom.js index 0b06225546..b17db80499 100644 --- a/lib/core/utils/preload-cssom.js +++ b/lib/core/utils/preload-cssom.js @@ -7,44 +7,35 @@ /** * Given a rootNode - construct CSSOM * -> get all source nodes (document & document fragments) within given root node - * -> recursively call `loadCssom` to resolve styles + * -> recursively call `axe.utils.parseStylesheets` to resolve styles for each node * * @method preloadCssom * @memberof `axe.utils` - * - * @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 - * @returns {Object} `axe.utils.queue` with CSSOM assets + * @param {Object} options composite options object + * @property {Array} options.assets array of preloaded assets requested, eg: [`cssom`] + * @property {Number} options.timeout timeout + * @property {Object} options.treeRoot (optional) the DOM tree to be inspected + * @returns {Promise} */ -axe.utils.preloadCssom = function preloadCssom({ - timeout, - treeRoot = axe._tree[0] -}) { +axe.utils.preloadCssom = function preloadCssom({ treeRoot = axe._tree[0] }) { /** * get all `document` and `documentFragment` with in given `tree` */ const rootNodes = getAllRootNodesInTree(treeRoot); - const q = axe.utils.queue(); - if (!rootNodes.length) { - return q; + return Promise.resolve(); } - const dynamicDoc = document.implementation.createHTMLDocument('New Document'); - const convertDataToStylesheet = getStyleSheetFactory(dynamicDoc); + const dynamicDoc = document.implementation.createHTMLDocument( + 'Dynamic document for loading cssom' + ); - q.defer((resolve, reject) => { - getCssomForAllRootNodes(rootNodes, convertDataToStylesheet, timeout) - .then(assets => { - const cssom = processCssomAssets(assets); - resolve(cssom); - }) - .catch(reject); - }); + const convertDataToStylesheet = axe.utils.getStyleSheetFactory(dynamicDoc); - return q; + return getCssomForAllRootNodes(rootNodes, convertDataToStylesheet).then( + assets => flattenAssets(assets) + ); }; /** @@ -75,283 +66,89 @@ function getAllRootNodesInTree(tree) { } /** - * Convert text to CSSStyleSheet - * Is a factory (closure) function, initialized with `document.implementation.createHTMLDocument()` which surfaces DOM API for creating `style` elements. - * - * @param {Object} param `document.implementation.createHTMLDocument() - * @param {Object} arg an object with properties to construct stylesheet - * @property {String} arg.data text content of the stylesheet - * @property {Boolean} arg.isExternal flag to notify if the resource was fetched from the network - * @property {String} arg.shadowId (Optional) shadowId if shadowDOM - * @property {Object} arg.root implementation document to create style elements - * @property {String} arg.priority a number indicating the loaded priority of CSS, to denote specificity of styles contained in the sheet. - */ -const getStyleSheetFactory = dynamicDoc => ({ - data, - isExternal, - shadowId, - root, - priority, - isLink = false -}) => { - 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, - isExternal, - shadowId, - root, - priority - }; -}; - -/** - * Deferred function for CSSOM queue processing on all root nodes + * Process CSSOM on all root nodes * * @param {Array} rootNodes array of root nodes, where node is an enhanced `document` or `documentFragment` object returned from `getAllRootNodesInTree` * @param {Function} convertDataToStylesheet fn to convert given data to Stylesheet object - * @returns {Object} `axe.utils.queue` - */ -function getCssomForAllRootNodes(rootNodes, convertDataToStylesheet, timeout) { - const q = axe.utils.queue(); - - rootNodes.forEach(({ rootNode, shadowId }, index) => - q.defer((resolve, reject) => - loadCssom({ - rootNode, - shadowId, - timeout, - convertDataToStylesheet, - rootIndex: index + 1 - }) - .then(resolve) - .catch(reject) - ) - ); - - return q; -} - -/** - * Process results from `loadCssom` queues of all root nodes - * NOTE: - * using `axe.utils.queue` from various `loadCssom` paths, returns a nested array of arrays at various depths, - * hence the need to flatten arrays - * - * @param {Array} assets CSSOM assets for each root - * @returns {Object} CSSOM object + * @returns {Promise} */ -function processCssomAssets(nestedAssets) { - const result = []; - - nestedAssets.forEach(item => { - if (Array.isArray(item)) { - result.push(...processCssomAssets(item)); - } else { - result.push(item); - } - }); - - return result; -} - -/** - * Returns `axe.utils.queue` of CSSStyleSheet(s) for a given root node - * - * @param {Object} options configuration options - * @property {Object} options.rootNode document or document fragment - * @property {Number} options.rootIndex a number representing the index of the document or document fragment, used for priority computation - * @property {String} options.shadowId an id if undefined denotes that given root is a document fragment/ shadowDOM - * @property {Number} options.timeout abort duration for network request - * @property {Function} options.convertDataToStylesheet a utility function to generate a style sheet from given data (text) - * @return {Object} queue - */ -function loadCssom(options) { - const { rootIndex } = options; - - const q = axe.utils.queue(); - - const sheets = getStylesheetsOfRootNode(options); - if (!sheets) { - return q; - } - - sheets.forEach((sheet, sheetIndex) => { - const priority = [rootIndex, sheetIndex]; - try { - const deferredQ = parseNonCrossOriginStylesheet(sheet, options, priority); - q.defer(deferredQ); - } catch (e) { - // cross-origin stylesheet -> make an XHR and q the response - const deferredQ = parseCrossOriginStylesheet( - sheet.href, - options, - priority - ); - q.defer(deferredQ); +function getCssomForAllRootNodes(rootNodes, convertDataToStylesheet) { + const promises = []; + + rootNodes.forEach(({ rootNode, shadowId }, index) => { + const sheets = getStylesheetsOfRootNode( + rootNode, + shadowId, + convertDataToStylesheet + ); + if (!sheets) { + return Promise.all(promises); } - }); - return q; -} - -/** - * Parse non cross-origin stylesheets - * - * @param {Object} sheet CSSStylesheet object - * @param {Object} options `loadCssom` options - * @param {Array} priority sheet priority - */ -function parseNonCrossOriginStylesheet(sheet, options, priority) { - const q = axe.utils.queue(); - - /** - * `sheet.cssRules` throws an error on `cross-origin` stylesheets - */ - const cssRules = sheet.cssRules; - - const rules = Array.from(cssRules); - if (!rules) { - return q; - } - - /** - * 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) { - q.defer(resolve => - resolve({ - isExternal: false, - priority, - root: options.rootNode, - shadowId: options.shadowId, - sheet + const rootIndex = index + 1; + const parseOptions = { + rootNode, + shadowId, + convertDataToStylesheet, + rootIndex + }; + /** + * Note: + * `importedUrls` - keeps urls of already imported stylesheets, to prevent re-fetching + * eg: nested, cyclic or cross referenced `@import` urls + */ + const importedUrls = []; + + const p = Promise.all( + sheets.map((sheet, sheetIndex) => { + const priority = [rootIndex, sheetIndex]; + + return axe.utils.parseStylesheet( + sheet, + parseOptions, + priority, + importedUrls + ); }) ); - // exit - return q; - } - - /** - * iterate `@import` rules and fetch styles - */ - cssImportRules.forEach((importRule, cssRuleIndex) => - q.defer((resolve, reject) => { - const importUrl = importRule.href; - const newPriority = [...priority, cssRuleIndex]; - const axiosOptions = { - method: 'get', - url: importUrl, - timeout: options.timeout - }; - axe.imports - .axios(axiosOptions) - .then(({ data }) => - resolve( - options.convertDataToStylesheet({ - data, - isExternal: true, - priority: newPriority, - root: options.rootNode, - shadowId: options.shadowId - }) - ) - ) - .catch(reject); - }) - ); - - const nonImportCSSRules = rules.filter(r => r.type !== 3); - - // no further rules to process in this sheet - if (!nonImportCSSRules.length) { - return q; - } - - // convert all `nonImportCSSRules` style rules into `text` and defer into queue - q.defer(resolve => - resolve( - options.convertDataToStylesheet({ - data: nonImportCSSRules.map(rule => rule.cssText).join(), - isExternal: false, - priority, - root: options.rootNode, - shadowId: options.shadowId - }) - ) - ); + promises.push(p); + }); - return q; + return Promise.all(promises); } /** - * Parse cross-origin stylesheets + * Flatten CSSOM assets * - * @param {String} url url from which to fetch stylesheet - * @param {Object} options `loadCssom` options - * @param {Array} priority sheet priority + * @param {[Array]} assets nested assets (varying depth) + * @returns {Array} Array of CSSOM object */ -function parseCrossOriginStylesheet(url, options, priority) { - const q = axe.utils.queue(); - - if (!url) { - return q; - } - - const axiosOptions = { - method: 'get', - url, - timeout: options.timeout - }; - - q.defer((resolve, reject) => { - axe.imports - .axios(axiosOptions) - .then(({ data }) => - resolve( - options.convertDataToStylesheet({ - data, - isExternal: true, - priority, - root: options.rootNode, - shadowId: options.shadowId - }) - ) - ) - .catch(reject); - }); - - return q; +function flattenAssets(assets) { + return assets.reduce( + (acc, val) => + Array.isArray(val) ? acc.concat(flattenAssets(val)) : acc.concat(val), + [] + ); } /** * Get stylesheet(s) for root * - * @param {Object} options configuration options of `loadCssom` - * @returns an array of stylesheets + * @param {Object} options.rootNode `document` or `documentFragment` + * @param {String} options.shadowId an id if undefined denotes that given root is a document fragment/ shadowDOM + * @param {Function} options.convertDataToStylesheet a utility function to generate a style sheet from given data (text) + * @returns {Array} an array of stylesheets */ -function getStylesheetsOfRootNode(options) { - const { rootNode, shadowId } = options; +function getStylesheetsOfRootNode(rootNode, shadowId, convertDataToStylesheet) { let sheets; // nodeType === 11 -> DOCUMENT_FRAGMENT if (rootNode.nodeType === 11 && shadowId) { - sheets = getStylesheetsFromDocumentFragment(options); + sheets = getStylesheetsFromDocumentFragment( + rootNode, + convertDataToStylesheet + ); } else { sheets = getStylesheetsFromDocument(rootNode); } @@ -362,11 +159,11 @@ function getStylesheetsOfRootNode(options) { /** * Get stylesheets from `documentFragment` * - * @param {Object} options configuration options of `loadCssom` + * @property {Object} options.rootNode `documentFragment` + * @property {Function} options.convertDataToStylesheet a utility function to generate a stylesheet from given data * @returns {Array} */ -function getStylesheetsFromDocumentFragment(options) { - const { rootNode, convertDataToStylesheet } = options; +function getStylesheetsFromDocumentFragment(rootNode, convertDataToStylesheet) { return ( Array.from(rootNode.children) .filter(filerStyleAndLinkAttributesInDocumentFragment) diff --git a/lib/core/utils/preload.js b/lib/core/utils/preload.js index 38cee08077..945e40f788 100644 --- a/lib/core/utils/preload.js +++ b/lib/core/utils/preload.js @@ -26,13 +26,13 @@ axe.utils.shouldPreload = function shouldPreload(options) { /** * Constructs a configuration object representing the preload requested assets & timeout * @param {Object} options run configuration options (or defaults) passed via axe.run - * @return {Object} + * @return {Object} configuration */ axe.utils.getPreloadConfig = function getPreloadConfig(options) { - // default fallback configuration + const { assets, timeout } = axe.constants.preload; const config = { - assets: axe.constants.preloadAssets, - timeout: axe.constants.preloadAssetsTimeout + assets, + timeout }; // if no `preload` is configured via `options` - return default config @@ -47,13 +47,13 @@ axe.utils.getPreloadConfig = function getPreloadConfig(options) { // check if requested assets to preload are valid items const areRequestedAssetsValid = options.preload.assets.every(a => - axe.constants.preloadAssets.includes(a.toLowerCase()) + assets.includes(a.toLowerCase()) ); if (!areRequestedAssetsValid) { throw new Error( `Requested assets, not supported. ` + - `Supported assets are: ${axe.constants.preloadAssets.join(', ')}.` + `Supported assets are: ${assets.join(', ')}.` ); } @@ -74,36 +74,62 @@ axe.utils.getPreloadConfig = function getPreloadConfig(options) { }; /** - * Returns a then(able) queue with results of all requested preload(able) assets. Eg: ['cssom']. - * If preload is set to false, returns an empty queue. + * Returns a Promise with results of all requested preload(able) assets. eg: ['cssom']. + * * @param {Object} options run configuration options (or defaults) passed via axe.run - * @return {Object} queue + * @return {Object} Promise */ axe.utils.preload = function preload(options) { const preloadFunctionsMap = { cssom: axe.utils.preloadCssom }; - const q = axe.utils.queue(); - const shouldPreload = axe.utils.shouldPreload(options); if (!shouldPreload) { - return q; + return Promise.resolve(); } - const preloadConfig = axe.utils.getPreloadConfig(options); + return new Promise((resolve, reject) => { + const { assets, timeout } = axe.utils.getPreloadConfig(options); + + /** + * Start `timeout` timer for preloading assets + * -> reject if allowed time expires. + */ + setTimeout(() => reject(`Preload assets timed out.`), timeout); + + /** + * Fetch requested `assets` + */ - preloadConfig.assets.forEach(asset => { - q.defer((resolve, reject) => { - preloadFunctionsMap[asset](preloadConfig) - .then(results => { - resolve({ - [asset]: results[0] - }); + Promise.all( + assets.map(asset => + preloadFunctionsMap[asset](options).then(results => { + return { + [asset]: results + }; }) - .catch(reject); + ) + ).then(results => { + /** + * Combine array of results into an object map + * + * From -> + * [{cssom: [...], aom: [...]}] + * To -> + * { + * cssom: [...] + * aom: [...] + * } + */ + const preloadAssets = results.reduce((out, result) => { + return { + ...out, + ...result + }; + }, {}); + + resolve(preloadAssets); }); }); - - return q; }; diff --git a/test/core/base/audit.js b/test/core/base/audit.js index 4f31701fc6..26029752b0 100644 --- a/test/core/base/audit.js +++ b/test/core/base/audit.js @@ -576,312 +576,319 @@ describe('Audit', function() { ); }); - it('should run rules (that do not need preload) and preload assets simultaneously', function(done) { - // overriding and resolving both check and preload with a delay - // but the invoked timestamp should ensure that they were invoked alomost immediately - - fixture.innerHTML = '
'; - - var runStartTime = new Date(); - var preloadInvokedTime = new Date(); - var noPreloadCheckedInvokedTime = new Date(); - var noPreloadRuleCheckEvaluateInvoked = false; - var preloadOverrideInvoked = false; - - // override preload method - axe.utils.preload = function(options) { - preloadInvokedTime = new Date(); - preloadOverrideInvoked = true; - - var q = axe.utils.queue(); - - q.defer(function(res, rej) { - // a delayed deferred fn, so the q resolves late - setTimeout(function() { - res(true); - }, 2000); + // PhantomJs does not have Promise support + (window.PHANTOMJS ? xit : it)( + 'should run rules (that do not need preload) and preload assets simultaneously', + function(done) { + /** + * Note: + * overriding and resolving both check and preload with a delay, + * but the invoked timestamp should ensure that they were invoked almost immediately + */ + + fixture.innerHTML = '
'; + + var runStartTime = new Date(); + var preloadInvokedTime = new Date(); + var noPreloadCheckedInvokedTime = new Date(); + var noPreloadRuleCheckEvaluateInvoked = false; + var preloadOverrideInvoked = false; + + // override preload method + axe.utils.preload = function(options) { + preloadInvokedTime = new Date(); + preloadOverrideInvoked = true; + + return new Promise(function(res, rej) { + setTimeout(function() { + res(true); + }, 2000); + }); + }; + + var audit = new Audit(); + // add a rule and check that does not need preload + audit.addRule({ + id: 'no-preload', + selector: 'div#div1', + any: ['no-preload-check'], + preload: false + }); + audit.addCheck({ + id: 'no-preload-check', + evaluate: function(node, options, vNode, context) { + noPreloadCheckedInvokedTime = new Date(); + noPreloadRuleCheckEvaluateInvoked = true; + var ready = this.async(); + setTimeout(function() { + ready(true); + }, 1000); + } }); - return q; - }; - - var audit = new Audit(); - // add a rule and check that does not need preload - audit.addRule({ - id: 'no-preload', - selector: 'div#div1', - any: ['no-preload-check'], - preload: false - }); - audit.addCheck({ - id: 'no-preload-check', - evaluate: function(node, options, vNode, context) { - noPreloadCheckedInvokedTime = new Date(); - noPreloadRuleCheckEvaluateInvoked = true; - var ready = this.async(); - setTimeout(function() { - ready(true); - }, 1000); - } - }); - - // add a rule which needs preload - audit.addRule({ - id: 'yes-preload', - selector: 'div#div2', - preload: true - }); - - var preloadOptions = { - preload: { - assets: ['cssom'] - } - }; - - var allowedDiff = 50; - - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(results) { - assert.isDefined(results); - // assert that check was invoked for rule(s) - assert.isTrue(noPreloadRuleCheckEvaluateInvoked); - // assert preload was invoked - assert.isTrue(preloadOverrideInvoked); - // assert that time diff(s) - // assert that run check invoked immediately - // choosing 5ms as an arbitary number - assert.isBelow( - noPreloadCheckedInvokedTime - runStartTime, - allowedDiff - ); - // assert that preload invoked immediately - assert.isBelow(preloadInvokedTime - runStartTime, allowedDiff); - // ensure cache is clear - assert.isTrue(typeof axe._selectCache === 'undefined'); - // done - done(); - }, - noop - ); - }); + // add a rule which needs preload + audit.addRule({ + id: 'yes-preload', + selector: 'div#div2', + preload: true + }); - it('should pass assets from preload to rule check that needs assets as context', function(done) { - fixture.innerHTML = '
'; + var preloadOptions = { + preload: { + assets: ['cssom'] + } + }; - var yesPreloadRuleCheckEvaluateInvoked = false; - var preloadOverrideInvoked = false; + var allowedDiff = 50; - var preloadData = { - data: 'you got it!' - }; - // override preload method - axe.utils.preload = function(options) { - preloadOverrideInvoked = true; - var q = axe.utils.queue(); - q.defer(function(resolve, reject) { - resolve({ + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(results) { + assert.isDefined(results); + // assert that check was invoked for rule(s) + assert.isTrue(noPreloadRuleCheckEvaluateInvoked); + // assert preload was invoked + assert.isTrue(preloadOverrideInvoked); + // assert that time diff(s) + // assert that run check invoked immediately + // choosing 5ms as an arbitary number + assert.isBelow( + noPreloadCheckedInvokedTime - runStartTime, + allowedDiff + ); + // assert that preload invoked immediately + assert.isBelow(preloadInvokedTime - runStartTime, allowedDiff); + // ensure cache is clear + assert.isTrue(typeof axe._selectCache === 'undefined'); + // done + done(); + }, + noop + ); + } + ); + + // PhantomJs does not have Promise support + (window.PHANTOMJS ? xit : it)( + 'should pass assets from preload to rule check that needs assets as context', + function(done) { + fixture.innerHTML = '
'; + + var yesPreloadRuleCheckEvaluateInvoked = false; + var preloadOverrideInvoked = false; + + var preloadData = { + data: 'you got it!' + }; + // override preload method + axe.utils.preload = function(options) { + preloadOverrideInvoked = true; + return Promise.resolve({ cssom: preloadData }); + }; + + var audit = new Audit(); + // add a rule and check that does not need preload + audit.addRule({ + id: 'no-preload', + selector: 'div#div1', + preload: false }); - return q; - }; - - var audit = new Audit(); - // add a rule and check that does not need preload - audit.addRule({ - id: 'no-preload', - selector: 'div#div1', - preload: false - }); - // add a rule which needs preload - audit.addRule({ - id: 'yes-preload', - selector: 'div#div2', - preload: true, - any: ['yes-preload-check'] - }); - audit.addCheck({ - id: 'yes-preload-check', - evaluate: function(node, options, vNode, context) { - yesPreloadRuleCheckEvaluateInvoked = true; - this.data(context); - return true; - } - }); - - var preloadOptions = { - preload: { - assets: ['cssom'] - } - }; - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(results) { - assert.isDefined(results); - // assert that check was invoked for rule(s) - assert.isTrue(yesPreloadRuleCheckEvaluateInvoked); - // assert preload was invoked - assert.isTrue(preloadOverrideInvoked); - - // assert preload data that was passed to check - var ruleResult = results.filter(function(r) { - return (r.id = 'yes-preload' && r.nodes.length > 0); - })[0]; - var checkResult = ruleResult.nodes[0].any[0]; - assert.isDefined(checkResult.data); - assert.property(checkResult.data, ['cssom']); - assert.deepEqual(checkResult.data.cssom, preloadData); - // ensure cache is clear - assert.isTrue(typeof axe._selectCache === 'undefined'); - // done - done(); - }, - noop - ); - }); - - it('should continue to run rules and return result when preload is rejected', function(done) { - fixture.innerHTML = '
'; - - var preloadOverrideInvoked = false; - var preloadNeededCheckInvoked = false; - var rejectionMsg = 'Boom! Things went terribly wrong!'; - - // override preload method - axe.utils.preload = function(options) { - preloadOverrideInvoked = true; - var q = axe.utils.queue(); - q.defer(function(resolve, reject) { - reject(rejectionMsg); + // add a rule which needs preload + audit.addRule({ + id: 'yes-preload', + selector: 'div#div2', + preload: true, + any: ['yes-preload-check'] + }); + audit.addCheck({ + id: 'yes-preload-check', + evaluate: function(node, options, vNode, context) { + yesPreloadRuleCheckEvaluateInvoked = true; + this.data(context); + return true; + } }); - return q; - }; - - var audit = new Audit(); - // add a rule and check that does not need preload - audit.addRule({ - id: 'no-preload', - selector: 'div#div1', - preload: false - }); - // add a rule which needs preload - audit.addRule({ - id: 'yes-preload', - selector: 'div#div2', - preload: true, - any: ['yes-preload-check'] - }); - audit.addCheck({ - id: 'yes-preload-check', - evaluate: function(node, options, vNode, context) { - preloadNeededCheckInvoked = true; - this.data(context); - return true; - } - }); - - var preloadOptions = { - preload: { - assets: ['cssom'] - } - }; - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(results) { - assert.isDefined(results); - // assert preload was invoked - assert.isTrue(preloadOverrideInvoked); - - // assert that both rules ran, although preload failed - assert.lengthOf(results, 2); - - // assert that because preload failed - // cssom was not populated on context of repective check - assert.isTrue(preloadNeededCheckInvoked); - var ruleResult = results.filter(function(r) { - return (r.id = 'yes-preload' && r.nodes.length > 0); - })[0]; - var checkResult = ruleResult.nodes[0].any[0]; - assert.isDefined(checkResult.data); - assert.notProperty(checkResult.data, ['cssom']); - // done - done(); - }, - noop - ); - }); - - it('should continue to run rules and return result when axios time(s)out and rejects preload', function(done) { - fixture.innerHTML = '
'; - // there is no stubbing here, - // the actual axios call is invoked, and timedout immediately as timeout is set to 0.1 + var preloadOptions = { + preload: { + assets: ['cssom'] + } + }; + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(results) { + assert.isDefined(results); + // assert that check was invoked for rule(s) + assert.isTrue(yesPreloadRuleCheckEvaluateInvoked); + // assert preload was invoked + assert.isTrue(preloadOverrideInvoked); + + // assert preload data that was passed to check + var ruleResult = results.filter(function(r) { + return (r.id = 'yes-preload' && r.nodes.length > 0); + })[0]; + var checkResult = ruleResult.nodes[0].any[0]; + assert.isDefined(checkResult.data); + assert.property(checkResult.data, ['cssom']); + assert.deepEqual(checkResult.data.cssom, preloadData); + // ensure cache is clear + assert.isTrue(typeof axe._selectCache === 'undefined'); + // done + done(); + }, + noop + ); + } + ); + + // PhantomJs does not have Promise support + (window.PHANTOMJS ? xit : it)( + 'should continue to run rules and return result when preload is rejected', + function(done) { + fixture.innerHTML = '
'; + + var preloadOverrideInvoked = false; + var preloadNeededCheckInvoked = false; + var rejectionMsg = + 'Boom! Things went terribly wrong! (But this was intended in this test)'; + + // override preload method + axe.utils.preload = function(options) { + preloadOverrideInvoked = true; + return Promise.reject(rejectionMsg); + }; + + var audit = new Audit(); + // add a rule and check that does not need preload + audit.addRule({ + id: 'no-preload', + selector: 'div#div1', + preload: false + }); + // add a rule which needs preload + audit.addRule({ + id: 'yes-preload', + selector: 'div#div2', + preload: true, + any: ['yes-preload-check'] + }); + audit.addCheck({ + id: 'yes-preload-check', + evaluate: function(node, options, vNode, context) { + preloadNeededCheckInvoked = true; + this.data(context); + return true; + } + }); - var preloadNeededCheckInvoked = false; - var audit = new Audit(); - // add a rule and check that does not need preload - audit.addRule({ - id: 'no-preload', - selector: 'div#div1', - preload: false - }); - // add a rule which needs preload - audit.addRule({ - id: 'yes-preload', - selector: 'div#div2', - preload: true, - any: ['yes-preload-check'] - }); - audit.addCheck({ - id: 'yes-preload-check', - evaluate: function(node, options, vNode, context) { - preloadNeededCheckInvoked = true; - this.data(context); - return true; - } - }); + var preloadOptions = { + preload: { + assets: ['cssom'] + } + }; + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(results) { + assert.isDefined(results); + // assert preload was invoked + assert.isTrue(preloadOverrideInvoked); + + // assert that both rules ran, although preload failed + assert.lengthOf(results, 2); + + // assert that because preload failed + // cssom was not populated on context of repective check + assert.isTrue(preloadNeededCheckInvoked); + var ruleResult = results.filter(function(r) { + return (r.id = 'yes-preload' && r.nodes.length > 0); + })[0]; + var checkResult = ruleResult.nodes[0].any[0]; + assert.isDefined(checkResult.data); + assert.notProperty(checkResult.data, ['cssom']); + // done + done(); + }, + noop + ); + } + ); + + // PhantomJs does not have Promise support + (window.PHANTOMJS ? xit : it)( + 'should continue to run rules and return result when axios time(s)out and rejects preload', + function(done) { + fixture.innerHTML = '
'; + + // there is no stubbing here, + // the actual axios call is invoked, and timedout immediately as timeout is set to 0.1 + + var preloadNeededCheckInvoked = false; + var audit = new Audit(); + // add a rule and check that does not need preload + audit.addRule({ + id: 'no-preload', + selector: 'div#div1', + preload: false + }); + // add a rule which needs preload + audit.addRule({ + id: 'yes-preload', + selector: 'div#div2', + preload: true, + any: ['yes-preload-check'] + }); + audit.addCheck({ + id: 'yes-preload-check', + evaluate: function(node, options, vNode, context) { + preloadNeededCheckInvoked = true; + this.data(context); + return true; + } + }); - var preloadOptions = { - preload: { - assets: ['cssom'], - timeout: 0.1 - } - }; - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(results) { - assert.isDefined(results); - // assert that both rules ran, although preload failed - assert.lengthOf(results, 2); - - // assert that because preload failed - // cssom was not populated on context of repective check - assert.isTrue(preloadNeededCheckInvoked); - var ruleResult = results.filter(function(r) { - return (r.id = 'yes-preload' && r.nodes.length > 0); - })[0]; - var checkResult = ruleResult.nodes[0].any[0]; - assert.isDefined(checkResult.data); - assert.notProperty(checkResult.data, ['cssom']); - // done - done(); - }, - noop - ); - }); + var preloadOptions = { + preload: { + assets: ['cssom'], + timeout: 0.1 + } + }; + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(results) { + assert.isDefined(results); + // assert that both rules ran, although preload failed + assert.lengthOf(results, 2); + + // assert that because preload failed + // cssom was not populated on context of repective check + assert.isTrue(preloadNeededCheckInvoked); + var ruleResult = results.filter(function(r) { + return (r.id = 'yes-preload' && r.nodes.length > 0); + })[0]; + var checkResult = ruleResult.nodes[0].any[0]; + assert.isDefined(checkResult.data); + assert.notProperty(checkResult.data, ['cssom']); + // done + done(); + }, + noop + ); + } + ); it('should assign an empty array to axe._selectCache', function(done) { var saved = axe.utils.ruleShouldRun; diff --git a/test/core/utils/get-stylesheet-factory.js b/test/core/utils/get-stylesheet-factory.js new file mode 100644 index 0000000000..025b18ba09 --- /dev/null +++ b/test/core/utils/get-stylesheet-factory.js @@ -0,0 +1,42 @@ +describe('axe.utils.getStyleSheetFactory', function() { + 'use strict'; + + var dynamicDoc = document.implementation.createHTMLDocument( + 'Dynamic document for testing axe.utils.getStyleSheetFactory' + ); + + it('throws if there is no argument of dynamicDocument', function() { + assert.throws(function() { + axe.utils.getStyleSheetFactory(); + }); + }); + + it('returns a function when passed argument of dynamicDocument', function() { + const actual = axe.utils.getStyleSheetFactory(dynamicDoc); + assert.isFunction(actual); + }); + + it('returns a CSSOM stylesheet, when invoked with data (text)', function() { + const stylesheetFactory = axe.utils.getStyleSheetFactory(dynamicDoc); + const actual = stylesheetFactory({ + data: `.someStyle{background-color:red;}`, + root: document, + priority: [1, 0] + }); + + assert.isDefined(actual); + assert.hasAllKeys(actual, [ + 'sheet', + 'isCrossOrigin', + 'shadowId', + 'root', + 'priority' + ]); + assert.deepEqual(actual.priority, [1, 0]); + axe.testUtils.assertStylesheet( + actual.sheet, + '.someStyle', + '.someStyle{background-color:red;}' + ); + }); +}); diff --git a/test/core/utils/parse-crossorigin-stylesheet.js b/test/core/utils/parse-crossorigin-stylesheet.js new file mode 100644 index 0000000000..a8a4b5718e --- /dev/null +++ b/test/core/utils/parse-crossorigin-stylesheet.js @@ -0,0 +1,94 @@ +describe('axe.utils.parseCrossOriginStylesheet', function() { + 'use strict'; + + var dynamicDoc; + var convertDataToStylesheet; + + beforeEach(function() { + dynamicDoc = document.implementation.createHTMLDocument( + 'Dynamic document for testing axe.utils.parseCrossOriginStylesheet' + ); + convertDataToStylesheet = axe.utils.getStyleSheetFactory(dynamicDoc); + }); + + afterEach(function() { + dynamicDoc = undefined; + convertDataToStylesheet = undefined; + }); + + it('returns cross-origin stylesheet', function(done) { + var importUrl = + 'https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.css'; + var options = { + rootNode: document, + shadowId: undefined, + convertDataToStylesheet, + rootIndex: 1 + }; + var priority = [1, 0]; + var importedUrls = []; + var isCrossOriginRequest = true; + + axe.utils + .parseCrossOriginStylesheet( + importUrl, + options, + priority, + importedUrls, + isCrossOriginRequest + ) + .then(function(data) { + assert.isDefined(data); + assert.isDefined(data.sheet); + + assert.equal(data.isCrossOrigin, isCrossOriginRequest); + assert.deepEqual(data.priority, priority); + + assert.property(data.sheet, 'cssRules'); + assert.isAtLeast(data.sheet.cssRules.length, 1); + + axe.testUtils.assertStylesheet( + data.sheet, + '.container', + '.container { position: relative; width: 100%; max-width: 960px; margin: 0px auto; padding: 0px 20px; box-sizing: border-box; }' + ); + done(); + }) + .catch(function() { + done( + new Error('Expected axe.utils.parseCrossOriginStylesheet to resolve.') + ); + }); + }); + + it(`rejects when given url to fetch is not found`, function(done) { + var importUrl = + 'https://make-up-a-website-that-does-not-exist.com/style.css'; + var options = { + rootNode: document, + shadowId: undefined, + convertDataToStylesheet, + rootIndex: 1 + }; + var priority = [1, 0]; + var importedUrls = []; + var isCrossOriginRequest = true; + axe.utils + .parseCrossOriginStylesheet( + importUrl, + options, + priority, + importedUrls, + isCrossOriginRequest + ) + .then(function() { + done( + new Error('Expected axe.utils.parseCrossOriginStylesheet to reject.') + ); + }) + .catch(function(err) { + assert.isNotNull(err); + done(); + }); + }); +}); diff --git a/test/core/utils/parse-sameorigin-stylesheet.js b/test/core/utils/parse-sameorigin-stylesheet.js new file mode 100644 index 0000000000..07d6cf70c0 --- /dev/null +++ b/test/core/utils/parse-sameorigin-stylesheet.js @@ -0,0 +1,171 @@ +describe('axe.utils.parseSameOriginStylesheet', function() { + 'use strict'; + + var stylesForPage; + var styleSheets = { + emptyStyleTag: { + id: 'emptyStyleTag', + text: '' + }, + styleTagWithOneImport: { + id: 'styleTagWithOneImport', + text: '@import "../integration/full/preload-cssom/base.css";' + }, + inlineStyle: { + id: 'inlineStyle', + text: '.inline-css { font-weight:normal; }' + } + }; + var dynamicDoc; + var convertDataToStylesheet; + + beforeEach(function() { + dynamicDoc = document.implementation.createHTMLDocument( + 'Dynamic document for testing axe.utils.parseSameOriginStylesheet' + ); + convertDataToStylesheet = axe.utils.getStyleSheetFactory(dynamicDoc); + }); + + afterEach(function(done) { + dynamicDoc = undefined; + convertDataToStylesheet = undefined; + axe.testUtils.removeStyleSheets(stylesForPage).then(function() { + done(); + stylesForPage = undefined; + }); + }); + + it('returns empty results when given sheet has no cssRules', function(done) { + // add style that has no styles + stylesForPage = [styleSheets.emptyStyleTag]; + + axe.testUtils.addStyleSheets(stylesForPage).then(function() { + // get recently added sheet + var sheet = Array.from(document.styleSheets).filter(function(sheet) { + return sheet.ownerNode.id === styleSheets.emptyStyleTag.id; + })[0]; + // parse sheet + var options = { + rootNode: document, + shadowId: undefined, + convertDataToStylesheet + }; + var priority = [1, 0]; + var importedUrls = []; + var isCrossOriginRequest = false; + axe.utils + .parseSameOriginStylesheet( + sheet, + options, + priority, + importedUrls, + false + ) + .then(function(data) { + assert.isDefined(data); + assert.isDefined(data.sheet); + assert.equal(data.isCrossOrigin, isCrossOriginRequest); + assert.deepEqual(data.priority, priority); + assert.property(data.sheet, 'cssRules'); + assert.isTrue(data.sheet.cssRules.length === 0); + done(); + }); + }); + }); + + it('returns @import rule specified in the stylesheet', function(done) { + // add style that has @import style + stylesForPage = [styleSheets.styleTagWithOneImport]; + + axe.testUtils.addStyleSheets(stylesForPage).then(function() { + // get recently added sheet + var sheet = Array.from(document.styleSheets).filter(function(sheet) { + return sheet.ownerNode.id === styleSheets.styleTagWithOneImport.id; + })[0]; + // parse sheet + var options = { + rootNode: document, + shadowId: undefined, + convertDataToStylesheet + }; + var priority = [1, 0]; + var importedUrls = []; + var isCrossOriginRequest = false; + axe.utils + .parseSameOriginStylesheet( + sheet, + options, + priority, + importedUrls, + false + ) + .then(function(data) { + assert.isDefined(data); + + var parsedImportData = data[0]; + assert.isDefined(parsedImportData.sheet); + assert.equal(parsedImportData.isCrossOrigin, isCrossOriginRequest); + // as @import is a style with in @imported sheet, an additional priority is appended. + assert.deepEqual(parsedImportData.priority, [1, 0, 0]); + assert.property(parsedImportData.sheet, 'cssRules'); + assert.isAtLeast(parsedImportData.sheet.cssRules.length, 1); + axe.testUtils.assertStylesheet( + parsedImportData.sheet, + '.style-from-base-css', + '.style-from-base-css {font-size: 100%; }' + ); + done(); + }); + }); + }); + + it('returns inline style specified in the stylesheet', function(done) { + // add style that has @import style + stylesForPage = [styleSheets.inlineStyle]; + + axe.testUtils.addStyleSheets(stylesForPage).then(function() { + // get recently added sheet + var sheet = Array.from(document.styleSheets).filter(function(sheet) { + return sheet.ownerNode.id === styleSheets.inlineStyle.id; + })[0]; + // parse sheet + var options = { + rootNode: document, + shadowId: undefined, + convertDataToStylesheet + }; + var priority = [1, 0]; + var importedUrls = []; + var isCrossOriginRequest = false; + axe.utils + .parseSameOriginStylesheet( + sheet, + options, + priority, + importedUrls, + false + ) + .then(function(data) { + assert.isDefined(data); + assert.isDefined(data.sheet); + assert.equal(data.isCrossOrigin, isCrossOriginRequest); + assert.deepEqual(data.priority, [1, 0]); + assert.property(data.sheet, 'cssRules'); + assert.isAtLeast(data.sheet.cssRules.length, 1); + axe.testUtils.assertStylesheet( + data.sheet, + '.inline-css', + '.inline-css { font-weight:normal; }' + ); + done(); + }); + }); + }); + + /** + * Note: + * Only single workflow of resolving either the `@import` or `inline` styles can be tested here. + * Multiple resolutions from a given stylesheet containing a combination of styles are test as integration tests. + * See: `/tests/full/integration/preload-cssom.html` + */ +}); diff --git a/test/core/utils/preload-cssom.js b/test/core/utils/preload-cssom.js index f25b7ddf17..9f65f83b81 100644 --- a/test/core/utils/preload-cssom.js +++ b/test/core/utils/preload-cssom.js @@ -1,7 +1,13 @@ -describe('axe.utils.preloadCssom unit tests', function() { +/** + * NOTE: + * `document.styleSheets` does not recognize dynamically injected stylesheets after `load` via `beforeEach`/ `before`, + * so tests for disabled and external stylesheets are done in `integration` tests + * Refer Directory: `./test/full/preload-cssom/**.*` + */ +describe('axe.utils.preloadCssom', function() { 'use strict'; - var args; + var treeRoot; function addStyleToHead() { var css = 'html {font-size: inherit;}'; @@ -22,33 +28,17 @@ describe('axe.utils.preloadCssom unit tests', function() { beforeEach(function() { addStyleToHead(); - args = { - asset: 'cssom', - timeout: 10000, - treeRoot: (axe._tree = axe.utils.getFlattenedTree(document)) - }; + treeRoot = axe._tree = axe.utils.getFlattenedTree(document); }); afterEach(function() { removeStyleFromHead(); }); - it('should be a function', function() { - assert.isFunction(axe.utils.preloadCssom); - }); - - it('should return a queue', function() { - var actual = axe.utils.preloadCssom(args); - assert.isObject(actual); - assert.containsAllKeys(actual, ['then', 'defer', 'catch']); - }); - - it('should ensure result of cssom is an array of sheets', function(done) { - var actual = axe.utils.preloadCssom(args); + it('returns CSSOM object containing an array of sheets', function(done) { + var actual = axe.utils.preloadCssom({ treeRoot }); actual - .then(function(results) { - // returned from queue, hence the index look up - var cssom = results[0]; + .then(function(cssom) { assert.isAtLeast(cssom.length, 2); done(); }) @@ -57,19 +47,17 @@ describe('axe.utils.preloadCssom unit tests', function() { }); }); - it('ensure that each of the cssom object have defined properties', function(done) { - var actual = axe.utils.preloadCssom(args); + it('returns CSSOM and ensure that each object have defined properties', function(done) { + var actual = axe.utils.preloadCssom({ treeRoot }); actual - .then(function(results) { - // returned from queue, hence the index look up - var cssom = results[0]; + .then(function(cssom) { assert.isAtLeast(cssom.length, 2); cssom.forEach(function(o) { assert.hasAllKeys(o, [ 'root', 'shadowId', 'sheet', - 'isExternal', + 'isCrossOrigin', 'priority' ]); }); @@ -80,11 +68,11 @@ describe('axe.utils.preloadCssom unit tests', function() { }); }); - it('should fail if number of sheets returned does not match stylesheets defined in document', function(done) { - var actual = axe.utils.preloadCssom(args); + it('returns false if number of sheets returned does not match stylesheets defined in document', function(done) { + var actual = axe.utils.preloadCssom({ treeRoot }); actual - .then(function(results) { - assert.isFalse(results[0].length <= 1); // returned from queue, hence the index look up + .then(function(cssom) { + assert.isFalse(cssom.length <= 1); done(); }) .catch(function(error) { @@ -92,12 +80,11 @@ describe('axe.utils.preloadCssom unit tests', function() { }); }); - it('should ensure all returned stylesheet is defined and has property cssRules', function(done) { - var actual = axe.utils.preloadCssom(args); + it('returns all stylesheets and ensure each sheet has property cssRules', function(done) { + var actual = axe.utils.preloadCssom({ treeRoot }); actual - .then(function(results) { - var sheets = results[0]; - sheets.forEach(function(s) { + .then(function(cssom) { + cssom.forEach(function(s) { assert.isDefined(s.sheet); assert.property(s.sheet, 'cssRules'); }); @@ -107,9 +94,4 @@ describe('axe.utils.preloadCssom unit tests', function() { done(error); }); }); - - /** - * NOTE: document.styleSheets does not recognise dynamically injected stylesheets after load via beforeEach/ before, so tests for disabled and external stylesheets are done in integration - * Refer Directory: ./test/full/preload-cssom/**.* - */ }); diff --git a/test/core/utils/preload.js b/test/core/utils/preload.js index 2a26e8a7b2..a4584620a5 100644 --- a/test/core/utils/preload.js +++ b/test/core/utils/preload.js @@ -1,82 +1,51 @@ describe('axe.utils.preload', function() { 'use strict'; - it('should return a queue', function() { - var actual = axe.utils.preload({}); - assert.isObject(actual); - assert.containsAllKeys(actual, ['then', 'defer', 'catch']); - }); + var isPhantom = window.PHANTOMJS ? true : false; + var fixture = document.getElementById('fixture'); - it('should ensure queue is defer(able)', function(done) { - var options = { - preload: false - }; - var actual = axe.utils.preload(options); - actual - .defer(function(res, rej) { - assert.isFunction(rej); - res(true); - done(); - }) - .catch(function(error) { - done(error); - }); + before(function() { + axe._tree = axe.utils.getFlattenedTree(fixture); }); - it('should ensure queue is then(able)', function(done) { - var options = { - preload: false - }; - var actual = axe.utils.preload(options); - actual - .then(function(results) { - assert.isDefined(results); - done(); - }) - .catch(function(error) { - done(error); - }); - }); + (isPhantom ? it.skip : it)( + 'returns `undefined` when `preload` option is set to false.', + function(done) { + var options = { + preload: false + }; + var actual = axe.utils.preload(options); + actual + .then(function(results) { + assert.isUndefined(results); + done(); + }) + .catch(function(error) { + done(error); + }); + } + ); - it('should return empty array as result', function(done) { - var options = { - preload: false - }; - var actual = axe.utils.preload(options); - actual - .then(function(results) { + (isPhantom ? it.skip : it)( + 'returns assets with `cssom`, verify result is same output from `preloadCssom` fn', + function(done) { + var options = { + preload: { + assets: ['cssom'] + } + }; + var actual = axe.utils.preload(options); + actual.then(function(results) { assert.isDefined(results); - assert.isArray(results); - assert.lengthOf(results, 0); - done(); - }) - .catch(function(error) { - done(error); - }); - }); + assert.property(results, 'cssom'); - it('should return an object with property cssom and verify result is same output from preloadCssom', function(done) { - var options = { - preload: { - assets: ['cssom'] - } - }; - var actual = axe.utils.preload(options); - actual - .then(function(results) { - assert.isDefined(results); - assert.isArray(results); - assert.property(results[0], 'cssom'); - // also verify that result from css matches that of preloadCssom axe.utils.preloadCssom(options).then(function(resultFromPreloadCssom) { - assert.deepEqual(results[0].cssom, resultFromPreloadCssom[0]); + assert.deepEqual(results.cssom, resultFromPreloadCssom); done(); }); - }) - .catch(function(error) { - done(error); }); - }); + } + ); describe('axe.utils.shouldPreload', function() { it('should return true if preload configuration is valid', function() { diff --git a/test/integration/full/css-orientation-lock/passes.js b/test/integration/full/css-orientation-lock/passes.js index 1b67802b56..40e97f06f4 100644 --- a/test/integration/full/css-orientation-lock/passes.js +++ b/test/integration/full/css-orientation-lock/passes.js @@ -18,7 +18,6 @@ describe('css-orientation-lock passes test', function() { before(function(done) { if (isPhantom) { this.skip(); - done(); } else { axe.testUtils .addStyleSheets(styleSheets) diff --git a/test/integration/full/css-orientation-lock/violations.js b/test/integration/full/css-orientation-lock/violations.js index 638050f6e6..ce94d605fe 100644 --- a/test/integration/full/css-orientation-lock/violations.js +++ b/test/integration/full/css-orientation-lock/violations.js @@ -18,7 +18,6 @@ describe('css-orientation-lock violations test', function() { before(function(done) { if (isPhantom) { this.skip(); - done(); } else { axe.testUtils .addStyleSheets(styleSheets) diff --git a/test/integration/full/preload-cssom/base.css b/test/integration/full/preload-cssom/base.css new file mode 100644 index 0000000000..5de9d516ee --- /dev/null +++ b/test/integration/full/preload-cssom/base.css @@ -0,0 +1,5 @@ +/* stylesheet used for preload cssom testing */ + +.style-from-base-css { + font-size: 100%; +} diff --git a/test/integration/full/preload-cssom/cyclic-cross-origin-import-1.css b/test/integration/full/preload-cssom/cyclic-cross-origin-import-1.css new file mode 100644 index 0000000000..aed040efe4 --- /dev/null +++ b/test/integration/full/preload-cssom/cyclic-cross-origin-import-1.css @@ -0,0 +1,4 @@ +/* calls a stylesheets, which inturn refers back to this one , but also imports a cross-origin stylesheet */ + +@import 'cyclic-cross-origin-import-2.css'; +@import 'https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.css'; \ No newline at end of file diff --git a/test/integration/full/preload-cssom/cyclic-cross-origin-import-2.css b/test/integration/full/preload-cssom/cyclic-cross-origin-import-2.css new file mode 100644 index 0000000000..49e1dbddb9 --- /dev/null +++ b/test/integration/full/preload-cssom/cyclic-cross-origin-import-2.css @@ -0,0 +1,3 @@ +/* calls a stylesheets, which inturn refers back to this one */ + +@import 'cyclic-cross-origin-import-1.css'; \ No newline at end of file diff --git a/test/integration/full/preload-cssom/cyclic-import-1.css b/test/integration/full/preload-cssom/cyclic-import-1.css new file mode 100644 index 0000000000..7003797431 --- /dev/null +++ b/test/integration/full/preload-cssom/cyclic-import-1.css @@ -0,0 +1,3 @@ +/* calls a stylesheets, which inturn refers back to this one */ + +@import 'cyclic-import-2.css'; \ No newline at end of file diff --git a/test/integration/full/preload-cssom/cyclic-import-2.css b/test/integration/full/preload-cssom/cyclic-import-2.css new file mode 100644 index 0000000000..0f4878afa6 --- /dev/null +++ b/test/integration/full/preload-cssom/cyclic-import-2.css @@ -0,0 +1,8 @@ + +/* calls a stylesheet, from which this stylesheet was called (cyclic references) */ + +@import 'cyclic-import-1.css'; + +.style-from-cyclic-import-2-css { + font-family: inherit; +} \ No newline at end of file diff --git a/test/integration/full/preload-cssom/frames/level1.html b/test/integration/full/preload-cssom/frames/level1.html index 5f2b59416a..e797d31416 100644 --- a/test/integration/full/preload-cssom/frames/level1.html +++ b/test/integration/full/preload-cssom/frames/level1.html @@ -12,7 +12,7 @@ media="print" /> diff --git a/test/integration/full/preload-cssom/import-non-existing-cross-origin.css b/test/integration/full/preload-cssom/import-non-existing-cross-origin.css new file mode 100644 index 0000000000..0d71d1f553 --- /dev/null +++ b/test/integration/full/preload-cssom/import-non-existing-cross-origin.css @@ -0,0 +1,3 @@ +/* loads a non existing cross origin stylesheet */ + +@import "https://non.existing/style.css" \ No newline at end of file diff --git a/test/integration/full/preload-cssom/multiple-import-1.css b/test/integration/full/preload-cssom/multiple-import-1.css new file mode 100644 index 0000000000..fc46302df8 --- /dev/null +++ b/test/integration/full/preload-cssom/multiple-import-1.css @@ -0,0 +1,4 @@ +/* import multiple stylesheets */ + +@import 'multiple-import-2.css'; +@import 'multiple-import-3.css'; \ No newline at end of file diff --git a/test/integration/full/preload-cssom/multiple-import-2.css b/test/integration/full/preload-cssom/multiple-import-2.css new file mode 100644 index 0000000000..541d6910ba --- /dev/null +++ b/test/integration/full/preload-cssom/multiple-import-2.css @@ -0,0 +1,5 @@ +/* this is referenced by multiple-import-1.css */ + +.style-from-multiple-import-2-css { + font-size: 100%; +} \ No newline at end of file diff --git a/test/integration/full/preload-cssom/multiple-import-3.css b/test/integration/full/preload-cssom/multiple-import-3.css new file mode 100644 index 0000000000..d83324ad9c --- /dev/null +++ b/test/integration/full/preload-cssom/multiple-import-3.css @@ -0,0 +1,5 @@ +/* this is referenced by multiple-import-1.css */ + +.style-from-multiple-import-3-css { + font-size: 100%; +} \ No newline at end of file diff --git a/test/integration/full/preload-cssom/nested-import-1.css b/test/integration/full/preload-cssom/nested-import-1.css new file mode 100644 index 0000000000..094c2fbead --- /dev/null +++ b/test/integration/full/preload-cssom/nested-import-1.css @@ -0,0 +1,4 @@ + +/* entry point for nested import scenario */ + +@import 'nested-import-2.css'; \ No newline at end of file diff --git a/test/integration/full/preload-cssom/nested-import-2.css b/test/integration/full/preload-cssom/nested-import-2.css new file mode 100644 index 0000000000..7aa3516048 --- /dev/null +++ b/test/integration/full/preload-cssom/nested-import-2.css @@ -0,0 +1,3 @@ +/* referred by nested-import-1.css */ + +@import 'nested-import-3.css'; \ No newline at end of file diff --git a/test/integration/full/preload-cssom/nested-import-3.css b/test/integration/full/preload-cssom/nested-import-3.css new file mode 100644 index 0000000000..e0e2e60827 --- /dev/null +++ b/test/integration/full/preload-cssom/nested-import-3.css @@ -0,0 +1,5 @@ +/* referred by nested-import-2.css */ + +.style-from-nested-import-3-css { + font-size: inherit +} \ No newline at end of file diff --git a/test/integration/full/preload-cssom/preload-cssom-shadow-blue.css b/test/integration/full/preload-cssom/preload-cssom-shadow-blue.css deleted file mode 100644 index 5f16f384cb..0000000000 --- a/test/integration/full/preload-cssom/preload-cssom-shadow-blue.css +++ /dev/null @@ -1,3 +0,0 @@ -.blue { - background-color: blue; -} diff --git a/test/integration/full/preload-cssom/preload-cssom.html b/test/integration/full/preload-cssom/preload-cssom.html index d6f92fd25d..e71d8d4b57 100644 --- a/test/integration/full/preload-cssom/preload-cssom.html +++ b/test/integration/full/preload-cssom/preload-cssom.html @@ -4,11 +4,11 @@ - + - +