Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(options): make axe.ping configurable with pingWaitTime #3273

Merged
merged 2 commits into from
Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -422,7 +422,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 @@ -447,6 +447,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