diff --git a/axe.d.ts b/axe.d.ts index a735784e8d..bde2cb393f 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -95,6 +95,7 @@ declare namespace axe { frameWaitTime?: number; preload?: boolean; performanceTimer?: boolean; + pingWaitTime?: number; } interface AxeResults extends EnvironmentData { toolOptions: RunOptions; diff --git a/doc/API.md b/doc/API.md index 02eace24f7..c605a5c3ae 100644 --- a/doc/API.md +++ b/doc/API.md @@ -421,7 +421,7 @@ axe.run( ); ``` -##### Options Parameter +``##### Options Parameter The options parameter is flexible way to configure how `axe.run` operates. The different modes of operation are: @@ -446,6 +446,7 @@ Additionally, there are a number or properties that allow configuration of diffe | `frameWaitTime` | `60000` | How long (in milliseconds) axe waits for a response from embedded frames before timing out | | `preload` | `true` | Any additional assets (eg: cssom) to preload before running rules. [See here for configuration details](#preload-configuration-details) | | `performanceTimer` | `false` | Log rule performance metrics to the console | +| `pingWaitTime` | `500` | Time before axe-core considers a frame unresponsive. [See frame messenger for details](frame-messenger.md) | ###### Options Parameter Examples diff --git a/doc/frame-messenger.md b/doc/frame-messenger.md index ca880ebc7d..3ac8e3545d 100644 --- a/doc/frame-messenger.md +++ b/doc/frame-messenger.md @@ -105,3 +105,15 @@ If for some reason the frameMessenger fails to open, post, or close you should n Axe-core has a timeout mechanism built in, which pings frames to see if they respond before instructing them to run. There is no retry behavior in axe-core, which assumes that whatever channel is used is stable. If this isn't the case, this will need to be built into frameMessenger. The `message` passed to responder may be an `Error`. If axe-core passes an `Error`, this should be propagated "as is". If this is not possible because the message needs to be serialized, a new `Error` object must be constructed as part of deserialization. + +### pingWaitTime + +When axe-core tests frames, it first sends a ping to that frame, to check that the frame has a compatible version of axe-core in it that can respond to the message. If it gets no response, that frame will be skipped in the test. Axe-core does this to avoid a situation where it waits the full frame timeout, just to find out the frame didn't have axe-core in it in the first place. + +In situations where communication between frames can be slow, it may be necessary to increase the ping timeout. This can be done with the `pingWaitTime` option. By default, this is 500ms. This can be configured in the following way: + +```js +const results = await axe.run(context, { pingWaitTime: 1000 })); +``` + +It is possible to skip this ping altogether by setting `pingWaitTime` to `0`. This can slightly speed up performance, but should only be used when long wait times for unresponsive frames can be avoided. Axe-core handles timeout errors the same way it handles any other frame communication errors. Therefore if a custom frame messenger has a timeout, it can inform axe by calling `replyHandler` with an `Error` object. diff --git a/lib/core/utils/send-command-to-frame.js b/lib/core/utils/send-command-to-frame.js index fb17e30d69..dd61ec069e 100644 --- a/lib/core/utils/send-command-to-frame.js +++ b/lib/core/utils/send-command-to-frame.js @@ -2,14 +2,6 @@ import getSelector from './get-selector'; import respondable from './respondable'; import log from '../log'; -function err(message, node) { - var selector; - // TODO: es-modules_tree - if (axe._tree) { - selector = getSelector(node); - } - return new Error(message + ': ' + (selector || node)); -} /** * Sends a command to an instance of axe in the specified frame @@ -17,16 +9,23 @@ function err(message, node) { * @param {Object} parameters Parameters to pass to the frame * @param {Function} callback Function to call when results from the frame has returned */ -function sendCommandToFrame(node, parameters, resolve, reject) { - var win = node.contentWindow; + export default function sendCommandToFrame(node, parameters, resolve, reject) { + const win = node.contentWindow; + const pingWaitTime = parameters.options?.pingWaitTime ?? 500 if (!win) { log('Frame does not have a content window', node); resolve(null); return; } + // Skip ping + if (pingWaitTime === 0) { + callAxeStart(node, parameters, resolve, reject); + return; + } + // give the frame .5s to respond to 'axe.ping', else log failed response - var timeout = setTimeout(() => { + let timeout = setTimeout(() => { // This double timeout is important for allowing iframes to respond // DO NOT REMOVE timeout = setTimeout(() => { @@ -36,30 +35,39 @@ function sendCommandToFrame(node, parameters, resolve, reject) { reject(err('No response from frame', node)); } }, 0); - }, parameters.options?.pingWaitTime ?? 500); + }, pingWaitTime); // send 'axe.ping' to the frame respondable(win, 'axe.ping', null, undefined, () => { clearTimeout(timeout); + callAxeStart(node, parameters, resolve, reject); + }); +} - // Give axe 60s (or user-supplied value) to respond to 'axe.start' - var frameWaitTime = - (parameters.options && parameters.options.frameWaitTime) || 60000; - - timeout = setTimeout(function collectResultFramesTimeout() { - reject(err('Axe in frame timed out', node)); - }, frameWaitTime); +function callAxeStart(node, parameters, resolve, reject) { + // Give axe 60s (or user-supplied value) to respond to 'axe.start' + const frameWaitTime = parameters.options?.frameWaitTime ?? 60000; + const win = node.contentWindow; + const timeout = setTimeout(function collectResultFramesTimeout() { + reject(err('Axe in frame timed out', node)); + }, frameWaitTime); - // send 'axe.start' and send the callback if it responded - respondable(win, 'axe.start', parameters, undefined, data => { - clearTimeout(timeout); - if (data instanceof Error === false) { - resolve(data); - } else { - reject(data); - } - }); + // send 'axe.start' and send the callback if it responded + respondable(win, 'axe.start', parameters, undefined, data => { + clearTimeout(timeout); + if (data instanceof Error === false) { + resolve(data); + } else { + reject(data); + } }); } -export default sendCommandToFrame; +function err(message, node) { + var selector; + // TODO: es-modules_tree + if (axe._tree) { + selector = getSelector(node); + } + return new Error(message + ': ' + (selector || node)); +} diff --git a/test/core/utils/send-command-to-frame.js b/test/core/utils/send-command-to-frame.js index 107a843009..6e76b4ed05 100644 --- a/test/core/utils/send-command-to-frame.js +++ b/test/core/utils/send-command-to-frame.js @@ -37,6 +37,42 @@ describe('axe.utils.sendCommandToFrame', function() { fixture.appendChild(frame); }); + it('adjusts skips ping with options.pingWaitTime=0', function (done) { + var frame = document.createElement('iframe'); + var params = { + command: 'rules', + options: { pingWaitTime: 0 } + }; + + frame.addEventListener('load', function() { + var topics = []; + frame.contentWindow.addEventListener('message', function (event) { + try { + topics.push(JSON.parse(event.data).topic) + } catch (_) { /* ignore */ } + }); + axe.utils.sendCommandToFrame( + frame, + params, + captureError(function() { + try { + assert.deepEqual(topics, ['axe.start']) + done(); + } catch (e) { + done(e); + } + }, 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) {