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

fix(no-autoplay-audio): don't timeout for preload=none media elements #4684

Merged
merged 5 commits into from
Mar 3, 2025
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
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
43 changes: 41 additions & 2 deletions lib/core/utils/preload-media.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,49 @@ import querySelectorAllFilter from './query-selector-all-filter';
function preloadMedia({ treeRoot = axe._tree[0] }) {
const mediaVirtualNodes = querySelectorAllFilter(
treeRoot,
'video, audio',
/**
* Only concern ourselves with media that autoplays as the no-autoplay-audio rule
* is the only rule that uses this information
*/
'video[autoplay], audio[autoplay]',
({ actualNode }) => {
/**
* 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
* Ignore media that won't load no matter how long we wait (i.e. preload=none).
*
* Although the spec says that the autoplay attribute can override the preload
* attribute, it depends on the browser settings (if autoplay is allowed) and
* operating system (e.g. Android does not preload autoplay media even when
* autoplay is allowed).
*
* We can identify preload=none media that won't load if the networkState is
* idle and the readyState is 0. If the browser is currently loading the media
* (networkState) or if the media is already loaded (readyState) that means the
* preload attribute was ignored.
*
* @see https://github.com/dequelabs/axe-core/issues/4665
* @see https://html.spec.whatwg.org/multipage/media.html#attr-media-preload
*/
if (
actualNode.preload === 'none' &&
actualNode.readyState === 0 &&
actualNode.networkState !== actualNode.NETWORK_LOADING
) {
return false;
}

/**
* Ignore media nodes which are `paused` or `muted` as the no-autoplay-audio
* rule matcher ignores them
*/
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
*/
if (actualNode.hasAttribute('src')) {
return !!actualNode.getAttribute('src');
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));
});
});
Loading