diff options
Diffstat (limited to '')
11 files changed, 596 insertions, 0 deletions
diff --git a/dom/media/mediasession/test/MediaSessionTestUtils.js b/dom/media/mediasession/test/MediaSessionTestUtils.js new file mode 100644 index 0000000000..1ab0e1fe9b --- /dev/null +++ b/dom/media/mediasession/test/MediaSessionTestUtils.js @@ -0,0 +1,30 @@ +const gMediaSessionActions = [ + "play", + "pause", + "seekbackward", + "seekforward", + "previoustrack", + "nexttrack", + "skipad", + "seekto", + "stop", +]; + +// gCommands and gResults are used in `test_active_mediasession_within_page.html` +const gCommands = { + createMainFrameSession: "create-main-frame-session", + createChildFrameSession: "create-child-frame-session", + destroyChildFrameSessions: "destroy-child-frame-sessions", + destroyActiveChildFrameSession: "destroy-active-child-frame-session", + destroyInactiveChildFrameSession: "destroy-inactive-child-frame-session", +}; + +const gResults = { + mainFrameSession: "main-frame-session", + childFrameSession: "child-session-unchanged", + childFrameSessionUpdated: "child-session-changed", +}; + +function nextWindowMessage() { + return new Promise(r => (window.onmessage = event => r(event))); +} diff --git a/dom/media/mediasession/test/browser.ini b/dom/media/mediasession/test/browser.ini new file mode 100644 index 0000000000..b3cd8300cc --- /dev/null +++ b/dom/media/mediasession/test/browser.ini @@ -0,0 +1,9 @@ +[DEFAULT] +subsuite = media-bc +tags = mediacontrol +support-files = + file_media_session.html + ../../test/gizmo.mp4 + +[browser_active_mediasession_among_tabs.js] + diff --git a/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js b/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js new file mode 100644 index 0000000000..a6a1dbee68 --- /dev/null +++ b/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js @@ -0,0 +1,201 @@ +/* eslint-disable no-undef */ +"use strict"; + +const PAGE = + "https://example.com/browser/dom/media/mediasession/test/file_media_session.html"; + +const ACTION = "previoustrack"; + +add_task(async function setupTestingPref() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.media.mediasession.enabled", true], + ["media.mediacontrol.testingevents.enabled", true], + ], + }); +}); + +/** + * When multiple tabs are all having media session, the latest created one would + * become an active session. When the active media session is destroyed via + * closing the tab, the previous active session would become current active + * session again. + */ +add_task(async function testActiveSessionWhenClosingTab() { + info(`open tab1 and load media session test page`); + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session`); + await checkIfActionReceived(tab1, ACTION); + + info(`open tab2 and load media session test page`); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab2); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab2 should become active session`); + await checkIfActionReceived(tab2, ACTION); + await checkIfActionNotReceived(tab1, ACTION); + + info(`remove tab2`); + const controllerChanged = waitUntilMainMediaControllerChanged(); + BrowserTestUtils.removeTab(tab2); + await controllerChanged; + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session again`); + await checkIfActionReceived(tab1, ACTION); + + info(`remove tab1`); + BrowserTestUtils.removeTab(tab1); +}); + +/** + * This test is similar with `testActiveSessionWhenClosingTab`, the difference + * is that the way we use to destroy active session is via naviagation, not + * closing tab. + */ +add_task(async function testActiveSessionWhenNavigatingTab() { + info(`open tab1 and load media session test page`); + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session`); + await checkIfActionReceived(tab1, ACTION); + + info(`open tab2 and load media session test page`); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab2); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab2 should become active session`); + await checkIfActionReceived(tab2, ACTION); + await checkIfActionNotReceived(tab1, ACTION); + + info(`navigate tab2 to blank page`); + const controllerChanged = waitUntilMainMediaControllerChanged(); + BrowserTestUtils.loadURIString(tab2.linkedBrowser, "about:blank"); + await controllerChanged; + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session`); + await checkIfActionReceived(tab1, ACTION); + + info(`remove tabs`); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +/** + * If we create a media session in a tab where no any playing media exists, then + * that session would not involve in global active media session selection. The + * current active media session would remain unchanged. + */ +add_task(async function testCreatingSessionWithoutPlayingMedia() { + info(`open tab1 and load media session test page`); + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session`); + await checkIfActionReceived(tab1, ACTION); + + info(`open tab2 and load media session test page`); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info( + `session in tab1 is still an active session because there is no media playing in tab2` + ); + await checkIfActionReceived(tab1, ACTION); + await checkIfActionNotReceived(tab2, ACTION); + + info(`remove tabs`); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +/** + * The following are helper functions + */ +async function startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab) { + await Promise.all([ + BrowserUtils.promiseObserved("active-media-session-changed"), + SpecialPowers.spawn(tab.linkedBrowser, [], () => { + const video = content.document.getElementById("testVideo"); + if (!video) { + ok(false, `can't get the media element!`); + } + video.play(); + }), + ]); +} + +async function checkIfActionReceived(tab, action) { + await SpecialPowers.spawn(tab.linkedBrowser, [action], expectedAction => { + return new Promise(resolve => { + const result = content.document.getElementById("result"); + if (!result) { + ok(false, `can't get the element for showing result!`); + } + + function checkAction() { + is( + result.innerHTML, + expectedAction, + `received '${expectedAction}' correctly` + ); + // Reset the result after finishing checking result, then we can dispatch + // same action again without worrying about previous result. + result.innerHTML = ""; + resolve(); + } + + if (result.innerHTML == "") { + info(`wait until receiving action`); + result.addEventListener("actionChanged", () => checkAction(), { + once: true, + }); + } else { + checkAction(); + } + }); + }); +} + +async function checkIfActionNotReceived(tab, action) { + await SpecialPowers.spawn(tab.linkedBrowser, [action], expectedAction => { + return new Promise(resolve => { + const result = content.document.getElementById("result"); + if (!result) { + ok(false, `can't get the element for showing result!`); + } + is(result.innerHTML, "", `should not receive any action`); + ok(result.innerHTML != expectedAction, `not receive '${expectedAction}'`); + resolve(); + }); + }); +} + +function waitUntilMainMediaControllerChanged() { + return BrowserUtils.promiseObserved("main-media-controller-changed"); +} diff --git a/dom/media/mediasession/test/crashtests/crashtests.list b/dom/media/mediasession/test/crashtests/crashtests.list new file mode 100644 index 0000000000..9ca4956ab6 --- /dev/null +++ b/dom/media/mediasession/test/crashtests/crashtests.list @@ -0,0 +1 @@ +load inactive-mediasession.html diff --git a/dom/media/mediasession/test/crashtests/inactive-mediasession.html b/dom/media/mediasession/test/crashtests/inactive-mediasession.html new file mode 100644 index 0000000000..b24fb887ff --- /dev/null +++ b/dom/media/mediasession/test/crashtests/inactive-mediasession.html @@ -0,0 +1,16 @@ +<html> +<head></head> +<script> +const frame = document.createElementNS('http://www.w3.org/1999/xhtml', 'frame'); +document.documentElement.appendChild(frame); + +const windowPointer = frame.contentWindow; +document.documentElement.replaceWith(); + +// Setting attributes on inactive media session should not cause crash. +windowPointer.navigator.mediaSession.setActionHandler('nexttrack', null); +windowPointer.navigator.mediaSession.playbackState = "playing"; +windowPointer.navigator.mediaSession.setPositionState(); +windowPointer.navigator.mediaSession.metadata = null; +</script> +</html> diff --git a/dom/media/mediasession/test/file_media_session.html b/dom/media/mediasession/test/file_media_session.html new file mode 100644 index 0000000000..b6680fb46b --- /dev/null +++ b/dom/media/mediasession/test/file_media_session.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> + <title>Media Session and non-autoplay media</title> +</head> +<body> +<video id="testVideo" src="gizmo.mp4" loop></video> +<h1 id="result"></h1> +<script type="text/javascript"> + +const MediaSessionActions = [ + "play", + "pause", + "seekbackward", + "seekforward", + "previoustrack", + "nexttrack", + "stop", +]; + +for (const action of MediaSessionActions) { + navigator.mediaSession.setActionHandler(action, () => { + // eslint-disable-next-line no-unsanitized/property + document.getElementById("result").innerHTML = action; + document.getElementById("result").dispatchEvent(new CustomEvent("actionChanged")); + }); +} + +</script> +</body> +</html> diff --git a/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html b/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html new file mode 100644 index 0000000000..4d55db2189 --- /dev/null +++ b/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test frame for triggering media session's action handler</title> + <script src="MediaSessionTestUtils.js"></script> + </head> +<body> +<video id="testVideo" src="gizmo.mp4" loop></video> +<script> + +const video = document.getElementById("testVideo"); +const w = window.opener || window.parent; + +window.onmessage = async event => { + if (event.data == "play") { + await video.play(); + // As we can't observe `media-displayed-playback-changed` notification, + // that can only be observed in the chrome process. Therefore, we use a + // workaround instead which is to wait for a while to ensure that the + // controller has already been created in the chrome process. + let timeupdatecount = 0; + await new Promise(r => video.ontimeupdate = () => { + if (++timeupdatecount == 3) { + video.ontimeupdate = null; + r(); + } + }); + w.postMessage("played", "*"); + } +} + +// Setup the action handlers which would post the result back to the main window. +for (const action of gMediaSessionActions) { + navigator.mediaSession.setActionHandler(action, () => { + w.postMessage(action, "*"); + }); +} +</script> +</body> +</html> diff --git a/dom/media/mediasession/test/file_trigger_actionhanlder_window.html b/dom/media/mediasession/test/file_trigger_actionhanlder_window.html new file mode 100644 index 0000000000..f3316d6dad --- /dev/null +++ b/dom/media/mediasession/test/file_trigger_actionhanlder_window.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test window for triggering media session's action handler</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="MediaSessionTestUtils.js"></script> + </head> +<body> +<video id="testVideo" src="gizmo.mp4" loop></video> +<iframe id="childFrame"></iframe> +<script> + +var triggeredActionNums = 0; + +nextWindowMessage().then( + async (event) => { + const testInfo = event.data; + await createSession(testInfo); + // Media session would only become active if there is any media currently + // playing. Non-active media session won't receive any actions. Therefore, + // we start media playback before testing media session. + await startMediaPlayback(testInfo); + for (const action of gMediaSessionActions) { + await waitUntilActionHandlerTriggered(action, testInfo); + } + endTestAndReportResult(); + }); + +/** + * The following are helper functions + */ +async function startMediaPlayback({shouldCreateFrom}) { + info(`wait until media starts playing`); + if (shouldCreateFrom == "main-frame") { + const video = document.getElementById("testVideo"); + await video.play(); + // As we can't observe `media-displayed-playback-changed` notification, + // that can only be observed in the chrome process. Therefore, we use a + // workaround instead which is to wait for a while to ensure that the + // controller has already been created in the chrome process. + let timeupdatecount = 0; + await new Promise(r => video.ontimeupdate = () => { + if (++timeupdatecount == 3) { + video.ontimeupdate = null; + r(); + } + }); + } else { + const iframe = document.getElementById("childFrame"); + iframe.contentWindow.postMessage("play", "*"); + await new Promise(r => { + window.onmessage = event => { + is(event.data, "played", `media started playing in child-frame`); + r(); + }; + }); + } +} + +async function createSession({shouldCreateFrom, origin}) { + info(`create media session in ${shouldCreateFrom}`); + if (shouldCreateFrom == "main-frame") { + // Simply referencing media session will create media session. + navigator.mediaSession; + return; + }; + const frame = document.getElementById("childFrame"); + const originURL = origin == "same-origin" + ? "http://mochi.test:8888" : "http://example.org"; + frame.src = originURL + "/tests/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html"; + await new Promise(r => frame.onload = r); +} + +async function waitUntilActionHandlerTriggered(action, {shouldCreateFrom}) { + info(`wait until '${action}' handler of media session created in ` + + `${shouldCreateFrom} is triggered`); + if (shouldCreateFrom == "main-frame") { + let promise = new Promise(resolve => { + navigator.mediaSession.setActionHandler(action, () => { + ok(true, `Triggered ${action} handler`); + triggeredActionNums++; + resolve(); + }); + }); + SpecialPowers.generateMediaControlKeyTestEvent(action); + await promise; + return; + } + SpecialPowers.generateMediaControlKeyTestEvent(action); + if ((await nextWindowMessage()).data == action) { + info(`Triggered ${action} handler in child-frame`); + triggeredActionNums++; + } +} + +function endTestAndReportResult() { + const w = window.opener || window.parent; + if (triggeredActionNums == gMediaSessionActions.length) { + w.postMessage("success", "*"); + } else { + w.postMessage("fail", "*"); + } +} + +</script> +</body> +</html> diff --git a/dom/media/mediasession/test/mochitest.ini b/dom/media/mediasession/test/mochitest.ini new file mode 100644 index 0000000000..146f1afea8 --- /dev/null +++ b/dom/media/mediasession/test/mochitest.ini @@ -0,0 +1,12 @@ +[DEFAULT] +subsuite = media +tags = mediasession mediacontrol + +support-files = + ../../test/gizmo.mp4 + file_trigger_actionhanlder_frame.html + file_trigger_actionhanlder_window.html + MediaSessionTestUtils.js + +[test_setactionhandler.html] +[test_trigger_actionhanlder.html] diff --git a/dom/media/mediasession/test/test_setactionhandler.html b/dom/media/mediasession/test/test_setactionhandler.html new file mode 100644 index 0000000000..e0fba77d80 --- /dev/null +++ b/dom/media/mediasession/test/test_setactionhandler.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <title></title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> + +SimpleTest.waitForExplicitFinish(); + +const ACTIONS = [ + "play", + "pause", + "seekbackward", + "seekforward", + "previoustrack", + "nexttrack", + "skipad", + "seekto", + "stop", +]; + +(async function testSetActionHandler() { + await setupPreference(); + + for (const action of ACTIONS) { + info(`Test setActionHandler for '${action}'`); + generateAction(action); + ok(true, "it's ok to do " + action + " without any handler"); + + let expectedDetails = generateActionDetails(action); + + let fired = false; + await setHandlerAndTakeAction(action, details => { + ok(hasSameValue(details, expectedDetails), "get expected details for " + action); + fired = !fired; + clearActionHandler(action); + }); + + generateAction(action); + ok(fired, "handler of " + action + " is cleared"); + } + + SimpleTest.finish(); +})(); + +function setupPreference() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.media.mediasession.enabled", true], + ]}); +} + +function generateAction(action) { + let details = generateActionDetails(action); + SpecialPowers.wrap(navigator.mediaSession).notifyHandler(details); +} + +function generateActionDetails(action) { + let details = { action }; + if (action == "seekbackward" || action == "seekforward") { + details.seekOffset = 3.14159; + } else if (action == "seekto") { + details.seekTime = 1.618; + } + return details; +} + +function setHandlerAndTakeAction(action, handler) { + let promise = new Promise(resolve => { + navigator.mediaSession.setActionHandler(action, details => { + handler(details); + resolve(); + }); + }); + generateAction(action); + return promise; +} + +function hasSameValue(a, b) { + // The order of the object matters when stringify the object. + return JSON.stringify(a) == JSON.stringify(b); +} + +function clearActionHandler(action) { + navigator.mediaSession.setActionHandler(action, null); +} + +</script> +</body> +</html> diff --git a/dom/media/mediasession/test/test_trigger_actionhanlder.html b/dom/media/mediasession/test/test_trigger_actionhanlder.html new file mode 100644 index 0000000000..72e8eaf7b5 --- /dev/null +++ b/dom/media/mediasession/test/test_trigger_actionhanlder.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test of triggering media session's action handlers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="MediaSessionTestUtils.js"></script> + </head> +<body> +<script> +/** + * This test is used to test if pressing media control keys can trigger media + * session's corresponding action handler under different situations. + */ +const testCases = [ + { + name: "Triggering action handlers for session created in [main-frame]", + shouldCreateFrom: "main-frame", + }, + { + name: "Triggering action handlers for session created in [same-origin] [child-frame]", + shouldCreateFrom: "child-frame", + origin: "same-origin", + }, + { + name: "Triggering action handlers for session created in [cross-origin] [child-frame]", + shouldCreateFrom: "child-frame", + origin: "cross-origin", + }, +]; + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({"set": [ + ["dom.media.mediasession.enabled", true], + ["media.mediacontrol.testingevents.enabled", true], +]}, startTest()); + +async function startTest() { + for (const testCase of testCases) { + info(`- loading test '${testCase.name}' in a new window -`); + const testURL = "file_trigger_actionhanlder_window.html"; + const testWindow = window.open(testURL, "", "width=500,height=500"); + await new Promise(r => testWindow.onload = r); + + info("- start running test -"); + testWindow.postMessage(testCase, window.origin); + is((await nextWindowMessage()).data, "success", + `- finished test '${testCase.name}' -`); + testWindow.close(); + } + SimpleTest.finish(); +} + +</script> +</body> +</html> |