Skip to content

Commit

Permalink
feat(options): make axe.ping configurable with pingWaitTime (#3273)
Browse files Browse the repository at this point in the history
* feat(options): make axe.ping configurable with pingWaitTime

* chore: add to type definition
  • Loading branch information
WilcoFiers authored Nov 12, 2021
1 parent bf7e60a commit ce4dfaf
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 30 deletions.
1 change: 1 addition & 0 deletions axe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ declare namespace axe {
frameWaitTime?: number;
preload?: boolean;
performanceTimer?: boolean;
pingWaitTime?: number;
}
interface AxeResults extends EnvironmentData {
toolOptions: RunOptions;
Expand Down
3 changes: 2 additions & 1 deletion doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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

Expand Down
12 changes: 12 additions & 0 deletions doc/frame-messenger.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
66 changes: 37 additions & 29 deletions lib/core/utils/send-command-to-frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,30 @@ 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
* @param {Element} node The frame element to send the message to
* @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(() => {
Expand All @@ -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));
}
36 changes: 36 additions & 0 deletions test/core/utils/send-command-to-frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit ce4dfaf

Please sign in to comment.