-
Notifications
You must be signed in to change notification settings - Fork 795
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rule): no-autoplay-audio (#1946)
* add meta data (json) for rules and checks * update is-hidden to exclude media tags * add test for matches * add axe.utils.preloadMedia helper fn * rule no-autoplay-audio * revert changes to extracting getAllRootNodesInTree Fn * update interpretation of audio src * update resolving src in audio check * add tests for no-autoplay-audio check * update test of preloadMedia * Apply suggestions from code review Co-Authored-By: Steven Lambert <steven.lambert@deque.com> * update matches * use querySelectorAllFilter * updates based on review * test: update tests * only handle audio media node with no controls in ishidden * update tests * use excludeHidden over isHidden fn * test: move integration test to full
- Loading branch information
Showing
11 changed files
with
658 additions
and
132 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* if duration cannot be read, this means `preloadMedia` has failed | ||
*/ | ||
if (!node.duration || Number.isNaN(node.duration)) { | ||
console.warn(`axe.utils.preloadMedia did not load metadata`); | ||
return undefined; | ||
} | ||
|
||
/** | ||
* Compute playable duration and verify if it within allowed duration | ||
*/ | ||
const { allowedDuration = 3 } = options; | ||
const playableDuration = getPlayableDuration(node); | ||
if (playableDuration <= allowedDuration && !node.hasAttribute('loop')) { | ||
return true; | ||
} | ||
|
||
/** | ||
* if media element does not provide controls mechanism | ||
* -> fail | ||
*/ | ||
if (!node.hasAttribute('controls')) { | ||
return false; | ||
} | ||
|
||
return true; | ||
|
||
/** | ||
* Compute playback duration | ||
* @param {HTMLMediaElement} elm media element | ||
*/ | ||
function getPlayableDuration(elm) { | ||
if (!elm.currentSrc) { | ||
return 0; | ||
} | ||
|
||
const playbackRange = getPlaybackRange(elm.currentSrc); | ||
if (!playbackRange) { | ||
return Math.abs(elm.duration - (elm.currentTime || 0)); | ||
} | ||
|
||
if (playbackRange.length === 1) { | ||
return Math.abs(elm.duration - playbackRange[0]); | ||
} | ||
|
||
return Math.abs(playbackRange[1] - playbackRange[0]); | ||
} | ||
|
||
/** | ||
* Get playback range from a media elements source, if specified | ||
* See - https://developer.mozilla.org/de/docs/Web/HTML/Using_HTML5_audio_and_video#Specifying_playback_range | ||
* | ||
* Eg: | ||
* src='....someMedia.mp3#t=8' | ||
* -> should yeild [8] | ||
* src='....someMedia.mp3#t=10,12' | ||
* -> should yeild [10,12] | ||
* @param {String} src media src | ||
* @returns {Array|undefined} | ||
*/ | ||
function getPlaybackRange(src) { | ||
const match = src.match(/#t=(.*)/); | ||
if (!match) { | ||
return; | ||
} | ||
const [, value] = match; | ||
const ranges = value.split(','); | ||
|
||
return ranges.map(range => { | ||
// range is denoted in HH:MM:SS -> convert to seconds | ||
if (/:/.test(range)) { | ||
return convertHourMinSecToSeconds(range); | ||
} | ||
return parseFloat(range); | ||
}); | ||
} | ||
|
||
/** | ||
* Add HH, MM, SS to seconds | ||
* @param {String} hhMmSs time expressed in HH:MM:SS | ||
*/ | ||
function convertHourMinSecToSeconds(hhMmSs) { | ||
let parts = hhMmSs.split(':'); | ||
let secs = 0; | ||
let mins = 1; | ||
|
||
while (parts.length > 0) { | ||
secs += mins * parseInt(parts.pop(), 10); | ||
mins *= 60; | ||
} | ||
|
||
return parseFloat(secs); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"id": "no-autoplay-audio", | ||
"evaluate": "no-autoplay-audio.js", | ||
"options": { | ||
"allowedDuration": 3 | ||
}, | ||
"metadata": { | ||
"impact": "moderate", | ||
"messages": { | ||
"pass": "<video> or <audio> does not output audio for more than allowed duration or has controls mechanism", | ||
"fail": "<video> or <audio> outputs audio for more than allowed duration and does not have a controls mechanism", | ||
"incomplete": "Check that the <video> or <audio> does not output audio for more than allowed duration or provides a controls mechanism" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** | ||
* Ignore media nodes without `currenSrc` | ||
* Notes: | ||
* - https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentSrc | ||
* - https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/src | ||
*/ | ||
if (!node.currentSrc) { | ||
return false; | ||
} | ||
|
||
/** | ||
* Ignore media nodes which are `paused` or `muted` | ||
*/ | ||
if (node.hasAttribute('paused') || node.hasAttribute('muted')) { | ||
return false; | ||
} | ||
|
||
return true; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"id": "no-autoplay-audio", | ||
"excludeHidden": false, | ||
"selector": "audio[autoplay], video[autoplay]", | ||
"matches": "no-autoplay-audio-matches.js", | ||
"tags": ["wcag2a", "wcag142", "experimental"], | ||
"metadata": { | ||
"description": "Ensures <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio", | ||
"help": "<video> or <audio> elements do not autoplay audio" | ||
}, | ||
"preload": true, | ||
"all": ["no-autoplay-audio"], | ||
"any": [], | ||
"none": [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
describe('no-autoplay-audio', function() { | ||
'use strict'; | ||
|
||
var check; | ||
var fixture = document.getElementById('fixture'); | ||
var isIE11 = axe.testUtils.isIE11; | ||
var checkSetup = axe.testUtils.checkSetup; | ||
var checkContext = axe.testUtils.MockCheckContext(); | ||
var preloadOptions = { preload: { assets: ['media'] } }; | ||
|
||
before(function() { | ||
// The tests actually pass in IE10/11 in Windows machine, but fails in IE in selenium-ie-driver | ||
// Issue has been created to debug selenium ie failing tests | ||
if (isIE11) { | ||
this.skip(); | ||
} | ||
check = checks['no-autoplay-audio']; | ||
}); | ||
|
||
afterEach(function() { | ||
fixture.innerHTML = ''; | ||
axe._tree = undefined; | ||
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 <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 false when <audio> can autoplay and has no controls mechanism', function(done) { | ||
var 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(); | ||
}); | ||
}); | ||
|
||
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 <audio> plays less than allowed dutation but loops', function(done) { | ||
var 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(); | ||
}); | ||
}); | ||
|
||
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 } | ||
); | ||
axe.utils.preload(preloadOptions).then(function() { | ||
assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); | ||
done(); | ||
}); | ||
}); | ||
|
||
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 | ||
); | ||
axe.utils.preload(preloadOptions).then(function() { | ||
assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('returns true when <audio> can autoplay but has controls mechanism', function(done) { | ||
var 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(); | ||
}); | ||
}); | ||
|
||
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>' | ||
); | ||
axe.utils.preload(preloadOptions).then(function() { | ||
assert.isTrue(check.evaluate.apply(null, checkArgs)); | ||
done(); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.