chromium/third_party/blink/web_tests/media/media-play-promise.html

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