UNPKG

axe-core

Version:

Accessibility engine for automated Web UI testing

262 lines (228 loc) • 7.19 kB
/*exported Context */ /*global isNodeInContext */ /** * Pushes a unique frame onto `frames` array, filtering any hidden iframes * @private * @param {Array} collection The array of unique frames that is being operated on * @param {HTMLElement} frame The frame to push onto Context */ function pushUniqueFrame(collection, frame) { 'use strict'; if (axe.utils.isHidden(frame)) { return; } var fr = axe.utils.findBy(collection, 'node', frame); if (!fr) { collection.push({ node: frame, include: [], exclude: [] }); } } /** * Unshift selectors of matching iframes * @private * @param {Context} context The context object to operate on and assign to * @param {String} type The "type" of context, 'include' or 'exclude' * @param {Array} selectorArray Array of CSS selectors, each element represents a frame; * where the last element is the actual node */ function pushUniqueFrameSelector(context, type, selectorArray) { 'use strict'; context.frames = context.frames || []; var result, frame; var frames = document.querySelectorAll(selectorArray.shift()); frameloop: for (var i = 0, l = frames.length; i < l; i++) { frame = frames[i]; for (var j = 0, l2 = context.frames.length; j < l2; j++) { if (context.frames[j].node === frame) { context.frames[j][type].push(selectorArray); break frameloop; } } result = { node: frame, include: [], exclude: [] }; if (selectorArray) { result[type].push(selectorArray); } context.frames.push(result); } } /** * Normalize the input of "context" so that many different methods of input are accepted * @private * @param {Mixed} context The configuration object passed to `Context` * @return {Object} Normalized context spec to include both `include` and `exclude` arrays */ function normalizeContext(context) { /*eslint complexity: ["error", 13] */ 'use strict'; // typeof NodeList.length in PhantomJS === function if (context && typeof context === 'object' || context instanceof NodeList) { if (context instanceof Node) { return { include: [context], exclude: [] }; } if (context.hasOwnProperty('include') || context.hasOwnProperty('exclude')) { return { include: (context.include && +context.include.length) ? context.include : [document], exclude: context.exclude || [] }; } if (context.length === +context.length) { return { include: context, exclude: [] }; } } if (typeof context === 'string') { return { include: [context], exclude: [] }; } return { include: [document], exclude: [] }; } /** * Finds frames in context, converts selectors to Element references and pushes unique frames * @private * @param {Context} context The instance of Context to operate on * @param {String} type The "type" of thing to parse, "include" or "exclude" * @return {Array} Parsed array of matching elements */ function parseSelectorArray(context, type) { 'use strict'; var item, result = [], nodeList; for (var i = 0, l = context[type].length; i < l; i++) { item = context[type][i]; // selector if (typeof item === 'string') { nodeList = Array.from(document.querySelectorAll(item)); //eslint no-loop-func:0 result = result.concat(nodeList.map((node) => { return axe.utils.getNodeFromTree(context.flatTree[0], node); })); break; } else if (item && item.length && !(item instanceof Node)) { if (item.length > 1) { pushUniqueFrameSelector(context, type, item); } else { nodeList = Array.from(document.querySelectorAll(item[0])); //eslint no-loop-func:0 result = result.concat(nodeList.map((node) => { return axe.utils.getNodeFromTree(context.flatTree[0], node); })); } } else if (item instanceof Node) { if (item.documentElement instanceof Node) { result.push(context.flatTree[0]); } else { result.push(axe.utils.getNodeFromTree(context.flatTree[0], item)); } } } // filter nulls return result.filter(function (r) { return r; }); } /** * Check that the context, as well as each frame includes at least 1 element * @private * @param {context} context * @return {Error} */ function validateContext(context) { 'use strict'; if (context.include.length === 0) { if (context.frames.length === 0) { var env = axe.utils.respondable.isInFrame() ? 'frame' : 'page'; return new Error('No elements found for include in ' + env + ' Context'); } context.frames.forEach(function (frame, i) { if (frame.include.length === 0) { return new Error('No elements found for include in Context of frame ' + i); } }); } } /** * For a context-like object, find its shared root node */ function getRootNode ({ include, exclude }) { const selectors = Array.from(include).concat(Array.from(exclude)); // Find the first Element.ownerDocument or Document const localDocument = selectors.reduce((result, item) => { if (result) { return result; } else if (item instanceof Element) { return item.ownerDocument } else if (item instanceof Document) { return item } }, null); return (localDocument || document).documentElement; } /** * Holds context of includes, excludes and frames for analysis. * * @todo clarify and sync changes to design doc * Context : {IncludeStrings} || { * // defaults to document/all * include: {IncludeStrings}, * exclude : {ExcludeStrings} * } * * IncludeStrings : [{CSSSelectorArray}] || Node * ExcludeStrings : [{CSSSelectorArray}] * `CSSSelectorArray` an Array of selector strings that addresses a Node in a multi-frame document. All addresses * are in this form regardless of whether the document contains any frames.To evaluate the selectors to * find the node referenced by the array, evaluate the selectors in-order, starting in window.top. If N * is the length of the array, then the first N-1 selectors should result in an iframe and the last * selector should result in the specific node. * * @param {Object} spec Configuration or "specification" object */ function Context(spec) { /* eslint max-statements:["error",22], no-unused-vars:0 */ 'use strict'; this.frames = []; this.initiator = (spec && typeof spec.initiator === 'boolean') ? spec.initiator : true; this.page = false; spec = normalizeContext(spec); //cache the flattened tree this.flatTree = axe.utils.getFlattenedTree(getRootNode(spec)); this.exclude = spec.exclude; this.include = spec.include; this.include = parseSelectorArray(this, 'include'); this.exclude = parseSelectorArray(this, 'exclude'); axe.utils.select('frame, iframe', this).forEach((frame) => { if (isNodeInContext(frame, this)) { pushUniqueFrame(this.frames, frame.actualNode); } }); if (this.include.length === 1 && this.include[0].actualNode === document.documentElement) { this.page = true; } // Validate outside of a frame var err = validateContext(this); if (err instanceof Error) { throw err; } if (!Array.isArray(this.include)) { this.include = Array.from(this.include); } this.include.sort(axe.utils.nodeSorter); // ensure that the order of the include nodes is document order }