| <!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> |