diff --git a/.circleci/config.yml b/.circleci/config.yml index 48503b6c23..066f9d0e26 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -97,10 +97,6 @@ jobs: - run: npx grunt fixture # run IE webdriver tests - run: npx grunt connect test-webdriver:ie - # test examples - # Note: Jasmine karma-chrome-launcher requires chrome browser - - run: choco install googlechrome --ignore-checksums - - run: npm run test:examples # Run examples under `doc/examples` test_examples: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1392b973e3..0189efec7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [4.1.3](https://github.com/dequelabs/axe-core/compare/v4.1.2...v4.1.3) (2021-03-04) + +t/8a699ecba6c77f6a705d44616f1bcefd634ff89b)) + +### Bug Fixes + +- **respondable:** Avoid message duplication with messageId ([#2816](https://github.com/dequelabs/axe-core/issues/2816)) ([9b6eb59](https://github.com/dequelabs/axe-core/commit/9b6eb5987da104398acaae60b7b7ee4e0b2d3c8f)) +- **types:** Add noHtml option ([#2810](https://github.com/dequelabs/axe-core/issues/2810)) ([8bc0bae](https://github.com/dequelabs/axe-core/commit/8bc0baec5c997873daf43ff5de61ea22a8e8c896)) + ### [4.1.2](https://github.com/dequelabs/axe-core/compare/v4.1.1...v4.1.2) (2021-02-08) ### Bug Fixes diff --git a/axe.d.ts b/axe.d.ts index 2ea753c1fe..f4f3009047 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -54,191 +54,192 @@ declare namespace axe { type ElementContext = Node | string | ContextObject; - interface TestEngine { - name: string; - version: string; - } - interface TestRunner { - name: string; - } - interface TestEnvironment { - userAgent: string; - windowWidth: number; - windowHeight: number; - orientationAngle?: number; - orientationType?: string; - } - interface RunOnly { - type: RunOnlyType; - values: TagValue[] | string[]; - } - interface RuleObject { - [key: string]: { - enabled: boolean; - }; - } - interface RunOptions { - runOnly?: RunOnly | TagValue[] | string[]; - rules?: RuleObject; - reporter?: ReporterVersion; - resultTypes?: resultGroups[]; - selectors?: boolean; - ancestry?: boolean; - xpath?: boolean; - absolutePaths?: boolean; - iframes?: boolean; - elementRef?: boolean; - frameWaitTime?: number; - preload?: boolean; - performanceTimer?: boolean; - } - interface AxeResults { - toolOptions: RunOptions; - testEngine: TestEngine; - testRunner: TestRunner; - testEnvironment: TestEnvironment; - url: string; - timestamp: string; - passes: Result[]; - violations: Result[]; - incomplete: Result[]; - inapplicable: Result[]; - } - interface Result { - description: string; - help: string; - helpUrl: string; - id: string; - impact?: ImpactValue; - tags: TagValue[]; - nodes: NodeResult[]; - } - interface NodeResult { - html: string; - impact?: ImpactValue; - target: string[]; - xpath?: string[]; - ancestry?: string[]; - any: CheckResult[]; - all: CheckResult[]; - none: CheckResult[]; - failureSummary?: string; - element?: HTMLElement; - } - interface CheckResult { - id: string; - impact: string; - message: string; - data: any; - relatedNodes?: RelatedNode[]; - } - interface RelatedNode { - target: string[]; - html: string; - } - interface RuleLocale { - [key: string]: { - description: string; - help: string; - }; - } - interface CheckLocale { - [key: string]: { - pass: string | { [key: string]: string }; - fail: string | { [key: string]: string }; - incomplete: string | { [key: string]: string }; - }; - } - interface Locale { - lang?: string; - rules?: RuleLocale; - checks?: CheckLocale; - } - interface AriaAttrs { - type: AriaAttrsType; - values?: string[]; - allowEmpty?: boolean; - global?: boolean; - unsupported?: boolean; - } - interface AriaRoles { - type: AriaRolesType | DpubRolesType; - requiredContext?: string[]; - requiredOwned?: string[]; - requiredAttrs?: string[]; - allowedAttrs?: string[]; - nameFromContent?: boolean; - unsupported?: boolean; - } - interface HtmlElmsVariant { - contentTypes?: HtmlContentTypes[]; - allowedRoles: boolean | string[]; - noAriaAttrs?: boolean; - shadowRoot?: boolean; - implicitAttrs?: { [key: string]: string }; - namingMethods?: string[]; - } - interface HtmlElms extends HtmlElmsVariant { - variant?: { [key: string]: HtmlElmsVariant }; - } - interface Standards { - ariaAttrs?: { [key: string]: AriaAttrs }; - ariaRoles?: { [key: string]: AriaRoles }; - htmlElms?: { [key: string]: HtmlElms }; - cssColors?: { [key: string]: number[] }; - } - interface Spec { - branding?: { - brand?: string; - application?: string; - }; - reporter?: ReporterVersion; - checks?: Check[]; - rules?: Rule[]; - standards?: Standards; - locale?: Locale; - disableOtherRules?: boolean; - axeVersion?: string; - // Deprecated - do not use. - ver?: string; - } - interface Check { - id: string; - evaluate: Function | string; - after?: Function | string; - options?: any; - matches?: string; - enabled?: boolean; - } - interface Rule { - id: string; - selector?: string; - impact?: ImpactValue; - excludeHidden?: boolean; - enabled?: boolean; - pageLevel?: boolean; - any?: string[]; - all?: string[]; - none?: string[]; - tags?: string[]; - matches?: string; - } - interface AxePlugin { - id: string; - run(...args: any[]): any; - commands: { - id: string; - callback(...args: any[]): void; - }[]; - cleanup?(callback: Function): void; - } - interface RuleMetadata { - ruleId: string; - description: string; - help: string; - helpUrl: string; - tags: string[]; - } + interface TestEngine { + name: string; + version: string; + } + interface TestRunner { + name: string; + } + interface TestEnvironment { + userAgent: string; + windowWidth: number; + windowHeight: number; + orientationAngle?: number; + orientationType?: string; + } + interface RunOnly { + type: RunOnlyType; + values: TagValue[] | string[]; + } + interface RuleObject { + [key: string]: { + enabled: boolean; + }; + } + interface RunOptions { + runOnly?: RunOnly | TagValue[] | string[]; + rules?: RuleObject; + reporter?: ReporterVersion; + resultTypes?: resultGroups[]; + selectors?: boolean; + ancestry?: boolean; + xpath?: boolean; + absolutePaths?: boolean; + iframes?: boolean; + elementRef?: boolean; + frameWaitTime?: number; + preload?: boolean; + performanceTimer?: boolean; + } + interface AxeResults { + toolOptions: RunOptions; + testEngine: TestEngine; + testRunner: TestRunner; + testEnvironment: TestEnvironment; + url: string; + timestamp: string; + passes: Result[]; + violations: Result[]; + incomplete: Result[]; + inapplicable: Result[]; + } + interface Result { + description: string; + help: string; + helpUrl: string; + id: string; + impact?: ImpactValue; + tags: TagValue[]; + nodes: NodeResult[]; + } + interface NodeResult { + html: string; + impact?: ImpactValue; + target: string[]; + xpath?: string[]; + ancestry?: string[]; + any: CheckResult[]; + all: CheckResult[]; + none: CheckResult[]; + failureSummary?: string; + element?: HTMLElement; + } + interface CheckResult { + id: string; + impact: string; + message: string; + data: any; + relatedNodes?: RelatedNode[]; + } + interface RelatedNode { + target: string[]; + html: string; + } + interface RuleLocale { + [key: string]: { + description: string; + help: string; + }; + } + interface CheckLocale { + [key: string]: { + pass: string | { [key: string]: string }; + fail: string | { [key: string]: string }; + incomplete: string | { [key: string]: string }; + }; + } + interface Locale { + lang?: string; + rules?: RuleLocale; + checks?: CheckLocale; + } + interface AriaAttrs { + type: AriaAttrsType; + values?: string[]; + allowEmpty?: boolean; + global?: boolean; + unsupported?: boolean; + } + interface AriaRoles { + type: AriaRolesType | DpubRolesType; + requiredContext?: string[]; + requiredOwned?: string[]; + requiredAttrs?: string[]; + allowedAttrs?: string[]; + nameFromContent?: boolean; + unsupported?: boolean; + } + interface HtmlElmsVariant { + contentTypes?: HtmlContentTypes[]; + allowedRoles: boolean | string[]; + noAriaAttrs?: boolean; + shadowRoot?: boolean; + implicitAttrs?: { [key: string]: string }; + namingMethods?: string[]; + } + interface HtmlElms extends HtmlElmsVariant { + variant?: { [key: string]: HtmlElmsVariant }; + } + interface Standards { + ariaAttrs?: { [key: string]: AriaAttrs }; + ariaRoles?: { [key: string]: AriaRoles }; + htmlElms?: { [key: string]: HtmlElms }; + cssColors?: { [key: string]: number[] }; + } + interface Spec { + branding?: { + brand?: string; + application?: string; + }; + reporter?: ReporterVersion; + checks?: Check[]; + rules?: Rule[]; + standards?: Standards; + locale?: Locale; + disableOtherRules?: boolean; + axeVersion?: string; + noHtml?: boolean; + // Deprecated - do not use. + ver?: string; + } + interface Check { + id: string; + evaluate: Function | string; + after?: Function | string; + options?: any; + matches?: string; + enabled?: boolean; + } + interface Rule { + id: string; + selector?: string; + impact?: ImpactValue; + excludeHidden?: boolean; + enabled?: boolean; + pageLevel?: boolean; + any?: string[]; + all?: string[]; + none?: string[]; + tags?: string[]; + matches?: string; + } + interface AxePlugin { + id: string; + run(...args: any[]): any; + commands: { + id: string; + callback(...args: any[]): void; + }[]; + cleanup?(callback: Function): void; + } + interface RuleMetadata { + ruleId: string; + description: string; + help: string; + helpUrl: string; + tags: string[]; + } let version: string; let plugins: any; diff --git a/bower.json b/bower.json index e9077134c6..d8017e80fb 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.1.2", + "version": "4.1.3", "contributors": [ { "name": "David Sturley", diff --git a/lib/core/public/load.js b/lib/core/public/load.js index 05c4a71adc..94e230af5b 100644 --- a/lib/core/public/load.js +++ b/lib/core/public/load.js @@ -1,6 +1,7 @@ import Audit from '../base/audit'; import cleanup from './cleanup'; import runRules from './run-rules'; +import respondable from '../utils/respondable'; function runCommand(data, keepalive, callback) { var resolve = callback; @@ -43,24 +44,21 @@ function runCommand(data, keepalive, callback) { } } +if (window.top !== window) { + respondable.subscribe('axe.start', runCommand); + respondable.subscribe('axe.ping', (data, keepalive, respond) => { + respond({ + axe: true + }); + }); +} + /** * Sets up Rules, Messages and default options for Checks, must be invoked before attempting analysis * @param {Object} audit The "audit specification" object * @private */ function load(audit) { - axe.utils.respondable.subscribe('axe.ping', function( - data, - keepalive, - respond - ) { - respond({ - axe: true - }); - }); - - axe.utils.respondable.subscribe('axe.start', runCommand); - axe._audit = new Audit(audit); } diff --git a/lib/core/utils/index.js b/lib/core/utils/index.js index a6232dcb67..13efbccb69 100644 --- a/lib/core/utils/index.js +++ b/lib/core/utils/index.js @@ -73,5 +73,6 @@ export { default as setScrollState } from './set-scroll-state'; export { default as toArray } from './to-array'; export { default as tokenList } from './token-list'; export { default as uniqueArray } from './unique-array'; +export { default as uuid } from './uuid'; export { default as validInputTypes } from './valid-input-type'; export { default as isValidLang, validLangs } from './valid-langs'; diff --git a/lib/core/utils/respondable.js b/lib/core/utils/respondable.js index caba684139..653f1a8c7c 100644 --- a/lib/core/utils/respondable.js +++ b/lib/core/utils/respondable.js @@ -1,116 +1,62 @@ -import { v1 as uuid } from './uuid'; -import cache from '../base/cache'; - -const messages = {}; -const subscribers = {}; -const errorTypes = Object.freeze([ - 'EvalError', - 'RangeError', - 'ReferenceError', - 'SyntaxError', - 'TypeError', - 'URIError' -]); +import { v4 as createUuid } from './uuid'; +import assert from './assert'; +import { + storeCallback, + getCallback, + deleteCallback +} from './respondable/callback-store'; +import { parseMessage } from './respondable/message-parser'; +import { + assertIsFrameWindow, + assertIsParentWindow +} from './respondable/assert-window'; +import { post } from './respondable/post'; +import { createMessageId, isNewMessage } from './respondable/message-id'; /** - * get the unique string to be used to identify our instance of axe - * @private - */ -function _getSource() { - var application = 'axeAPI', - version = '', - src; - // TODO: es-modules_audit - if (typeof axe !== 'undefined' && axe._audit && axe._audit.application) { - application = axe._audit.application; - } - if (typeof axe !== 'undefined') { - // TODO: es-modules-version - version = axe.version; - } - src = application + '.' + version; - return src; -} -/** - * Verify the received message is from the "respondable" module - * @private - * @param {Object} postedMessage The message received via postMessage - * @return {Boolean} `true` if the message is verified from respondable - */ -function verify(postedMessage) { - if ( - // Check incoming message is valid - typeof postedMessage === 'object' && - typeof postedMessage.uuid === 'string' && - postedMessage._respondable === true - ) { - var messageSource = _getSource(); - return ( - // Check the version matches - postedMessage._source === messageSource - ); - } - return false; -} - -/** - * Posts the message to correct frame. - * This abstraction necessary because IE9 & 10 do not support posting Objects; only strings - * @private - * @param {Window} win The `window` to post the message to + * Post a message to a window who may or may not respond to it. + * @param {Window} win The window to post the message to * @param {String} topic The topic of the message * @param {Object} message The message content - * @param {String} uuid The UUID, or pseudo-unique ID of the message * @param {Boolean} keepalive Whether to allow multiple responses - default is false * @param {Function} callback The function to invoke when/if the message is responded to */ -function post(win, topic, message, uuid, keepalive, callback) { - var error; - if (message instanceof Error) { - error = { - name: message.name, - message: message.message, - stack: message.stack - }; - message = undefined; - } - - var data = { - uuid: uuid, - topic: topic, - message: message, - error: error, - _respondable: true, - _source: _getSource(), - // TODO: es-modules_uuid - _axeuuid: axe._uuid, - _keepalive: keepalive - }; - - var axeRespondables = cache.get('axeRespondables'); - if (!axeRespondables) { - axeRespondables = {}; - cache.set('axeRespondables', axeRespondables); - } - axeRespondables[uuid] = true; +export default function respondable(win, topic, message, keepalive, callback) { + const channelId = `${createUuid()}:${createUuid()}`; if (typeof callback === 'function') { - messages[uuid] = callback; + storeCallback({ channelId }, callback, false); } + post(win, { + topic, + channelId, + message, + messageId: createMessageId(), + keepalive: !!keepalive, + sendToParent: false + }); +} - win.postMessage(JSON.stringify(data), '*'); +// Set up the global listener +if (typeof window.addEventListener === 'function') { + window.addEventListener('message', messageListener, false); } /** - * Post a message to a window who may or may not respond to it. - * @param {Window} win The window to post the message to - * @param {String} topic The topic of the message - * @param {Object} message The message content - * @param {Boolean} keepalive Whether to allow multiple responses - default is false - * @param {Function} callback The function to invoke when/if the message is responded to + * Handle incoming window messages + * @param {MessageEvent} */ -function respondable(win, topic, message, keepalive, callback) { - var id = uuid(); - post(win, topic, message, id, keepalive, callback); +function messageListener({ data: dataString, source: win }) { + const { channelId, topic, message, messageId, keepalive } = + parseMessage(dataString) || {}; + const { callback, sendToParent } = getCallback({ channelId, topic }) || {}; + if (!shouldRunCallback({ message, messageId, callback, sendToParent })) { + return; + } + + if (!keepalive && channelId) { + deleteCallback({ channelId }); + } + runCallback(win, { channelId, message, keepalive, sendToParent, callback }); } /** @@ -122,143 +68,98 @@ function respondable(win, topic, message, keepalive, callback) { * @param {Function} callback The function to invoke when a message is received */ respondable.subscribe = function subscribe(topic, callback) { - subscribers[topic] = callback; + assert( + typeof callback === 'function', + 'Subscriber callback must be a function' + ); + storeCallback({ topic }, callback); }; /** * checks if the current context is inside a frame * @return {Boolean} */ -respondable.isInFrame = function isInFrame(win) { - win = win || window; +respondable.isInFrame = function isInFrame(win = window) { return !!win.frameElement; }; /** - * Helper closure to create a function that may be used to respond to a message - * @private - * @param {Window} source The window from which the message originated - * @param {String} topic The topic of the message - * @param {String} uuid The "unique" ID of the original message - * @return {Function} A function that may be invoked to respond to the message - */ -function createResponder(source, topic, uuid) { - return function(message, keepalive, callback) { - post(source, topic, message, uuid, keepalive, callback); - }; -} - -/** - * Publishes the "respondable" message to the appropriate subscriber - * @private - * @param {Window} source The window from which the message originated - * @param {Object} data The data sent with the message - * @param {Boolean} keepalive Whether to allow multiple responses - default is false + * Test if the callback should be invoked with this message + * @param {object} messageData */ -function publish(source, data, keepalive) { - var topic = data.topic; - var subscriber = subscribers[topic]; - - if (subscriber) { - var responder = createResponder(source, null, data.uuid); - subscriber(data.message, keepalive, responder); +function shouldRunCallback({ message, messageId, callback, sendToParent }) { + // An error should never come from a parent. Log it and exit. + if (message instanceof Error && sendToParent) { + axe.log(message); + return false; } -} -// added for testing so we can fire subscriber events without having -// to mock the universe going through `respondable()` -respondable._publish = publish; + return !!callback && isNewMessage(messageId); +} /** - * Convert a javascript Error into something that can be stringified - * @param {Error} error Any type of error - * @return {Object} Processable object + * Run the callback, including handling any errors that might come doing so + * @param {window} win + * @param {object} messageData */ -function buildErrorObject(error) { - var msg = error.message || 'Unknown error occurred'; - var errorName = errorTypes.includes(error.name) ? error.name : 'Error'; - var ErrConstructor = window[errorName] || Error; - - if (error.stack) { - msg += '\n' + error.stack.replace(error.message, ''); +function runCallback( + win, + { channelId, message, keepalive, sendToParent, callback } +) { + try { + // Only accept messages from parent or child frames + sendToParent ? assertIsParentWindow(win) : assertIsFrameWindow(win); + const responder = createResponder(win, channelId, sendToParent); + callback(message, keepalive, responder); + } catch (error) { + processError(win, error, channelId, sendToParent); } - return new ErrConstructor(msg); } /** - * Parse the received message for processing - * @param {string} dataString Message received - * @return {object} Object to be used for pub/sub + * Log, or post an error to the parent window + * @param {window} win + * @param {object} messageData */ -function parseMessage(dataString) { - /*eslint no-empty: 0*/ - var data; - if (typeof dataString !== 'string') { - return; +function processError(win, error, channelId, sendToParent) { + if (!sendToParent) { + return axe.log(error); } try { - data = JSON.parse(dataString); - } catch (ex) {} - - if (!verify(data)) { - return; + post(win, { + topic: null, + channelId, + message: error, + messageId: createMessageId(), + keepalive: true, + sendToParent + }); + } catch (err) { + // Last resort, logging + return axe.log(err); } - - if (typeof data.error === 'object') { - data.error = buildErrorObject(data.error); - } else { - data.error = undefined; - } - return data; } -if (typeof window.addEventListener === 'function') { - window.addEventListener( - 'message', - function(e) { - var data = parseMessage(e.data); - if (!data || !data._axeuuid) { - return; - } - - var uuid = data.uuid; - - /** - * NOTE: messages from other contexts (frames) in response - * to a message should not contain the same axe._uuid. We - * ignore these messages to prevent rogue postMessage - * handlers reflecting our messages. - * @see https://github.com/dequelabs/axe-core/issues/1754 - */ - var axeRespondables = cache.get('axeRespondables') || {}; - if (axeRespondables[uuid] && data._axeuuid === axe._uuid) { - return; - } - - var keepalive = data._keepalive; - var callback = messages[uuid]; - - if (callback) { - var result = data.error || data.message; - var responder = createResponder(e.source, data.topic, uuid); - callback(result, keepalive, responder); - - if (!keepalive) { - delete messages[uuid]; - } - } - - if (!data.error) { - try { - publish(e.source, data, keepalive); - } catch (err) { - post(e.source, null, err, uuid, false); - } - } - }, - false - ); +/** + * Helper closure to create a function that may be used to respond to a message + * @private + * @param {Window} win The window from which the message originated + * @param {String} channelId The "unique" ID of the original message + * @return {Function} A function that may be invoked to respond to the message + */ +function createResponder(win, channelId, sendToParent) { + return function respond(message, keepalive, callback) { + if (typeof callback === 'function') { + storeCallback({ channelId }, callback, sendToParent); + } + post(win, { + topic: null, + channelId, + message, + messageId: createMessageId(), + keepalive: !!keepalive, + sendToParent + }); + }; } - -export default respondable; diff --git a/lib/core/utils/respondable/assert-window.js b/lib/core/utils/respondable/assert-window.js new file mode 100644 index 0000000000..0a93487f36 --- /dev/null +++ b/lib/core/utils/respondable/assert-window.js @@ -0,0 +1,21 @@ +import assert from '../assert'; + +export function assertIsParentWindow(win) { + assetNotGlobalWindow(win); + assert( + window.parent === win, + 'Source of the response must be the parent window.' + ); +} + +export function assertIsFrameWindow(win) { + assetNotGlobalWindow(win); + const frames = Array.from(window.frames); + if (!frames.some(frame => frame === win)) { + throw new Error('Respondable target must be a frame in the current window'); + } +} + +export function assetNotGlobalWindow(win) { + assert(window !== win, 'Messages can not be sent to the same window.'); +} diff --git a/lib/core/utils/respondable/callback-store.js b/lib/core/utils/respondable/callback-store.js new file mode 100644 index 0000000000..dfbff8936f --- /dev/null +++ b/lib/core/utils/respondable/callback-store.js @@ -0,0 +1,36 @@ +import assert from '../assert'; +const subscribers = {}; +const channels = {}; + +export function storeCallback( + { topic, channelId }, + callback, + sendToParent = true +) { + if (topic) { + assert(!subscribers[topic], `Topic ${topic} is already registered to.`); + subscribers[topic] = { callback, sendToParent }; + } else if (channelId) { + assert( + !channels[channelId], + `A callback already exists for this message channel.` + ); + channels[channelId] = { callback, sendToParent }; + } +} + +export function getCallback({ topic, channelId }) { + if (topic) { + return subscribers[topic]; + } else if (channelId) { + return channels[channelId]; + } +} + +export function deleteCallback({ topic, channelId }) { + if (topic) { + delete subscribers[topic]; + } else if (channelId) { + delete channels[channelId]; + } +} diff --git a/lib/core/utils/respondable/message-id.js b/lib/core/utils/respondable/message-id.js new file mode 100644 index 0000000000..deb3e1cfa9 --- /dev/null +++ b/lib/core/utils/respondable/message-id.js @@ -0,0 +1,22 @@ +import { v4 as createUuid } from '../uuid'; + +const messageIds = []; + +export function createMessageId() { + const uuid = `${createUuid()}:${createUuid()}`; + // Prevent repeats + if (messageIds.includes(uuid)) { + return createMessageId(); + } + + messageIds.push(uuid); + return uuid; +} + +export function isNewMessage(uuid) { + if (messageIds.includes(uuid)) { + return false; + } + messageIds.push(uuid); + return true; +} diff --git a/lib/core/utils/respondable/message-parser.js b/lib/core/utils/respondable/message-parser.js new file mode 100644 index 0000000000..4905f2ffe2 --- /dev/null +++ b/lib/core/utils/respondable/message-parser.js @@ -0,0 +1,109 @@ +const errorTypes = Object.freeze([ + 'EvalError', + 'RangeError', + 'ReferenceError', + 'SyntaxError', + 'TypeError', + 'URIError' +]); + +export function stringifyMessage({ + topic, + channelId, + message, + messageId, + keepalive +}) { + const data = { + channelId, + topic, + messageId, + keepalive, + source: getSource() + }; + + if (message instanceof Error) { + data.error = { + name: message.name, + message: message.message, + stack: message.stack + }; + } else { + data.payload = message; + } + return JSON.stringify(data); +} + +/** + * Parse the received message for processing + * @param {string} dataString Message received + * @return {object} Object to be used for pub/sub + */ +export function parseMessage(dataString) { + let data; + try { + data = JSON.parse(dataString); + } catch (e) { + /* ignored */ + } + if (!isRespondableMessage(data)) { + return; + } + + const { topic, channelId, messageId, keepalive } = data; + const message = + typeof data.error === 'object' + ? buildErrorObject(data.error) + : data.payload; + + return { topic, message, messageId, channelId, keepalive }; +} + +/** + * Verify the received message is from the "respondable" module + * @private + * @param {Object} postedMessage The message received via postMessage + * @return {Boolean} `true` if the message is verified from respondable + */ +function isRespondableMessage(postedMessage) { + return ( + typeof postedMessage === 'object' && + typeof postedMessage.channelId === 'string' && + postedMessage.source === getSource() + ); +} + +/** + * Convert a javascript Error into something that can be stringified + * @param {Error} error Any type of error + * @return {Object} Processable object + */ +function buildErrorObject(error) { + let msg = error.message || 'Unknown error occurred'; + const errorName = errorTypes.includes(error.name) ? error.name : 'Error'; + const ErrConstructor = window[errorName] || Error; + + if (error.stack) { + msg += '\n' + error.stack.replace(error.message, ''); + } + return new ErrConstructor(msg); +} + +/** + * get the unique string to be used to identify our instance of axe + * @private + */ +function getSource() { + let application = 'axeAPI'; + let version = ''; + + // TODO: es-modules_audit + if (typeof axe !== 'undefined' && axe._audit && axe._audit.application) { + application = axe._audit.application; + } + if (typeof axe !== 'undefined') { + // TODO: es-modules-version + version = axe.version; + } + return application + '.' + version; +} diff --git a/lib/core/utils/respondable/post.js b/lib/core/utils/respondable/post.js new file mode 100644 index 0000000000..4928bfb938 --- /dev/null +++ b/lib/core/utils/respondable/post.js @@ -0,0 +1,32 @@ +import { stringifyMessage } from './message-parser'; +import { assertIsParentWindow, assertIsFrameWindow } from './assert-window'; + +/** + * Posts the message to correct frame. + * This abstraction necessary because IE9 & 10 do not support posting Objects; only strings + * @private + * @param {Window} win The `window` to post the message to + * @param {String} topic The topic of the message + * @param {Object} message The message content + * @param {String} uuid The UUID, or pseudo-unique ID of the message + * @param {Boolean} keepalive Whether to allow multiple responses - default is false + */ +export function post( + win, + { topic, message, messageId, channelId, keepalive, sendToParent } +) { + // Prevent messaging to an inappropriate window + sendToParent ? assertIsParentWindow(win) : assertIsFrameWindow(win); + if (message instanceof Error && !sendToParent) { + return axe.log(message); + } + // console.log({ topic, message, messageId, channelId, keepalive }) + const dataString = stringifyMessage({ + topic, + message, + messageId, + channelId, + keepalive + }); + win.postMessage(dataString, '*'); +} diff --git a/lib/core/utils/uuid.js b/lib/core/utils/uuid.js index 231085651e..b52b113c3d 100644 --- a/lib/core/utils/uuid.js +++ b/lib/core/utils/uuid.js @@ -23,6 +23,15 @@ if (!_rng && _crypto && _crypto.getRandomValues) { }; } +try { + if (!_rng && require) { + const nodeCrypto = require('crypto'); + _rng = () => nodeCrypto.randomBytes(16); + } +} catch (e) { + /* do nothing */ +} + if (!_rng) { // Math.random()-based (RNG) // diff --git a/package-lock.json b/package-lock.json index d6606947ea..8d33094ec2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.1.2", + "version": "4.1.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4358,6 +4358,16 @@ "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", "dev": true }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true, + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6176,6 +6186,12 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true + }, "is-observable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", @@ -7473,6 +7489,12 @@ } } }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -7962,6 +7984,12 @@ "xtend": "^4.0.0" } }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, "morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -8823,6 +8851,17 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, + "proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", diff --git a/package.json b/package.json index eacbc800fc..62b11a99d8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "axe-core", "description": "Accessibility engine for automated Web UI testing", - "version": "4.1.2", + "version": "4.1.3", "license": "MPL-2.0", "engines": { "node": ">=4" @@ -127,6 +127,7 @@ "mocha-headless-chrome": "^2.0.3", "node-notifier": "^7.0.1", "prettier": "^1.17.1", + "proxyquire": "^2.1.3", "puppeteer": "^2.0.0", "revalidator": "~0.3.1", "selenium-webdriver": "~3.6.0", diff --git a/sri-history.json b/sri-history.json index 09ff335619..68ebcc1946 100644 --- a/sri-history.json +++ b/sri-history.json @@ -214,5 +214,9 @@ "4.1.2": { "axe.js": "sha256-wnrhK+P+FXp6U/oYQrYVu6/6PrpO0chRAkik/2ENuS0=", "axe.min.js": "sha256-TzERMakAkktf5sw0quZYuHVmniguowRhWRIVYpjbLbA=" + }, + "4.1.3": { + "axe.js": "sha256-T1FukaTlhjibBqAm8SBwYmo8GLAHdn45r52XRllMdv8=", + "axe.min.js": "sha256-2Z3/OrInIQjAzZ5kMvTBZ2CDUiAgUG/2J5ca1XpNfYw=" } } diff --git a/test/checks/media/frame-tested.js b/test/checks/media/frame-tested.js index cec7e85efa..187eb13187 100644 --- a/test/checks/media/frame-tested.js +++ b/test/checks/media/frame-tested.js @@ -18,7 +18,7 @@ describe('frame-tested', function() { }); it('passes if the iframe contains axe-core', function(done) { - iframe.src = '/test/mock/frames/responder.html'; + iframe.src = '/test/mock/frames/test.html'; iframe.addEventListener('load', function() { checkContext._onAsync = function(result) { assert.isTrue(result); diff --git a/test/core/public/cleanup.js b/test/core/public/cleanup.js index 92017cce0c..e6ff6baf68 100644 --- a/test/core/public/cleanup.js +++ b/test/core/public/cleanup.js @@ -5,7 +5,7 @@ describe('cleanup', function() { function createFrames(callback) { var frame; frame = document.createElement('iframe'); - frame.src = '../mock/frames/responder.html'; + frame.src = '../mock/frames/test.html'; frame.addEventListener('load', function() { setTimeout(callback, 500); }); @@ -93,7 +93,7 @@ describe('cleanup', function() { win.addEventListener('message', function(message) { var data = JSON.parse(message.data); if (data.topic === 'axe.start') { - assert.deepEqual(data.message, { command: 'cleanup-plugin' }); + assert.deepEqual(data.payload, { command: 'cleanup-plugin' }); done(); } }); diff --git a/test/core/public/load.js b/test/core/public/load.js index 4dfebc9797..69043d8465 100644 --- a/test/core/public/load.js +++ b/test/core/public/load.js @@ -1,7 +1,8 @@ describe('axe._load', function() { - 'use strict'; + var fixture = document.querySelector('#fixture'); + var captureError = axe.testUtils.captureError; + var isIE11 = axe.testUtils.isIE11; - // var Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; afterEach(function() { axe._audit = null; }); @@ -31,23 +32,34 @@ describe('axe._load', function() { }); describe('respondable subscriber', function() { - it('should add a respondable subscriber for axe.ping', function(done) { - var mockAudit = { - rules: [{ id: 'monkeys' }, { id: 'bananas' }] - }; + (isIE11 ? it.skip : it)( + // In IE win.parent is read-only + 'should add a respondable subscriber for axe.ping', + function(done) { + var winParent = window.parent; + var mockAudit = { + rules: [{ id: 'monkeys' }, { id: 'bananas' }] + }; - axe._load(mockAudit); + axe._load(mockAudit); - var win = { - postMessage: function(message) { - var data = JSON.parse(message); - assert.deepEqual(data.message, { axe: true }); - done(); - } - }; + var frame = document.createElement('iframe'); + frame.src = '../mock/frames/test.html'; + frame.addEventListener('load', function() { + var win = frame.contentWindow; + window.parent = win; + win.postMessage = captureError(function(message) { + var data = JSON.parse(message); + assert.deepEqual(data.payload, { axe: true }); + window.parent = winParent; + done(); + }, done); + axe.utils.respondable(win, 'axe.ping', { axe: true }); + }); - axe.utils.respondable._publish(win, { topic: 'axe.ping' }); - }); + fixture.appendChild(frame); + } + ); describe('given command rules', function() { // todo: see issue - https://github.com/dequelabs/axe-core/issues/2168 diff --git a/test/core/public/run.js b/test/core/public/run.js index af8d2ff09e..9f984a7c58 100644 --- a/test/core/public/run.js +++ b/test/core/public/run.js @@ -392,6 +392,9 @@ describe('axe.run iframes', function() { var fixture = document.getElementById('fixture'); var origRunRules = axe._runRules; + var captureError = axe.testUtils.captureError; + + this.timeout(1000); beforeEach(function() { fixture.innerHTML = '
Target in top frame
'; @@ -422,37 +425,33 @@ describe('axe.run iframes', function() { it('includes iframes by default', function(done) { var frame = document.createElement('iframe'); - frame.addEventListener('load', function() { - var safetyTimeout = window.setTimeout(function() { - done(); - }, 1000); - - axe.run('#fixture', {}, function(err, result) { - assert.equal(result.violations.length, 1); - var violation = result.violations[0]; - assert.equal( - violation.nodes.length, - 2, - 'one node for top frame, one for iframe' - ); - assert.isTrue( - violation.nodes.some(function(node) { - return node.target.length === 1 && node.target[0] === '#target'; - }), - 'one result from top frame' - ); - assert.isTrue( - violation.nodes.some(function(node) { - return ( - node.target.length === 2 && node.target[0] === '#fixture > iframe' - ); - }), - 'one result from iframe' - ); - window.clearTimeout(safetyTimeout); - done(); - }); + axe.run( + '#fixture', + {}, + captureError(function(err, result) { + assert.equal(result.violations.length, 1); + var violation = result.violations[0]; + assert.equal( + violation.nodes.length, + 2, + 'one node for top frame, one for iframe' + ); + assert.isTrue( + violation.nodes.some(function(node) { + return node.target.length === 1 && node.target[0] === '#target'; + }), + 'one result from top frame' + ); + assert.isTrue( + violation.nodes.some(function(node) { + return node.target.length === 2 && node.target[0] === 'iframe'; + }), + 'one result from iframe' + ); + done(); + }, done) + ); }); frame.src = '../mock/frames/test.html'; @@ -461,21 +460,19 @@ describe('axe.run iframes', function() { it('excludes iframes if iframes is false', function(done) { var frame = document.createElement('iframe'); - frame.addEventListener('load', function() { - var safetyTimeout = setTimeout(function() { - done(); - }, 1000); - - axe.run('#fixture', { iframes: false }, function(err, result) { - assert.equal(result.violations.length, 1); - var violation = result.violations[0]; - assert.equal(violation.nodes.length, 1, 'only top frame'); - assert.equal(violation.nodes[0].target.length, 1); - assert.equal(violation.nodes[0].target[0], '#target'); - window.clearTimeout(safetyTimeout); - done(); - }); + axe.run( + '#fixture', + { iframes: false }, + captureError(function(err, result) { + assert.equal(result.violations.length, 1); + var violation = result.violations[0]; + assert.equal(violation.nodes.length, 1, 'only top frame'); + assert.equal(violation.nodes[0].target.length, 1); + assert.equal(violation.nodes[0].target[0], '#target'); + done(); + }, done) + ); }); frame.src = '../mock/frames/test.html'; @@ -484,18 +481,16 @@ describe('axe.run iframes', function() { it('ignores unexpected messages from non-axe iframes', function(done) { var frame = document.createElement('iframe'); - frame.addEventListener('load', function() { - var safetyTimeout = window.setTimeout(function() { - done('timeout'); - }, 1000); - - axe.run('#fixture', {}, function(err, result) { - assert.isNull(err); - assert.equal(result.violations.length, 1); - window.clearTimeout(safetyTimeout); - done(); - }); + axe.run( + '#fixture', + {}, + captureError(function(err, result) { + assert.isNull(err); + assert.equal(result.violations.length, 1); + done(); + }, done) + ); }); frame.src = '../mock/frames/with-echo.html'; @@ -506,18 +501,15 @@ describe('axe.run iframes', function() { var frame = document.createElement('iframe'); frame.addEventListener('load', function() { - var safetyTimeout = window.setTimeout(function() { - done('timeout'); - }, 1000); - if (!axe._audit) { - throw new Error('no _audit'); - } - axe.run('#fixture', {}, function(err, result) { - assert.isNull(err); - assert.equal(result.violations.length, 1); - window.clearTimeout(safetyTimeout); - done(); - }); + axe.run( + '#fixture', + {}, + captureError(function(err, result) { + assert.isNull(err); + assert.equal(result.violations.length, 1); + done(); + }, done) + ); }); frame.src = '../mock/frames/with-echo-axe.html'; diff --git a/test/core/utils/collect-results-from-frames.js b/test/core/utils/collect-results-from-frames.js index fb71b37bb3..2b4f977689 100644 --- a/test/core/utils/collect-results-from-frames.js +++ b/test/core/utils/collect-results-from-frames.js @@ -143,9 +143,11 @@ describe('axe.utils.collectResultsFromFrames', function() { axe.utils.collectResultsFromFrames( context, {}, - 'command', - 'params', - noop, + 'rules', + 'morestuff', + function() { + done(new Error('Should not be called')); + }, function(err) { assert.instanceOf(err, Error); assert.equal(err.message.split(/\n/)[0], 'error in axe.throw'); diff --git a/test/core/utils/respondable.js b/test/core/utils/respondable.js index 5d2c31f7ca..1ed7d1deff 100644 --- a/test/core/utils/respondable.js +++ b/test/core/utils/respondable.js @@ -1,515 +1,568 @@ +function afterMessage(win, callback) { + var handler = function() { + win.removeEventListener('message', handler); + // Wait one more tick for stuff to resolve + setTimeout(function() { + callback(); + }, 10); + }; + win.addEventListener('message', handler); +} + +function once(callback) { + var called = false; + return function() { + if (!called) { + callback.apply(this, arguments); + } + called = true; + }; +} + describe('axe.utils.respondable', function() { - 'use strict'; + var fixture, + axeVersion, + axeApplication, + frame, + frameWin, + respondable, + frameSubscribe, + axeLog; + var postMessage = window.postMessage; + var captureError = axe.testUtils.captureError; + var isIE11 = axe.testUtils.isIE11; + this.timeout(1000); + + beforeEach(function(done) { + axe._load({}); + respondable = axe.utils.respondable; + axeVersion = axe.version; + axeLog = axe.log; + axeApplication = axe._audit.application; + + frame = document.createElement('iframe'); + frame.src = '../mock/frames/test.html'; + frame.addEventListener('load', function() { + frameWin = frame.contentWindow; + frameSubscribe = frameWin.axe.utils.respondable.subscribe; + done(); + }); - it('should be a function', function() { - assert.isFunction(axe.utils.respondable); + fixture = document.querySelector('#fixture'); + fixture.appendChild(frame); }); - it('should accept 5 parameters', function() { - assert.lengthOf(axe.utils.respondable, 5); + afterEach(function() { + fixture.innerHTML = ''; + axe.version = axeVersion; + axe._audit.application = axeApplication; + axe.log = axeLog; + window.postMessage = postMessage; }); - it('should call `postMessage` on first parameter', function() { - var success = false; - var win = { - postMessage: function() { - success = true; - } - }; - - axe.utils.respondable(win, 'batman', 'nananana'); - assert.isTrue(success); + it('can be subscribed to', function(done) { + frameSubscribe('greeting', function() { + done(); + }); + respondable(frameWin, 'greeting', 'hello'); }); - it('should stringify message', function(done) { - var win = { - postMessage: function(msg) { - assert.isString(msg); + it('forwards the message', function(done) { + var expected = { hello: 'world' }; + frameSubscribe( + 'greeting', + captureError(function(actual) { + assert.deepEqual(actual, expected); done(); - } - }; - - axe.utils.respondable(win, 'batman', { derp: true }); + }, done) + ); + respondable(frameWin, 'greeting', expected); }); - it('should add the `topic` and `message` the message', function(done) { - var win = { - postMessage: function(msg) { - msg = JSON.parse(msg); - - assert.equal(msg.topic, 'batman'); - assert.isTrue(msg._respondable); + it('passes a truthy keepalive value', function(done) { + frameSubscribe( + 'greeting', + captureError(function(_, keepalive) { + assert.isTrue(keepalive); done(); - } - }; - - axe.utils.respondable(win, 'batman', 'nananana'); + }, done) + ); + respondable(frameWin, 'greeting', 'hello', 'truthy'); }); - it('should add the `keepalive`', function(done) { - var win = { - postMessage: function(msg) { - msg = JSON.parse(msg); - - assert.equal(msg._keepalive, 'batman'); + it('passes a falsy keepalive value', function(done) { + frameSubscribe( + 'greeting', + captureError(function(_, keepalive) { + assert.isFalse(keepalive); done(); - } - }; + }, done) + ); + respondable(frameWin, 'greeting', 'hello', 0); + }); - axe.utils.respondable(win, 'superman', 'spidey', 'batman'); + it('can not publish to a parent frame', function(done) { + var isCalled = false; + axe.utils.respondable.subscribe('greeting', function() { + isCalled = true; + }); + assert.throws(function() { + frameWin.axe.utils.respondable(window, 'greeting', 'hello', 0); + }); + setTimeout( + captureError(function() { + assert.isFalse(isCalled); + done(); + }, done), + 100 + ); }); - it('should add `_respondable` to the message', function(done) { - var win = { - postMessage: function(msg) { - msg = JSON.parse(msg); + it('does not expose private methods', function() { + var methods = Object.keys(respondable).sort(); + assert.deepEqual(methods, ['subscribe', 'isInFrame'].sort()); + }); - assert.equal(msg._respondable, true); + it('passes serialized information only', function(done) { + var div = document.createElement('div'); + frameSubscribe( + 'greeting', + captureError(function(message) { + assert.deepEqual(message, {}); done(); - } - }; + }, done) + ); - axe.utils.respondable(win, 'batman', 'nananana'); + respondable(frameWin, 'greeting', div); }); - describe('messageHandler', function() { - var event = document.createEvent('Event'); - // Define that the event name is 'build'. - event.initEvent('message', true, true); - event.source = window; - - var eventData; - var win; - var axeVersion; - - beforeEach(function() { - axeVersion = axe.version; - win = { - postMessage: function(message) { - var data = JSON.parse(message); - eventData.uuid = data.uuid; - event.data = JSON.stringify(eventData); - document.dispatchEvent(event); - } - }; - }); - - afterEach(function() { - axe.version = axeVersion; + describe('subscribe', function() { + it('is called with the same topic', function(done) { + var called = false; + frameSubscribe('greeting', function() { + called = true; + }); + respondable(frameWin, 'greeting'); + afterMessage( + frameWin, + captureError(function() { + assert.isTrue(called); + done(); + }, done) + ); }); - it('should pass messages that have all required properties', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - message: 'Help us Obi-Wan', - _axeuuid: 'otherAxe' - }; - - axe.utils.respondable(win, 'Death star', null, true, function(data) { - assert.equal(data, 'Help us Obi-Wan'); - done(); + it('is not called on a different topic', function(done) { + var called = false; + frameSubscribe('otherTopic', function() { + called = true; }); + respondable(frameWin, 'greeting'); + afterMessage( + frameWin, + captureError(function() { + assert.isFalse(called); + done(); + }, done) + ); }); - it('should reject messages if the axe version is different', function(done) { + it('is not called for different axe-core versions', function(done) { + var called = false; axe.version = '1.0.0'; - eventData = { - _respondable: true, - _source: 'axeAPI.2.0.0', - message: 'Help us Obi-Wan', - _axeuuid: 'otherAxe' - }; - - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + frameSubscribe('greeting', function() { + called = true; }); - - setTimeout(function() { - done(); - }, 100); + respondable(frameWin, 'greeting'); + afterMessage( + frameWin, + captureError(function() { + assert.isFalse(called); + done(); + }, done) + ); }); - it('should reject messages if the axe version is x.y.z', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.x.y.z', - message: 'Help us Obi-Wan', - _axeuuid: 'otherAxe' - }; + it('is not called with the "x.y.z" wildcard', function(done) { + var called = false; + axe.version = 'x.y.z'; + frameSubscribe('greeting', function() { + called = true; + }); + respondable(frameWin, 'greeting'); + afterMessage( + frameWin, + captureError(function() { + assert.isFalse(called); + done(); + }, done) + ); + }); - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + it('is not called for different applications', function(done) { + var called = false; + axe._audit.application = 'Coconut'; + frameSubscribe('greeting', function() { + called = true; }); + respondable(frameWin, 'greeting'); + afterMessage( + frameWin, + captureError(function() { + assert.isFalse(called); + done(); + }, done) + ); + }); - setTimeout(function() { + it('logs errors passed to respondable, rather than passing them on', function(done) { + axe.log = captureError(function(e) { + assert.equal(e.message, 'expected message'); done(); - }, 100); - }); + }, done); - it('should reject messages that are that are not strings', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - message: 'Help us Obi-Wan', - _axeuuid: 'otherAxe' - }; + frameSubscribe('greeting', function() { + done(new Error('subscribe should not be called')); + }); + respondable(frameWin, 'greeting', new Error('expected message')); + }); - win = { - postMessage: function(message) { - var data = JSON.parse(message); - eventData.uuid = data.uuid; - event.data = eventData; - document.dispatchEvent(event); - } - }; + (isIE11 ? it.skip : it)( + // In IE win.parent is read-only + 'is not called when the source is not a frame in the page', + function(done) { + var doneOnce = once(done); + var called = false; + frameWin.axe.log = function() { + called = true; + }; + + frameSubscribe('greeting', function() { + doneOnce(new Error('subscribe should not be called')); + }); + frameWin.parent = frameWin; + respondable(frameWin, 'greeting'); + setTimeout( + captureError(function() { + assert.isTrue(called); + doneOnce(); + }, doneOnce), + 100 + ); + } + ); - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + it('throws when targeting itself', function() { + assert.throws(function() { + respondable(window, 'greeting'); + }); + assert.throws(function() { + frameWin.respondable(frameWin, 'greeting'); }); - - setTimeout(function() { - done(); - }, 100); }); - it('should reject messages that are invalid stringified objects', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - message: 'Help us Obi-Wan', - _axeuuid: 'otherAxe' - }; + it('throws when targeting a window that is not a frame in the page', function() { + var blankPage = window.open(''); + var frameCopy = window.open(frameWin.location.href); - win = { - postMessage: function(message) { - var data = JSON.parse(message); - eventData.uuid = data.uuid; - event.data = JSON.stringify(eventData) + 'joker tricks!'; - document.dispatchEvent(event); - } - }; - - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + // seems our non-karma tests can't open new windows? + if (!blankPage) { + return; + } + // Cleanup + setTimeout(function() { + blankPage.close(); + frameCopy.close(); }); - setTimeout(function() { - done(); - }, 100); + assert.throws(function() { + respondable(blankPage, 'greeting'); + }); + assert.throws(function() { + respondable(frameCopy, 'greeting'); + }); }); - it('should reject messages that do not have a uuid', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - message: 'Help us Obi-Wan', - _axeuuid: 'otherAxe' - }; + it('is not triggered by "repeaters"', function(done) { + var calls = 0; + frameSubscribe('greeting', function() { + calls++; + }); + // Repeat fire the event + frameWin.addEventListener('message', function handler(evt) { + frameWin.postMessage(evt.data, '*'); + frameWin.removeEventListener('message', handler); + }); - win = { - postMessage: function() { - event.data = JSON.stringify(eventData); - document.dispatchEvent(event); - } - }; + respondable(frameWin, 'greeting', 'hello'); + setTimeout( + captureError(function() { + assert.equal(calls, 1); + done(); + }, done), + 100 + ); + }); + }); - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + describe('respond', function() { + it('passes the response back', function(done) { + var receivedResponse; + frameSubscribe( + 'greeting', + captureError(function(message, keepalive, respond) { + assert.isFalse(keepalive); + respond({ greet: 'bonjour' }); + }, done) + ); + + respondable(frameWin, 'greeting', 'hello', false, function(message) { + receivedResponse = message; }); - setTimeout(function() { - done(); - }, 100); + afterMessage( + window, + captureError(function() { + assert.deepEqual(receivedResponse, { greet: 'bonjour' }); + done(); + }, done) + ); }); - it('should reject messages that do not have a matching uuid', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - message: 'Help us Obi-Wan', - _axeuuid: 'otherAxe' - }; - - win = { - postMessage: function(message) { - var data = JSON.parse(message); - eventData.uuid = data.uuid + 'joker tricks!'; - event.data = JSON.stringify(eventData); - document.dispatchEvent(event); - } - }; + it('prohibits multiple response calls when respond sets keepalive to false', function(done) { + var receivedResponse; + frameSubscribe('greeting', function(message, keepalive, respond) { + assert.isTrue(keepalive); + respond({ responseNum: 1 }, false); + setTimeout(function() { + respond({ responseNum: 2 }); + }, 10); + }); - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + respondable(frameWin, 'greeting', '', true, function(message) { + receivedResponse = message; }); - setTimeout(function() { - done(); - }, 100); + afterMessage(window, function() { + assert.deepEqual(receivedResponse, { responseNum: 1 }); + setTimeout(function() { + assert.deepEqual(receivedResponse, { responseNum: 1 }); + done(); + }, 100); + }); }); - it('should reject messages that do not have `_respondable: true`', function(done) { - eventData = { - _source: 'axeAPI.' + axe.version, - message: 'Help us Obi-Wan', - _axeuuid: 'otherAxe' - }; + it('allows multiple response calls with keepalive: true', function(done) { + var receivedResponse; + frameSubscribe('greeting', function(message, keepalive, respond) { + assert.isTrue(keepalive); + respond({ responseNum: 1 }, true); + setTimeout(function() { + respond({ responseNum: 2 }); + }, 30); + }); - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + respondable(frameWin, 'greeting', '', true, function(message) { + receivedResponse = message; }); - setTimeout(function() { - done(); - }, 100); + afterMessage(window, function() { + assert.deepEqual(receivedResponse, { responseNum: 1 }); + afterMessage(window, function() { + assert.deepEqual(receivedResponse, { responseNum: 2 }); + done(); + }); + }); }); - it('should reject messages that do not have `_axeuuid`', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - message: 'Help us Obi-Wan' - }; - - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + it('responds until after keepalive: false is called', function(done) { + var concat = ''; + frameSubscribe('greeting', function(_, keepalive, respond) { + respond('1', true); + respond('2', true); + respond('3', false); + respond('4', true); + respond('5', false); }); + respondable(frameWin, 'greeting', '', true, function(result) { + concat += result; + }); setTimeout(function() { + assert.equal(concat, '123'); done(); - }, 100); + }, 200); }); - it('should reject messages from the same axe instance (`_axeuuid`)', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - message: 'Help us Obi-Wan' - }; - - win = { - postMessage: function(message) { - var data = JSON.parse(message); - eventData.uuid = data.uuid; - eventData._axeuuid = data._axeuuid; - event.data = JSON.stringify(eventData); - document.dispatchEvent(event); - } - }; - - axe.utils.respondable(win, 'Death star', null, true, function() { - done(new Error('should not call callback')); + it('receives errors if the subscriber throws', function(done) { + var errorMessage = 'Something went wrong'; + frameSubscribe('greeting', function() { + throw new frameWin.TypeError(errorMessage); }); - setTimeout(function() { - done(); - }, 100); + respondable( + frameWin, + 'greeting', + '', + true, + captureError(function(result) { + assert.instanceOf(result, TypeError); + assert.equal(result.message.split(/\n/)[0], errorMessage); + done(); + }, done) + ); }); - it('should throw if an error message was send', function(done) { - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - error: { - name: 'ReferenceError', - message: 'The exhaust port is open!', - trail: '... boom' - }, - _axeuuid: 'otherAxe' - }; + it('receives errors responded by the subscriber', function(done) { + var errorMessage = 'Something went wrong'; + frameSubscribe('greeting', function(data, keepalive, respond) { + respond(new frameWin.TypeError(errorMessage)); + }); - axe.utils.respondable(win, 'Death star', null, true, function(data) { - assert.instanceOf(data, ReferenceError); - assert.equal(data.message, 'The exhaust port is open!'); + respondable(frameWin, 'greeting', '', true, function(result) { + assert.instanceOf(result, TypeError); + assert.equal(result.message.split(/\n/)[0], errorMessage); done(); }); }); - it('should create an Error if an invalid error type is passed', function(done) { - window.evil = function() {}; - - eventData = { - _respondable: true, - _source: 'axeAPI.' + axe.version, - error: { - name: 'evil', - message: 'The exhaust port is open!', - trail: '... boom' - }, - _axeuuid: 'otherAxe' - }; - - axe.utils.respondable(win, 'Death star', null, true, function(data) { - assert.instanceOf(data, Error); - assert.equal(data.message, 'The exhaust port is open!'); - window.evil = undefined; - done(); - }); + it('can pass messages back to the subscriber (without triggering the subscriber)', function(done) { + frameSubscribe( + 'greeting', + captureError(function(message, _, respond) { + assert.equal(message, '1'); + respond('2', true, function(message) { + assert.equal(message, '3'); + done(); + }); + }, done) + ); + + respondable( + frameWin, + 'greeting', + '1', + false, + captureError(function(message, _, respond) { + assert.equal(message, '2'); + respond('3'); + }, done) + ); }); - }); - it('uses respondable.isInFrame() to check if the page is in a frame or not', function() { - assert.equal(axe.utils.respondable.isInFrame(), !!window.frameElement); + it('it errors if multiple callbacks are registered', function(done) { + var calledFirst = captureError(function(message, _, respond) { + respond('2a', true, calledThird); + assert.throws(function() { + respond('2b', true, function() { + done(new Error('Should never be called')); + }); + }); + }, done); + + var calledSecond = captureError(function(message, _, respond) { + assert.equal(message, '2a'); + respond('3a'); + }, done); + + var calledThird = captureError(function(message) { + assert.equal(message, '3a'); + setTimeout(function() { + done(); // No further messages received + }, 50); + }, done); - assert.isFalse( - axe.utils.respondable.isInFrame({ - frameElement: null - }) - ); - assert.isTrue( - axe.utils.respondable.isInFrame({ - frameElement: document.createElement('iframe') - }) - ); - }); + frameSubscribe('greeting', calledFirst); + respondable(frameWin, 'greeting', '1', false, calledSecond); + }); - describe('subscribe', function() { - var origAxeUUID = axe._uuid; - var counter = 0; - - before(function() { - // assign axe a new uuid every time it's requested to trick - // the code that each respondable was called from a different - // context - Object.defineProperty(axe, '_uuid', { - get: function() { - return ++counter; - } + it('logs errors in respondable callbacks', function(done) { + var doneOnce = once(done); + var logged = false; + axe.log = function(e) { + logged = true; + assert.equal(e.message, 'This should not go to the frame'); + }; + + frameSubscribe( + 'greeting', + captureError(function(message, _, respond) { + assert.equal(message, '1'); + + respond('2', true, function() { + doneOnce(new Error('should not call callback')); + }); + }, doneOnce) + ); + + respondable(frameWin, 'greeting', '1', false, function(message) { + assert.equal(message, '2'); + setTimeout( + captureError(function() { + assert.isTrue(logged); + doneOnce(); + }, done), + 100 + ); + + throw new Error('This should not go to the frame'); }); }); - after(function() { - Object.defineProperty(axe, '_uuid', { - value: origAxeUUID + it('is not called if the frame is not in the page', function(done) { + var receivedResponse; + frameSubscribe('greeting', function(message, keepalive, respond) { + respond({ greet: 'ola' }); }); - }); - it('should be a function', function() { - assert.isFunction(axe.utils.respondable.subscribe); - }); + respondable(frameWin, 'greeting', 'hello', false, function(message) { + receivedResponse = message; + fixture.innerHTML = ''; + }); - it('should receive messages', function(done) { - var expected = null; - axe.utils.respondable.subscribe('catman', function(data) { - assert.equal(data, expected); - if (data === 'yay') { - done(); - } - }); - axe.utils.respondable(window, 'catman', null, undefined, function( - data, - keepalive, - respond - ) { - assert.isNull(data); - setTimeout(function() { - respond('yay'); - expected = 'yay'; - }, 0); + afterMessage(window, function() { + assert.deepEqual(receivedResponse, { greet: 'ola' }); + done(); }); }); - it('should propagate the keepalive setting', function(done) { - var expected = null; - axe.utils.respondable.subscribe('catman', function(data, keepalive) { - assert.equal(keepalive, expected); - if (data === 'yayyay') { - done(); - } - }); - axe.utils.respondable(window, 'catman', null, undefined, function( - data, - keepalive, - respond - ) { - assert.isNull(data); - setTimeout(function() { - expected = 'keepy'; - respond('yayyay', expected); - }, 0); + it('is not triggered by "repeaters"', function(done) { + // Repeat fire the event + window.addEventListener('message', function handler(evt) { + frameWin.parent.postMessage(evt.data, '*'); + window.removeEventListener('message', handler); }); - }); - it('should allow multiple responses when keepalive', function(done) { - var expected = 2; - var called = 0; - axe.utils.respondable.subscribe('catman', function(data) { - if (data === 'yayyayyay') { - called += 1; - if (called === expected) { - done(); - } - } - }); - axe.utils.respondable(window, 'catman', null, undefined, function( - data, - keepalive, - respond - ) { - assert.isNull(data); - setTimeout(function() { - respond('yayyayyay', true); - }, 0); - setTimeout(function() { - respond('yayyayyay', true); - }, 100); + frameSubscribe('greeting', function(message, _, respond) { + respond('2', true); }); - }); - it('does not trigger for error messages', function(done) { - var published = false; - axe.utils.respondable.subscribe('catman', function() { - published = true; + var calls = 0; + respondable(frameWin, 'greeting', '1', false, function() { + calls++; }); - var err = new ReferenceError('whoopsy'); - axe.utils.respondable(window, 'catman', err); - setTimeout(function() { - assert.ok(!published, 'Error events should not trigger'); - done(); - }, 10); + setTimeout( + captureError(function() { + assert.equal(calls, 1); + done(); + }, done), + 100 + ); }); + }); - it('returns an error if the subscribe method responds with an error', function(done) { - var expected = 'Expected owlman to be batman'; - var wait = true; - axe.utils.respondable.subscribe('owlman', function( - data, - keepalive, - respond - ) { - wait = false; - respond(new TypeError(expected)); - }); - - axe.utils.respondable(window, 'owlman', 'help!', true, function(data) { - if (!wait) { - assert.instanceOf(data, TypeError); - assert.equal(data.message.split(/\n/)[0], expected); - done(); - } - }); + describe('isInFrame', function() { + it('is false for the page window', function() { + var frameRespondable = frameWin.axe.utils.respondable; + assert.isFalse(respondable.isInFrame()); + assert.isFalse(frameRespondable.isInFrame(window)); }); - it('returns an error if the subscribe method throws', function(done) { - var wait = true; - var expected = 'Expected owlman to be batman'; - axe.utils.respondable.subscribe('owlman', function() { - wait = false; - throw new TypeError(expected); - }); - - // use keepalive, because we're running on the same window, - // otherwise it would delete the response before subscribe - // gets to react - axe.utils.respondable(window, 'owlman', null, true, function(data) { - if (!wait) { - assert.instanceOf(data, TypeError); - assert.equal(data.message.split(/\n/)[0], expected); - done(); - } - }); + it('is true for iframes', function() { + var frameRespondable = frameWin.axe.utils.respondable; + assert.isTrue(frameRespondable.isInFrame()); + assert.isTrue(respondable.isInFrame(frameWin)); }); }); }); diff --git a/test/core/utils/send-command-to-frame.js b/test/core/utils/send-command-to-frame.js index 83edabf035..3d2fa51423 100644 --- a/test/core/utils/send-command-to-frame.js +++ b/test/core/utils/send-command-to-frame.js @@ -2,6 +2,8 @@ describe('axe.utils.sendCommandToFrame', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var params = { command: 'rules' }; + var captureError = axe.testUtils.captureError; afterEach(function() { fixture.innerHTML = ''; @@ -13,6 +15,28 @@ describe('axe.utils.sendCommandToFrame', function() { assert.ok(false, 'should not be called'); }; + it('should return results from frames', function(done) { + var frame = document.createElement('iframe'); + frame.addEventListener('load', function() { + axe.utils.sendCommandToFrame( + frame, + params, + captureError(function(res) { + assert.lengthOf(res, 1); + assert.equal(res[0].id, 'html'); + done(); + }, done), + function() { + done(new Error('sendCommandToFrame should not error')); + } + ); + }); + + frame.id = 'level0'; + frame.src = '../mock/frames/test.html'; + fixture.appendChild(frame); + }); + it('should timeout if there is no response from frame', function(done) { var orig = window.setTimeout; window.setTimeout = function(fn, to) { @@ -31,7 +55,7 @@ describe('axe.utils.sendCommandToFrame', function() { axe._tree = axe.utils.getFlattenedTree(document.documentElement); axe.utils.sendCommandToFrame( frame, - {}, + params, function(result) { assert.equal(result, null); done(); @@ -45,82 +69,4 @@ describe('axe.utils.sendCommandToFrame', function() { frame.src = '../mock/frames/zombie-frame.html'; fixture.appendChild(frame); }); - - it('should respond once when no keepalive', function(done) { - var frame = document.createElement('iframe'); - frame.addEventListener('load', function() { - axe.utils.sendCommandToFrame( - frame, - { - number: 1 - }, - function() { - assert.isTrue(true); - done(); - }, - assertNotCalled - ); - }); - - frame.id = 'level0'; - frame.src = '../mock/frames/responder.html'; - fixture.appendChild(frame); - }); - - it('should respond multiple times when keepalive', function(done) { - var number = 3; - var called = 0; - var frame = document.createElement('iframe'); - frame.addEventListener('load', function() { - setTimeout(function() { - axe.utils.sendCommandToFrame( - frame, - { - number: number, - keepalive: true - }, - function() { - called += 1; - if (called === number) { - assert.isTrue(true); - done(); - } - }, - assertNotCalled - ); - }, 500); - }); - - frame.id = 'level0'; - frame.src = '../mock/frames/responder.html'; - fixture.appendChild(frame); - }); - - it('should respond once when no keepalive', function(done) { - var number = 1; - var called = 0; - var frame = document.createElement('iframe'); - frame.addEventListener('load', function() { - axe.utils.sendCommandToFrame( - frame, - { - number: number - }, - function() { - called += 1; - if (called === number) { - assert.isTrue(true); - done(); - } else { - throw new Error('should not have been called'); - } - }, - assertNotCalled - ); - }); - - frame.id = 'level0'; - frame.src = '../mock/frames/responder.html'; - fixture.appendChild(frame); - }); }); diff --git a/test/integration/full/configure-options/configure-options.js b/test/integration/full/configure-options/configure-options.js index 0ab804655a..d80a60f73b 100644 --- a/test/integration/full/configure-options/configure-options.js +++ b/test/integration/full/configure-options/configure-options.js @@ -1,18 +1,18 @@ describe('Configure Options', function() { 'use strict'; - var target = document.querySelector('#target'); + var target = document.querySelector('#target'); - afterEach(function() { - axe.reset(); - target.innerHTML = ''; - }); + afterEach(function() { + axe.reset(); + target.innerHTML = ''; + }); - describe('Check', function() { - describe('aria-allowed-attr', function() { - it('should allow an attribute supplied in options', function(done) { - target.setAttribute('role', 'separator'); - target.setAttribute('aria-valuenow', '0'); + describe('Check', function() { + describe('aria-allowed-attr', function() { + it('should allow an attribute supplied in options', function(done) { + target.setAttribute('role', 'separator'); + target.setAttribute('aria-valuenow', '0'); axe.configure({ checks: [ @@ -152,122 +152,123 @@ describe('Configure Options', function() { assert.lengthOf(results.passes, 1, 'passes'); assert.equal(results.passes[0].id, 'html-has-lang'); - assert.lengthOf(results.violations, 0, 'violations'); - assert.lengthOf(results.incomplete, 0, 'incomplete'); - assert.lengthOf(results.inapplicable, 0, 'inapplicable'); - done(); - }); - }); - }); + assert.lengthOf(results.violations, 0, 'violations'); + assert.lengthOf(results.incomplete, 0, 'incomplete'); + assert.lengthOf(results.inapplicable, 0, 'inapplicable'); + done(); + }); + }); + }); - describe('noHtml', function() { - it('prevents html property on nodes', function(done) { - target.setAttribute('role', 'slider'); - axe.configure({ - noHtml: true, - checks: [ - { - id: 'aria-required-attr', - options: { slider: ['aria-snuggles'] } - } - ] - }); - axe.run( - '#target', - { - runOnly: { - type: 'rule', - values: ['aria-required-attr'] - } - }, - function(error, results) { - try { - assert.isNull(results.violations[0].nodes[0].html); - done(); - } catch (e) { - done(e); - } - } - ); - }); + describe('noHtml', function() { + var captureError = axe.testUtils.captureError; + it('prevents html property on nodes', function(done) { + target.setAttribute('role', 'slider'); + axe.configure({ + noHtml: true, + checks: [ + { + id: 'aria-required-attr', + options: { slider: ['aria-snuggles'] } + } + ] + }); + axe.run( + '#target', + { + runOnly: { + type: 'rule', + values: ['aria-required-attr'] + } + }, + captureError(function(error, results) { + assert.isNull(error); + assert.isNull(results.violations[0].nodes[0].html); + done(); + }, done) + ); + }); - it('prevents html property on nodes from iframes', function(done) { - axe.configure({ - noHtml: true, - rules: [ - { - id: 'div#target', - // purposefully don't match so the first result is from - // the iframe - selector: 'foo' - } - ] - }); + it('prevents html property on nodes from iframes', function(done) { + var config = { + noHtml: true, + rules: [ + { + id: 'div#target', + // purposefully don't match so the first result is from + // the iframe + selector: 'foo' + } + ] + }; - var iframe = document.createElement('iframe'); - iframe.src = '/test/mock/frames/context.html'; - iframe.onload = function() { - axe.run( - '#target', - { - runOnly: { - type: 'rule', - values: ['div#target'] - } - }, - function(error, results) { - try { - assert.deepEqual(results.passes[0].nodes[0].target, [ - 'iframe', - '#target' - ]); - assert.isNull(results.passes[0].nodes[0].html); - done(); - } catch (e) { - done(e); - } - } - ); - }; - target.appendChild(iframe); - }); + var iframe = document.createElement('iframe'); + iframe.src = '/test/mock/frames/context.html'; + iframe.onload = function() { + axe.configure(config); + iframe.contentWindow.axe.configure(config); - it('prevents html property in postMesage', function(done) { - axe.configure({ - noHtml: true, - rules: [ - { - id: 'div#target', - // purposefully don't match so the first result is from - // the iframe - selector: 'foo' - } - ] - }); + axe.run( + '#target', + { + runOnly: { + type: 'rule', + values: ['div#target'] + } + }, + captureError(function(error, results) { + assert.isNull(error); + assert.deepEqual(results.passes[0].nodes[0].target, [ + 'iframe', + '#target' + ]); + assert.isNull(results.passes[0].nodes[0].html); + done(); + }, done) + ); + }; + target.appendChild(iframe); + }); - var iframe = document.createElement('iframe'); - iframe.src = '/test/mock/frames/noHtml-config.html'; - iframe.onload = function() { - axe.run('#target', { - runOnly: { - type: 'rule', - values: ['div#target'] - } - }); - }; - target.appendChild(iframe); + it('prevents html property in postMesage', function(done) { + var config = { + noHtml: true, + rules: [ + { + id: 'div#target', + // purposefully don't match so the first result is from + // the iframe + selector: 'foo' + } + ] + }; - window.addEventListener('message', function(e) { - var data = JSON.parse(e.data); - if (Array.isArray(data.message)) { - try { - assert.isNull(data.message[0].nodes[0].node.source); - done(); - } catch (e) { - done(e); - } - } - }); - }); - }); + var iframe = document.createElement('iframe'); + iframe.src = '/test/mock/frames/noHtml-config.html'; + iframe.onload = function() { + axe.configure(config); + iframe.contentWindow.axe.configure(config); + + axe.run('#target', { + runOnly: { + type: 'rule', + values: ['div#target'] + } + }); + }; + target.appendChild(iframe); + + window.addEventListener('message', function(e) { + var data = JSON.parse(e.data); + if (Array.isArray(data.message)) { + try { + assert.isNull(data.message[0].nodes[0].node.source); + done(); + } catch (e) { + done(e); + } + } + }); + }); + }); }); diff --git a/test/integration/full/umd/umd-module-exports.js b/test/integration/full/umd/umd-module-exports.js index b192b75d38..2ae2f16063 100644 --- a/test/integration/full/umd/umd-module-exports.js +++ b/test/integration/full/umd/umd-module-exports.js @@ -7,13 +7,19 @@ describe('UMD module.export', function() { }); it('does not use `require` functions', function() { + var result; + var requireRegex = /[^.]require\(([^\)])\)/g; + // This is to avoid colliding with Cypress.js which overloads all // uses of variables named `require`. - assert.notMatch( - axe.source, - /[^.]require\(/, - 'Axe source should not contain `require` variables' - ); + while ((result = requireRegex.exec(axe.source)) !== null) { + // Allow 'crypto' as it is used in an unobtrusive way. + assert.includes( + result[1], + 'crypto', + 'Axe source should not contain `require` variables' + ); + } }); it('should include doT', function() { diff --git a/test/integration/rules/link-in-text-block/link-in-text-block.json b/test/integration/rules/link-in-text-block/link-in-text-block.json deleted file mode 100644 index 6a17200c04..0000000000 --- a/test/integration/rules/link-in-text-block/link-in-text-block.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "description": "link-in-text-block tests", - "rule": "link-in-text-block", - "violations": [["#fail1"]], - "passes": [["#pass1"], ["#pass2"]] -} diff --git a/test/mock/frames/responder.html b/test/mock/frames/responder.html deleted file mode 100644 index aa8387b5fa..0000000000 --- a/test/mock/frames/responder.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - Double responding frame - - - - - - - - diff --git a/test/mock/frames/results-timeout.html b/test/mock/frames/results-timeout.html index 5825a01593..afacc453fd 100644 --- a/test/mock/frames/results-timeout.html +++ b/test/mock/frames/results-timeout.html @@ -4,22 +4,6 @@ Message Iframe Fixture - - - - + diff --git a/test/mock/frames/throwing.html b/test/mock/frames/throwing.html index d793d5a7bf..abfb0a9523 100644 --- a/test/mock/frames/throwing.html +++ b/test/mock/frames/throwing.html @@ -4,27 +4,11 @@ Error returning frame frame + - - - diff --git a/test/node/uuid.js b/test/node/uuid.js new file mode 100644 index 0000000000..eeb470401a --- /dev/null +++ b/test/node/uuid.js @@ -0,0 +1,19 @@ +var assert = require('chai').assert; +var sinon = require('sinon'); +var proxyquire = require('proxyquire'); +var crypto = require('crypto'); // Node package + +// 16 byte array, all 0's +var returnVal = new Array(16).fill(0); +var cryptoStub = sinon.stub(crypto, 'randomBytes').returns(returnVal); + +describe('uuid.v4', function() { + var axe = proxyquire('../../', { crypto: cryptoStub }); + var uuidV4 = axe.utils.uuid.v4; + + it('uses node crypto', function() { + var uuid = uuidV4(); + assert.isTrue(cryptoStub.randomBytes.called); + assert.deepEqual(uuid, '00000000-0000-4000-8000-000000000000'); + }); +}); diff --git a/test/testutils.js b/test/testutils.js index f5776c4d08..c5ac376bb8 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -398,3 +398,13 @@ axe.testUtils = testUtils; afterEach(function() { axe._cache.clear(); }); + +testUtils.captureError = function captureError(cb, errorHandler) { + return function() { + try { + cb.apply(null, arguments); + } catch (e) { + errorHandler(e); + } + }; +}; diff --git a/test/version.js b/test/version.js index bddfea2c63..ee55d075bd 100644 --- a/test/version.js +++ b/test/version.js @@ -1,2 +1,2 @@ // This is temporary as the tests still need to use this file -axe.version = '4.1.2'; +axe.version = '4.1.3';