Skip to content

Commit

Permalink
fix(no-autoplay-audio): don't timeout for preload=none media elements
Browse files Browse the repository at this point in the history
  • Loading branch information
straker committed Jan 28, 2025
1 parent 9a743ee commit b96dd2b
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 137 deletions.
14 changes: 12 additions & 2 deletions lib/checks/media/no-autoplay-audio-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
function noAutoplayAudioEvaluate(node, options) {
const hasControls = node.hasAttribute('controls');

/**
* if the media loops then we only need to know if it has controls, regardless
* of the duration
*/
if (node.hasAttribute('loop')) {
return hasControls;
}

/**
* if duration cannot be read, this means `preloadMedia` has failed
*/
Expand All @@ -12,15 +22,15 @@ function noAutoplayAudioEvaluate(node, options) {
*/
const { allowedDuration = 3 } = options;
const playableDuration = getPlayableDuration(node);
if (playableDuration <= allowedDuration && !node.hasAttribute('loop')) {
if (playableDuration <= allowedDuration) {
return true;
}

/**
* if media element does not provide controls mechanism
* -> fail
*/
if (!node.hasAttribute('controls')) {
if (!hasControls) {
return false;
}

Expand Down
21 changes: 21 additions & 0 deletions lib/core/utils/preload-media.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ function preloadMedia({ treeRoot = axe._tree[0] }) {
treeRoot,
'video, audio',
({ actualNode }) => {
/**
* Ignore media that won't load no matter how long we wait
* @see https://github.com/dequelabs/axe-core/issues/4665
*/
if (
actualNode.preload === 'none' &&
actualNode.networkState !== actualNode.NETWORK_LOADING
) {
return false;
}

/**
* Ignore media nodes which are `paused` or `muted`
*/
if (
actualNode.hasAttribute('paused') ||
actualNode.hasAttribute('muted')
) {
return false;
}

/**
* this is to safe-gaurd against empty `src` values which can get resolved `window.location`, thus never preloading as the URL is not a media asset
*/
Expand Down
183 changes: 95 additions & 88 deletions test/checks/media/no-autoplay-audio.js
Original file line number Diff line number Diff line change
@@ -1,119 +1,126 @@
describe('no-autoplay-audio', function () {
'use strict';
describe('no-autoplay-audio', () => {
const check = checks['no-autoplay-audio'];
const checkSetup = axe.testUtils.checkSetup;
const checkContext = axe.testUtils.MockCheckContext();
const preloadOptions = { preload: { assets: ['media'] } };

var check;
var fixture = document.getElementById('fixture');
var checkSetup = axe.testUtils.checkSetup;
var checkContext = axe.testUtils.MockCheckContext();
var preloadOptions = { preload: { assets: ['media'] } };

before(function () {
check = checks['no-autoplay-audio'];
});

afterEach(function () {
fixture.innerHTML = '';
axe._tree = undefined;
afterEach(() => {
checkContext.reset();
});

it('returns undefined when <audio> has no source (duration cannot be interpreted)', function (done) {
var checkArgs = checkSetup('<audio id="target"></audio>');
axe.utils.preload(preloadOptions).then(function () {
assert.isUndefined(check.evaluate.apply(checkContext, checkArgs));
done();
});
it('returns undefined when <audio> has no source (duration cannot be interpreted)', async () => {
const checkArgs = checkSetup('<audio id="target"></audio>');
await axe.utils.preload(preloadOptions);
assert.isUndefined(check.evaluate.apply(checkContext, checkArgs));
});

it('returns undefined when <video> has no source (duration cannot be interpreted)', function (done) {
var checkArgs = checkSetup('<video id="target"><source src=""/></video>');
axe.utils.preload(preloadOptions).then(function () {
assert.isUndefined(check.evaluate.apply(checkContext, checkArgs));
done();
});
it('returns undefined when <video> has no source (duration cannot be interpreted)', async () => {
const checkArgs = checkSetup('<video id="target"><source src=""/></video>');
await axe.utils.preload(preloadOptions);
assert.isUndefined(check.evaluate.apply(checkContext, checkArgs));
});

it('returns false when <audio> can autoplay and has no controls mechanism', function (done) {
var checkArgs = checkSetup(
it('returns false when <audio> can autoplay and has no controls mechanism', async () => {
const checkArgs = checkSetup(
'<audio id="target" src="/test/assets/moon-speech.mp3" autoplay="true"></audio>'
);
axe.utils.preload(preloadOptions).then(function () {
assert.isFalse(check.evaluate.apply(checkContext, checkArgs));
done();
});
await axe.utils.preload(preloadOptions);
assert.isFalse(check.evaluate.apply(checkContext, checkArgs));
});

it('returns false when <video> can autoplay and has no controls mechanism', function (done) {
var checkArgs = checkSetup(
'<video id="target" autoplay="true">' +
'<source src="/test/assets/video.webm" type="video/webm" />' +
'<source src="/test/assets/video.mp4" type="video/mp4" />' +
'</video>'
);
axe.utils.preload(preloadOptions).then(function () {
assert.isFalse(check.evaluate.apply(checkContext, checkArgs));
done();
});
it('returns false when <video> can autoplay and has no controls mechanism', async () => {
const checkArgs = checkSetup(`
<video id="target" autoplay="true">
<source src="/test/assets/video.webm" type="video/webm" />
<source src="/test/assets/video.mp4" type="video/mp4" />
</video>
`);
await axe.utils.preload(preloadOptions);
assert.isFalse(check.evaluate.apply(checkContext, checkArgs));
});

it('returns false when <audio> plays less than allowed dutation but loops', function (done) {
var checkArgs = checkSetup(
it('returns false when <audio> plays less than allowed dutation but loops', async () => {
const checkArgs = checkSetup(
'<audio id="target" src="/test/assets/moon-speech.mp3#t=2,4" autoplay="true" loop="true"></audio>'
);
axe.utils.preload(preloadOptions).then(function () {
assert.isFalse(check.evaluate.apply(checkContext, checkArgs));
done();
});
await axe.utils.preload(preloadOptions);
assert.isFalse(check.evaluate.apply(checkContext, checkArgs));
});

it('returns true when <video> can autoplay and duration is below allowed duration (by passing options)', function (done) {
var checkArgs = checkSetup(
'<video id="target" autoplay="true">' +
'<source src="/test/assets/video.webm" type="video/webm" />' +
'<source src="/test/assets/video.mp4" type="video/mp4" />' +
'</video>',
{ allowedDuration: 15 }
it('returns false when <video> loops and has no controls mechanism when duration is unknown', () => {
const checkArgs = checkSetup(`
<video id="target" loop>
<source src="/test/assets/video.webm#t=7,9" type="video/webm" />
<source src="/test/assets/video.mp4#t=7,9" type="video/mp4" />
</video>
`);
assert.isFalse(check.evaluate.apply(checkContext, checkArgs));
});

it('returns false when <audio> loops and has no controls mechanism when duration is unknown', () => {
const checkArgs = checkSetup(
'<audio id="target" src="/test/assets/moon-speech.mp3#t=2,4" loop="true"></audio>'
);
axe.utils.preload(preloadOptions).then(function () {
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
done();
});
assert.isFalse(check.evaluate.apply(checkContext, checkArgs));
});

it('returns true when <video> can autoplay and duration is below allowed duration (by setting playback range)', function (done) {
var checkArgs = checkSetup(
'<video id="target" autoplay="true">' +
'<source src="/test/assets/video.webm#t=7,9" type="video/webm" />' +
'<source src="/test/assets/video.mp4#t=7,9" type="video/mp4" />' +
'</video>'
// Note: default allowed duration is 3s
it('returns true when <video> can autoplay and duration is below allowed duration (by passing options)', async () => {
const checkArgs = checkSetup(
`
<video id="target" autoplay="true">
<source src="/test/assets/video.webm" type="video/webm" />
<source src="/test/assets/video.mp4" type="video/mp4" />
</video>`,
{ allowedDuration: 15 }
);
axe.utils.preload(preloadOptions).then(function () {
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
done();
});
await axe.utils.preload(preloadOptions);
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
});

it('returns true when <video> can autoplay and duration is below allowed duration (by setting playback range)', async () => {
const checkArgs = checkSetup(`
<video id="target" autoplay="true">
<source src="/test/assets/video.webm#t=7,9" type="video/webm" />
<source src="/test/assets/video.mp4#t=7,9" type="video/mp4" />
</video>`);
// Note: default allowed duration is 3s
await axe.utils.preload(preloadOptions);
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
});

it('returns true when <audio> can autoplay but has controls mechanism', function (done) {
var checkArgs = checkSetup(
it('returns true when <audio> can autoplay but has controls mechanism', async () => {
const checkArgs = checkSetup(
'<audio id="target" src="/test/assets/moon-speech.mp3" autoplay="true" controls></audio>'
);
axe.utils.preload(preloadOptions).then(function () {
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
done();
});
await axe.utils.preload(preloadOptions);
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
});

it('returns true when <video> can autoplay and has controls mechanism', async () => {
const checkArgs = checkSetup(`
<video id="target" autoplay="true" controls>
<source src="/test/assets/video.webm" type="video/webm" />
<source src="/test/assets/video.mp4" type="video/mp4" />
</video>
`);
await axe.utils.preload(preloadOptions);
assert.isTrue(check.evaluate.apply(null, checkArgs));
});

it('returns true when <video> loops and has controls mechanism when duration is unknown', () => {
const checkArgs = checkSetup(`
<video id="target" loop controls>
<source src="/test/assets/video.webm#t=7,9" type="video/webm" />
<source src="/test/assets/video.mp4#t=7,9" type="video/mp4" />
</video>
`);
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
});

it('returns true when <video> can autoplay and has controls mechanism', function (done) {
var checkArgs = checkSetup(
'<video id="target" autoplay="true" controls>' +
'<source src="/test/assets/video.webm" type="video/webm" />' +
'<source src="/test/assets/video.mp4" type="video/mp4" />' +
'</video>'
it('returns true when <audio> loops and has controls mechanism when duration is unknown', () => {
const checkArgs = checkSetup(
'<audio id="target" src="/test/assets/moon-speech.mp3#t=2,4" controls="true" loop="true"></audio>'
);
axe.utils.preload(preloadOptions).then(function () {
assert.isTrue(check.evaluate.apply(null, checkArgs));
done();
});
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
});
});
95 changes: 53 additions & 42 deletions test/core/utils/preload-media.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,68 @@
describe('axe.utils.preloadMedia', function () {
'use strict';
describe('axe.utils.preloadMedia', () => {
const fixtureSetup = axe.testUtils.fixtureSetup;

var fixture = document.getElementById('fixture');
var fixtureSetup = axe.testUtils.fixtureSetup;

afterEach(function () {
fixture.innerHTML = '';
});

it('returns empty array when there are no media nodes to be preloaded', function (done) {
it('returns empty array when there are no media nodes to be preloaded', async () => {
axe._tree = axe.utils.getFlattenedTree(document);

axe.utils.preloadMedia({ treeRoot: axe._tree[0] }).then(function (result) {
assert.equal(result.length, 0);
done();
});
const result = await axe.utils.preloadMedia({ treeRoot: axe._tree[0] });
assert.equal(result.length, 0);
});

it('returns empty array when <audio> has no source', function (done) {
it('returns empty array when <audio> has no source', async () => {
fixtureSetup('<audio autoplay="true" controls></audio>');

axe.utils.preloadMedia({ treeRoot: axe._tree[0] }).then(function (result) {
assert.equal(result.length, 0);
done();
});
const result = await axe.utils.preloadMedia({ treeRoot: axe._tree[0] });
assert.equal(result.length, 0);
});

it('returns empty array when <video> has no source', function (done) {
it('returns empty array when <video> has no source', async () => {
fixtureSetup('<video id="target"><source src=""/></video>');
axe.utils.preloadMedia({ treeRoot: axe._tree[0] }).then(function (result) {
assert.equal(result.length, 0);
done();
});
const result = await axe.utils.preloadMedia({ treeRoot: axe._tree[0] });
assert.equal(result.length, 0);
});

it('returns media node (audio) after their metadata has been preloaded', function (done) {
it('returns empty array when media node does not preload', async () => {
fixtureSetup(`
<video id="target" preload="none">
<source src="/test/assets/video.mp4" type="video/mp4" />
</video>
`);
const result = await axe.utils.preloadMedia({ treeRoot: axe._tree[0] });
assert.equal(result.length, 0);
});

it('returns empty array when media node is muted', async () => {
fixtureSetup(`
<video id="target" muted>
<source src="/test/assets/video.mp4" type="video/mp4" />
</video>
`);
const result = await axe.utils.preloadMedia({ treeRoot: axe._tree[0] });
assert.equal(result.length, 0);
});

it('returns empty array when media node is paused', async () => {
fixtureSetup(`
<video id="target" paused>
<source src="/test/assets/video.mp4" type="video/mp4" />
</video>
`);
const result = await axe.utils.preloadMedia({ treeRoot: axe._tree[0] });
assert.equal(result.length, 0);
});

it('returns media node (audio) after their metadata has been preloaded', async () => {
fixtureSetup(
'<audio src="/test/assets/moon-speech.mp3" autoplay="true" controls></audio>'
);

axe.utils.preloadMedia({ treeRoot: axe._tree[0] }).then(function (result) {
assert.equal(result.length, 1);
assert.isTrue(result[0].readyState > 0);
assert.equal(Math.round(result[0].duration), 27);

done();
});
const result = await axe.utils.preloadMedia({ treeRoot: axe._tree[0] });
assert.equal(result.length, 1);
assert.isTrue(result[0].readyState > 0);
assert.equal(Math.round(result[0].duration), 27);
});

it('returns media nodes (audio, video) after their metadata has been preloaded', function (done) {
it('returns media nodes (audio, video) after their metadata has been preloaded', async () => {
fixtureSetup(
// 1 audio elm
'<audio src="/test/assets/moon-speech.mp3"></audio>' +
Expand All @@ -59,15 +73,12 @@ describe('axe.utils.preloadMedia', function () {
'</video>'
);

axe.utils.preloadMedia({ treeRoot: axe._tree[0] }).then(function (result) {
assert.equal(result.length, 2);
assert.isTrue(result[0].readyState > 0);
assert.equal(Math.round(result[0].duration), 27);

assert.isTrue(result[1].readyState > 0);
assert.equal(Math.round(result[1].duration), 14);
const result = await axe.utils.preloadMedia({ treeRoot: axe._tree[0] });
assert.equal(result.length, 2);
assert.isTrue(result[0].readyState > 0);
assert.equal(Math.round(result[0].duration), 27);

done();
});
assert.isTrue(result[1].readyState > 0);
assert.equal(Math.round(result[1].duration), 14);
});
});
Loading

0 comments on commit b96dd2b

Please sign in to comment.