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
-
-
-
-
+