<!DOCTYPE html>
<title>Test the play() behaviour with regards to the returned promise for media elements.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="network-error.js"></script>
<script>
// This is testing the behavior of play() with regards to the returned
// promise. This test file is creating a small framework in order to be able
// to test different cases easily and independently of each other.
// All tests have to be part of the tests and testsWithUserGesture arrays.
// Each test returns a promise for audio.play() which is either resolved or rejected.
var tests = [
// Test that play() on an element that doesn't have enough data will
// return a promise that resolves successfully.
function playBeforeCanPlay(t, audio) {
audio.src = "content/test.oga";
assert_equals(audio.readyState, HTMLMediaElement.HAVE_NOTHING);
playExpectingResolvedPromise(t, audio);
},
// Test that play() on an element that has enough data will return a
// promise that resolves successfully.
function playWhenCanPlay(t, audio) {
audio.src = "content/test.oga";
audio.oncanplay = t.step_func(function() {
assert_greater_than_equal(audio.readyState, HTMLMediaElement.HAVE_FUTURE_DATA);
assert_true(audio.paused);
playExpectingResolvedPromise(t, audio);
});
},
// Test that play() on an element that is already playing returns a
// promise that resolves successfully.
function playAfterPlaybackStarted(t, audio) {
audio.preload = "auto";
audio.src = "content/test.oga";
audio.onplaying = t.step_func(function() {
assert_equals(audio.readyState, HTMLMediaElement.HAVE_ENOUGH_DATA);
assert_false(audio.paused);
playExpectingResolvedPromise(t, audio);
});
audio.oncanplaythrough = t.step_func(function() {
audio.play();
});
},
// Test that play() on an element with an unsupported content will
// return a rejected promise.
function playNotSupportedContent(t, audio) {
audio.src = "data:,.oga";
audio.onerror = t.step_func(function() {
assert_true(audio.error instanceof MediaError);
assert_equals(audio.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
});
playExpectingRejectedPromise(t, audio, "NotSupportedError");
},
// Test that play() returns a resolved promise if called after the
// element suffered from a decode error.
// This test doesn't test a spec behaviour but tests that the Blink
// implementation properly changed after the spec changed.
function playDecodeError(t, audio) {
audio.src = "content/corrupt.webm";
audio.onerror = t.step_func(function() {
assert_true(audio.error instanceof MediaError);
assert_equals(audio.error.code, MediaError.MEDIA_ERR_DECODE);
});
playExpectingResolvedPromise(t, audio);
},
// Test that play() returns a resolved promise if called after the
// element suffered from a network error.
// This test doesn't test a spec behaviour but tests that the Blink
// implementation properly changed after the spec changed
//
// TODO(mlamouri): This test is disabled because the underlying behavior it
// is intended to test is actually broken. http://crbug.com/841063
//
// function playNetworkError(t, audio) {
// audio.onerror = t.step_func(function() {
// assert_true(audio.error instanceof MediaError);
// assert_equals(audio.error.code, MediaError.MEDIA_ERR_NETWORK);
// });
// audio.onloadedmetadata = t.step_func(function() {
// playExpectingResolvedPromise(t, audio);
// });
// generateNetworkError(audio);
// },
// Test that play() returns a rejected promise if the element is
// sufferring from a not supported error.
function playWithErrorAlreadySet(t, audio) {
audio.src = "data:,.oga";
audio.onerror = t.step_func(function() {
assert_true(audio.error instanceof MediaError);
assert_equals(audio.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
playExpectingRejectedPromise(t, audio, "NotSupportedError");
});
},
// Test that play() returns a resolved promise if the element had its
// source changed after suffering from an error.
function playSrcChangedAfterError(t, audio) {
audio.src = "data:,.oga";
audio.onerror = t.step_func(function() {
assert_true(audio.error instanceof MediaError);
assert_equals(audio.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
audio.src = "content/test.oga";
audio.onloadedmetadata = t.step_func(function() {
playExpectingResolvedPromise(t, audio);
});
});
},
// Test that play() returns a rejected promise if the element had an
// error and just changed its source.
function playRaceWithSrcChangeError(t, audio) {
audio.src = "data:,.oga";
audio.onerror = t.step_func(function() {
assert_true(audio.error instanceof MediaError);
assert_equals(audio.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
audio.src = "content/test.oga";
assert_equals(audio.error, null);
assert_equals(audio.readyState, HTMLMediaElement.HAVE_NOTHING);
playExpectingResolvedPromise(t, audio);
});
},
// Test that play() returns a resolved promise when calling play() then
// pause() on an element that already has enough data to play. In other
// words, pause() doesn't cancel play() because it was resolved
// immediately.
function playAndPauseWhenCanplay(t, audio) {
audio.src = "content/test.oga";
audio.oncanplaythrough = t.step_func(function() {
assert_equals(audio.readyState, HTMLMediaElement.HAVE_ENOUGH_DATA);
playExpectingResolvedPromise(t, audio);
assert_false(audio.paused);
audio.pause();
assert_true(audio.paused);
});
},
// Test that play() returns a rejected promise when calling play() then
// pause() on an element that doesn't have enough data to play. In other
// words, pause() cancels play() before it can be resolved.
function playAndPauseBeforeCanPlay(t, audio) {
assert_equals(audio.readyState, HTMLMediaElement.HAVE_NOTHING);
playExpectingRejectedPromise(t, audio, "AbortError");
assert_false(audio.paused);
audio.pause();
assert_true(audio.paused);
},
// Test that load() rejects all the pending play() promises.
// This might be tested by other tests in this file but it is present in
// order to be explicit.
function loadRejectsPendingPromises(t, audio) {
playExpectingRejectedPromise(t, audio, "AbortError"); // the promise will be left pending.
audio.load();
},
// Test that changing the src rejects the pending play() promises.
function newSrcRejectPendingPromises(t, audio) {
playExpectingRejectedPromise(t, audio, "AbortError"); // the promise will be left pending.
audio.src = "content/test.oga";
},
// Test ordering of events and promises.
// This is testing a bug in Blink, see https://crbug.com/587871
function testEventAndPromiseOrdering(t, audio) {
audio.src = "data:,.oga";
audio.onerror = t.step_func(function() {
// Until https://crbug.com/587871 is fixed, the events will be
// [ promise, volumechange, volumechange ], it should be
// [ volumechange, promise, volumechange ]. Right now test finishes
// as soon as promise is rejected, before "volumechange" is fired.
var numVolumeChangeEvents = 0;
audio.onvolumechange = t.step_func(function() { ++numVolumeChangeEvents; });
audio.volume = 0.1;
audio.play().then(t.unreached_func(), t.step_func_done(function() {
assert_equals(arguments.length, 1);
assert_equals(arguments[0].name, "NotSupportedError");
assert_equals(numVolumeChangeEvents, 0);
}));
audio.volume = 0.2;
});
},
// Test that calling pause() then play() on an element that is already
// playing returns a promise that resolves successfully.
function pausePlayAfterPlaybackStarted(t, audio) {
audio.preload = "auto";
audio.src = "content/test.oga";
audio.onplaying = t.step_func(function() {
assert_equals(audio.readyState, HTMLMediaElement.HAVE_ENOUGH_DATA);
assert_false(audio.paused);
audio.pause();
playExpectingResolvedPromise(t, audio);
});
audio.oncanplaythrough = t.step_func(function() {
audio.play();
});
},
// Test that running the load algorithm will not drop all the promises about
// to be resolved.
function loadAlgorithmDoesNotCancelTasks(t, audio) {
audio.src = 'content/test.oga';
audio.addEventListener('canplaythrough', t.step_func(function() {
// The play() promise will be queued to be resolved.
playExpectingResolvedPromise(t, audio);
audio.src = 'content/test.oga';
assert_true(audio.paused);
}));
},
// Test that when the load algorithm is run, if it does not pause the
// playback, it will leave the promise pending, allowing it to be resolved.
function loadAlgorithmKeepPromisesPendingWhenNotPausing(t, audio) {
playExpectingResolvedPromise(t, audio);
setTimeout(t.step_func(function() {
audio.src = 'content/test.oga';
assert_false(audio.paused);
}), 0);
},
// Test that when the load algorithm is run, if it resolves multiple
// promises, they are resolved in the order in which they were added.
function loadAlgorithmResolveOrdering(t, audio) {
audio.src = 'content/test.oga';
audio.addEventListener('canplaythrough', t.step_func(function() {
var firstPromiseResolved = false;
audio.play().then(t.step_func(_ => firstPromiseResolved = true),
t.unreached_func());
audio.play().then(t.step_func_done(function() {
assert_true(firstPromiseResolved);
}), t.unreached_func());
audio.src = 'content/test.oga';
}));
},
// Test that when the load algorithm is run, if it does not pause the
// playback, it will leave the promise pending, allowing it to be resolved
// (version with preload='none').
// TODO(mlamouri): there is a bug in Blink where the media element ends up
// in a broken state, see https://crbug.com/633591
// function loadAlgorithmKeepPromisesPendingWhenNotPausingAndPreloadNone(t, audio) {
// audio.preload = 'none';
// playExpectingRejectedPromise(t, audio, 'AbortError');
// setTimeout(_ => audio.src = 'content/test.oga', 0);
// },
// Test that when the load algorithm is run, if it does pause the playback,
// it will reject the pending promises.
function loadAlgorithmRejectPromisesWhenPausing(t, audio) {
playExpectingRejectedPromise(t, audio, 'AbortError');
audio.src = 'content/test.oga';
assert_true(audio.paused);
},
// Test that when the load algorithm is run, if it does pause the playback,
// it will reject the pending promises (version with preload='none').
function loadAlgorithmRejectPromisesWhenPausingAndPreloadNone(t, audio) {
audio.preload = 'none';
playExpectingRejectedPromise(t, audio, 'AbortError');
audio.src = 'content/test.oga';
assert_true(audio.paused);
},
// Test that when the load algorithm is run, if it rejects multiple
// promises, they are rejected in the order in which they were added.
function loadAlgorithmResolveOrdering(t, audio) {
var firstPromiseRejected = false;
audio.play().then(t.unreached_func(), t.step_func(function(e) {
assert_equals(e.name, 'AbortError');
assert_equals(e.message,
'The play() request was interrupted by a call to pause(). https://goo.gl/LdLk22');
firstPromiseRejected = true;
}));
audio.play().then(t.unreached_func(), t.step_func_done(function(e) {
assert_equals(e.name, 'AbortError');
assert_equals(e.message,
'The play() request was interrupted by a call to pause(). https://goo.gl/LdLk22');
assert_true(firstPromiseRejected);
}));
setTimeout(t.step_func(function() {
audio.pause();
audio.src = 'content/test.oga';
}), 0);
},
];
tests.forEach(function(test) {
internals.settings.setAutoplayPolicy('no-user-gesture-required');
async_test(function(t) {
var audio = document.createElement("audio");
test(t, audio);
}, test.name);
});
var testsWithUserGesture = [
// Test that play() on an element when media playback requires a gesture
// returns a resolved promise if there is a user gesture.
function playRequiresUserGestureAndHasIt(t, audio) {
audio.src = "content/test.oga";
playWithUserGesture(t, audio);
},
// Test that play() on an element when media playback requires a gesture
// returns a rejected promise if there is no user gesture.
function playRequiresUserGestureAndDoesNotHaveIt(t, audio) {
// Consume transient user activation triggered in the previous test.
eventSender.consumeUserActivation();
audio.src = "content/test.oga";
playExpectingRejectedPromise(t, audio, "NotAllowedError");
}
];
testsWithUserGesture.forEach(function(test) {
internals.settings.setAutoplayPolicy('user-gesture-required');
async_test(function(t) {
var audio = document.createElement("audio");
test(t, audio);
}, test.name);
});
function playExpectingResolvedPromise(t, audio) {
audio.play().then(t.step_func_done(function() {
assert_equals(arguments.length, 1);
assert_equals(arguments[0], undefined);
}), t.unreached_func());
}
function playExpectingRejectedPromise(t, audio, error) {
audio.play().then(t.unreached_func(), t.step_func_done(function() {
assert_equals(arguments.length, 1);
assert_equals(arguments[0].name, error);
}));
}
function playWithUserGesture(t, audio) {
document.onclick = function() {
playExpectingResolvedPromise(t, audio);
document.onclick = null;
};
eventSender.mouseDown();
eventSender.mouseUp();
}
</script>