summaryrefslogtreecommitdiffstats
path: root/dom/media/mediasession/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/media/mediasession/test
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--dom/media/mediasession/test/MediaSessionTestUtils.js30
-rw-r--r--dom/media/mediasession/test/browser.ini9
-rw-r--r--dom/media/mediasession/test/browser_active_mediasession_among_tabs.js201
-rw-r--r--dom/media/mediasession/test/crashtests/crashtests.list1
-rw-r--r--dom/media/mediasession/test/crashtests/inactive-mediasession.html16
-rw-r--r--dom/media/mediasession/test/file_media_session.html31
-rw-r--r--dom/media/mediasession/test/file_trigger_actionhanlder_frame.html40
-rw-r--r--dom/media/mediasession/test/file_trigger_actionhanlder_window.html107
-rw-r--r--dom/media/mediasession/test/mochitest.ini12
-rw-r--r--dom/media/mediasession/test/test_setactionhandler.html92
-rw-r--r--dom/media/mediasession/test/test_trigger_actionhanlder.html57
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>