UNPKG

axe-core

Version:

Accessibility engine for automated Web UI testing

281 lines (252 loc) • 7.63 kB
import { isHidden, findBy, getNodeFromTree, getFlattenedTree, select, isNodeInContext, nodeSorter, respondable } from '../utils'; /** * 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) { if (isHidden(frame)) { return; } var fr = 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) { 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) { // typeof NodeList.length in PhantomJS === function if ( (context && typeof context === 'object') || context instanceof window.NodeList ) { if (context instanceof window.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) { 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 getNodeFromTree(node); }) ); break; } else if (item && item.length && !(item instanceof window.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 getNodeFromTree(node); }) ); } } else if (item instanceof window.Node) { if (item.documentElement instanceof window.Node) { result.push(context.flatTree[0]); } else { result.push(getNodeFromTree(item)); } } } // filter nulls return result.filter(r => r); } /** * Check that the context, as well as each frame includes at least 1 element * @private * @param {context} context * @return {Error} */ function validateContext(context) { if (context.include.length === 0) { if (context.frames.length === 0) { var env = respondable.isInFrame() ? 'frame' : 'page'; return new Error('No elements found for include in ' + env + ' Context'); } context.frames.forEach((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 for (var i = 0; i < selectors.length; ++i) { var item = selectors[i]; if (item instanceof window.Element) { return item.ownerDocument.documentElement; } if (item instanceof window.Document) { return item.documentElement; } } return 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) { this.frames = []; this.initiator = spec && typeof spec.initiator === 'boolean' ? spec.initiator : true; this.focusable = spec && typeof spec.focusable === 'boolean' ? spec.focusable : true; this.boundingClientRect = spec && typeof spec.boundingClientRect === 'object' ? spec.boundingClientRect : {}; this.page = false; spec = normalizeContext(spec); //cache the flattened tree this.flatTree = getFlattenedTree(getRootNode(spec)); this.exclude = spec.exclude; this.include = spec.include; this.include = parseSelectorArray(this, 'include'); this.exclude = parseSelectorArray(this, 'exclude'); 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(nodeSorter); // ensure that the order of the include nodes is document order } export default Context;