From eac28634c992d4cb6249d57b71030b0d1f819f78 Mon Sep 17 00:00:00 2001 From: alwu Date: Fri, 15 May 2020 21:50:12 +0000 Subject: [PATCH] Bug 1627999 - part7 : add test. r=bryce This patch will do : - add test cases - introduce the test-only notification `media-displayed-metadata-changed` when the event source updates its metadata The advantage of doing so : - increase test coverage Differential Revision: https://phabricator.services.mozilla.com/D72501 --- .../mediacontrol/MediaControlKeysManager.cpp | 6 + dom/media/mediacontrol/tests/browser.ini | 3 + ...media_control_audio_focus_within_a_page.js | 357 ++++++++++++++++++ .../tests/browser_media_control_metadata.js | 24 -- ...me_with_multiple_child_session_frames.html | 11 + .../tests/file_media_session_page.html | 33 ++ dom/media/mediacontrol/tests/head.js | 50 ++- 7 files changed, 457 insertions(+), 27 deletions(-) create mode 100644 dom/media/mediacontrol/tests/browser_media_control_audio_focus_within_a_page.js create mode 100644 dom/media/mediacontrol/tests/file_main_frame_with_multiple_child_session_frames.html create mode 100644 dom/media/mediacontrol/tests/file_media_session_page.html diff --git a/dom/media/mediacontrol/MediaControlKeysManager.cpp b/dom/media/mediacontrol/MediaControlKeysManager.cpp index 78facff0d7fe..6bd3dc6e7f22 100644 --- a/dom/media/mediacontrol/MediaControlKeysManager.cpp +++ b/dom/media/mediacontrol/MediaControlKeysManager.cpp @@ -126,6 +126,12 @@ void MediaControlKeysManager::SetMediaMetadata( NS_ConvertUTF16toUTF8(mMetadata.mTitle).get(), NS_ConvertUTF16toUTF8(mMetadata.mArtist).get(), NS_ConvertUTF16toUTF8(mMetadata.mAlbum).get()); + if (StaticPrefs::media_mediacontrol_testingevents_enabled()) { + if (nsCOMPtr obs = services::GetObserverService()) { + obs->NotifyObservers(nullptr, "media-displayed-metadata-changed", + nullptr); + } + } } } // namespace dom diff --git a/dom/media/mediacontrol/tests/browser.ini b/dom/media/mediacontrol/tests/browser.ini index 11f625a978ab..4ad73b34dfc5 100644 --- a/dom/media/mediacontrol/tests/browser.ini +++ b/dom/media/mediacontrol/tests/browser.ini @@ -2,6 +2,8 @@ tags = mediacontrol support-files = file_autoplay.html + file_main_frame_with_multiple_child_session_frames.html + file_media_session_page.html file_muted_autoplay.html file_non_autoplay.html file_non_eligible_media.html @@ -13,6 +15,7 @@ support-files = ../../../../toolkit/content/tests/browser/silentAudioTrack.webm [browser_audio_focus_management.js] +[browser_media_control_audio_focus_within_a_page.js] [browser_media_control_captured_audio.js] [browser_media_control_metadata.js] [browser_media_control_keys_event.js] diff --git a/dom/media/mediacontrol/tests/browser_media_control_audio_focus_within_a_page.js b/dom/media/mediacontrol/tests/browser_media_control_audio_focus_within_a_page.js new file mode 100644 index 000000000000..cbc3c5449f95 --- /dev/null +++ b/dom/media/mediacontrol/tests/browser_media_control_audio_focus_within_a_page.js @@ -0,0 +1,357 @@ +/* eslint-disable no-undef */ +const mainPageURL = + "https://example.com/browser/dom/media/mediacontrol/tests/file_main_frame_with_multiple_child_session_frames.html"; +const frameURL = + "https://example.com/browser/dom/media/mediacontrol/tests/file_media_session_page.html"; + +const frame1 = "frame1"; +const frame2 = "frame2"; + +add_task(async function setupTestingPref() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.mediacontrol.testingevents.enabled", true], + ["dom.media.mediasession.enabled", true], + ], + }); +}); + +/** + * This test is used to check the behaviors when we play media from different + * frames. When a page contains multiple frames, if those frames are using the + * media session and set the metadata, then we have to know which frame owns the + * audio focus that would be the last tab playing media. When the frame owns + * audio focus, it means its metadata would be displayed on the virtual control + * interface if it has a media session. + */ +add_task(async function testAudioFocusChangesAmongMultipleFrames() { + /** + * Play the media from the main frame, so it would own the audio focus and + * its metadata should be shown on the virtual control interface. As the main + * frame doesn't use media session, the current metadata would be the default + * metadata. + */ + const tab = await createTabAndLoad(mainPageURL); + await playAndWaitUntilMetadataChanged(tab); + await isUsingDefaultMetadata(tab); + + /** + * Play media for frame1, so the audio focus switches to frame1 because it's + * the last tab playing media and frame1's metadata should be displayed. + */ + await loadPageForFrame(tab, frame1, frameURL); + let metadata = await setMetadataAndGetReturnResult(tab, frame1); + await playAndWaitUntilMetadataChanged(tab, frame1); + isCurrentMetadataEqualTo(metadata); + + /** + * Play media for frame2, so the audio focus switches to frame2 because it's + * the last tab playing media and frame2's metadata should be displayed. + */ + await loadPageForFrame(tab, frame2, frameURL); + metadata = await setMetadataAndGetReturnResult(tab, frame2); + await playAndWaitUntilMetadataChanged(tab, frame2); + isCurrentMetadataEqualTo(metadata); + + /** + * Remove tab and end test. + */ + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testAudioFocusChangesAfterPausingAudioFocusOwner() { + /** + * Play the media from the main frame, so it would own the audio focus and + * its metadata should be shown on the virtual control interface. As the main + * frame doesn't use media session, the current metadata would be the default + * metadata. + */ + const tab = await createTabAndLoad(mainPageURL); + await playAndWaitUntilMetadataChanged(tab); + await isUsingDefaultMetadata(tab); + + /** + * Play media for frame1, so the audio focus switches to frame1 because it's + * the last tab playing media and frame1's metadata should be displayed. + */ + await loadPageForFrame(tab, frame1, frameURL); + let metadata = await setMetadataAndGetReturnResult(tab, frame1); + await playAndWaitUntilMetadataChanged(tab, frame1); + isCurrentMetadataEqualTo(metadata); + + /** + * Pause media for frame1, so the audio focus switches back to the main frame + * which is still playing media. + */ + await pauseAndWaitUntilMetadataChangedFrom(tab, frame1); + await isUsingDefaultMetadata(tab); + + /** + * Remove tab and end test. + */ + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testAudioFocusUnchangesAfterPausingAudioFocusOwner() { + /** + * Play the media from the main frame, so it would own the audio focus and + * its metadata should be shown on the virtual control interface. As the main + * frame doesn't use media session, the current metadata would be the default + * metadata. + */ + const tab = await createTabAndLoad(mainPageURL); + await playAndWaitUntilMetadataChanged(tab); + await isUsingDefaultMetadata(tab); + + /** + * Play media for frame1, so the audio focus switches to frame1 because it's + * the last tab playing media and frame1's metadata should be displayed. + */ + await loadPageForFrame(tab, frame1, frameURL); + let metadata = await setMetadataAndGetReturnResult(tab, frame1); + await playAndWaitUntilMetadataChanged(tab, frame1); + isCurrentMetadataEqualTo(metadata); + + /** + * Pause main frame's media first. When pausing frame1's media, there are not + * other frames playing media, so frame1 still owns the audio focus and its + * metadata should be displayed. + */ + await pauseMediaFrom(tab); + isCurrentMetadataEqualTo(metadata); + + /** + * Remove tab and end test. + */ + await BrowserTestUtils.removeTab(tab); +}); + +add_task( + async function testSwitchAudioFocusToMainFrameAfterRemovingAudioFocusOwner() { + /** + * Play the media from the main frame, so it would own the audio focus and + * its metadata should be displayed on the virtual control interface. As the + * main frame doesn't use media session, the current metadata would be the + * default metadata. + */ + const tab = await createTabAndLoad(mainPageURL); + await playAndWaitUntilMetadataChanged(tab); + await isUsingDefaultMetadata(tab); + + /** + * Play media for frame1, so the audio focus switches to frame1 because it's + * the last tab playing media and frame1's metadata should be displayed. + */ + await loadPageForFrame(tab, frame1, frameURL); + let metadata = await setMetadataAndGetReturnResult(tab, frame1); + await playAndWaitUntilMetadataChanged(tab, frame1); + isCurrentMetadataEqualTo(metadata); + + /** + * Remove frame1, the audio focus would switch to the main frame which + * metadata should be displayed. + */ + await Promise.all([ + waitUntilDisplayedMetadataChanged(), + removeFrame(tab, frame1), + ]); + await isUsingDefaultMetadata(tab); + + /** + * Remove tab and end test. + */ + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testSwitchAudioFocusToIframeAfterRemovingAudioFocusOwner() { + /** + * Play media for frame1, so frame1 owns the audio focus and frame1's metadata + * should be displayed. + */ + const tab = await createTabAndLoad(mainPageURL); + await loadPageForFrame(tab, frame1, frameURL); + let metadataFrame1 = await setMetadataAndGetReturnResult(tab, frame1); + await playAndWaitUntilMetadataChanged(tab, frame1); + isCurrentMetadataEqualTo(metadataFrame1); + + /** + * Play media for frame2, so the audio focus switches to frame2 because it's + * the last tab playing media and frame2's metadata should be displayed. + */ + await loadPageForFrame(tab, frame2, frameURL); + let metadataFrame2 = await setMetadataAndGetReturnResult(tab, frame2); + await playAndWaitUntilMetadataChanged(tab, frame2); + isCurrentMetadataEqualTo(metadataFrame2); + + /** + * Remove frame2, the audio focus would switch to frame1 which metadata should + * be displayed. + */ + await Promise.all([ + waitUntilDisplayedMetadataChanged(), + removeFrame(tab, frame2), + ]); + isCurrentMetadataEqualTo(metadataFrame1); + + /** + * Remove tab and end test. + */ + await BrowserTestUtils.removeTab(tab); + } +); + +add_task(async function testNoAudioFocusAfterRemovingAudioFocusOwner() { + /** + * Play the media from the main frame, so it would own the audio focus and + * its metadata should be shown on the virtual control interface. As the main + * frame doesn't use media session, the current metadata would be the default + * metadata. + */ + const tab = await createTabAndLoad(mainPageURL); + await playAndWaitUntilMetadataChanged(tab); + await isUsingDefaultMetadata(tab); + + /** + * Play media for frame1, so the audio focus switches to frame1 because it's + * the last tab playing media and frame1's metadata should be displayed. + */ + await loadPageForFrame(tab, frame1, frameURL); + let metadata = await setMetadataAndGetReturnResult(tab, frame1); + await playAndWaitUntilMetadataChanged(tab, frame1); + isCurrentMetadataEqualTo(metadata); + + /** + * Pause media in main frame and then remove frame1. As the frame which owns + * the audio focus is removed and no frame is still playing media, the current + * metadata would be the default metadata. + */ + await pauseMediaFrom(tab); + await Promise.all([ + waitUntilDisplayedMetadataChanged(), + removeFrame(tab, frame1), + ]); + await isUsingDefaultMetadata(tab); + + /** + * Remove tab and end test. + */ + await BrowserTestUtils.removeTab(tab); +}); + +/** + * The following are helper functions. + */ +function loadPageForFrame(tab, frameId, pageUrl) { + info(`start to load page for ${frameId}`); + return SpecialPowers.spawn( + tab.linkedBrowser, + [frameId, pageUrl], + async (id, url) => { + const iframe = content.document.getElementById(id); + if (!iframe) { + ok(false, `can not get iframe '${id}'`); + } + iframe.src = url; + await new Promise(r => (iframe.onload = r)); + // Set the document title that would be used as the value for properties + // in frame's medadata. + iframe.contentDocument.title = id; + } + ); +} + +function playMediaFrom(tab, frameId = undefined) { + return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => { + if (id == undefined) { + info(`start to play media from main frame`); + const video = content.document.getElementById("video"); + if (!video) { + ok(false, `can't get the media element!`); + } + return video.play(); + } + + info(`start to play media from ${id}`); + const iframe = content.document.getElementById(id); + if (!iframe) { + ok(false, `can not get ${id}`); + } + iframe.contentWindow.postMessage("play", "*"); + return new Promise(r => { + content.onmessage = event => { + is(event.data, "played", `media started playing in ${id}`); + r(); + }; + }); + }); +} + +function playAndWaitUntilMetadataChanged(tab, frameId = undefined) { + const metadataChanged = waitUntilDisplayedMetadataChanged(); + return Promise.all([metadataChanged, playMediaFrom(tab, frameId)]); +} + +function pauseMediaFrom(tab, frameId = undefined) { + return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => { + if (id == undefined) { + info(`start to pause media from in frame`); + const video = content.document.getElementById("video"); + if (!video) { + ok(false, `can't get the media element!`); + } + return video.pause(); + } + + info(`start to pause media in ${id}`); + const iframe = content.document.getElementById(id); + if (!iframe) { + ok(false, `can not get ${id}`); + } + iframe.contentWindow.postMessage("pause", "*"); + return new Promise(r => { + content.onmessage = event => { + is(event.data, "paused", `media paused in ${id}`); + r(); + }; + }); + }); +} + +function pauseAndWaitUntilMetadataChangedFrom(tab, frameId = undefined) { + const metadataChanged = waitUntilDisplayedMetadataChanged(); + return Promise.all([metadataChanged, pauseMediaFrom(tab, frameId)]); +} + +function setMetadataAndGetReturnResult(tab, frameId) { + info(`start to set metadata for ${frameId}`); + return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => { + const iframe = content.document.getElementById(id); + if (!iframe) { + ok(false, `can not get ${id}`); + } + iframe.contentWindow.postMessage("setMetadata", "*"); + info(`wait until we get metadata for ${id}`); + return new Promise(r => { + content.onmessage = event => { + ok( + event.data.title && event.data.artist && event.data.album, + "correct return format" + ); + r(event.data); + }; + }); + }); +} + +function removeFrame(tab, frameId) { + info(`remove ${frameId}`); + return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => { + const iframe = content.document.getElementById(id); + if (!iframe) { + ok(false, `can not get ${id}`); + } + content.document.body.removeChild(iframe); + }); +} diff --git a/dom/media/mediacontrol/tests/browser_media_control_metadata.js b/dom/media/mediacontrol/tests/browser_media_control_metadata.js index 4b57dd46ead7..8b104b033932 100644 --- a/dom/media/mediacontrol/tests/browser_media_control_metadata.js +++ b/dom/media/mediacontrol/tests/browser_media_control_metadata.js @@ -335,30 +335,6 @@ add_task(async function testMetadataAfterTabNavigation() { /** * The following are helper functions. */ -async function isUsingDefaultMetadata(tab, options = {}) { - let metadata = ChromeUtils.getCurrentActiveMediaMetadata(); - if (options.isPrivateBrowsing) { - is( - metadata.title, - "Firefox is playing media", - "Using generic title to not expose sensitive information" - ); - } else { - await SpecialPowers.spawn(tab.linkedBrowser, [metadata.title], title => { - is( - title, - content.document.title, - "Using website title as a default title" - ); - }); - } - is(metadata.artwork.length, 1, "Default metada contains one artwork"); - ok( - metadata.artwork[0].src.includes(defaultFaviconName), - "Using default favicon as a default art work" - ); -} - function setMediaMetadata(tab, metadata) { const promise = SpecialPowers.spawn(tab.linkedBrowser, [metadata], data => { content.navigator.mediaSession.metadata = new content.MediaMetadata(data); diff --git a/dom/media/mediacontrol/tests/file_main_frame_with_multiple_child_session_frames.html b/dom/media/mediacontrol/tests/file_main_frame_with_multiple_child_session_frames.html new file mode 100644 index 000000000000..f8e7aa9afe08 --- /dev/null +++ b/dom/media/mediacontrol/tests/file_main_frame_with_multiple_child_session_frames.html @@ -0,0 +1,11 @@ + + + +Media control page with multiple iframes which contain media session + + + + + + + diff --git a/dom/media/mediacontrol/tests/file_media_session_page.html b/dom/media/mediacontrol/tests/file_media_session_page.html new file mode 100644 index 000000000000..4e7e855bdbe9 --- /dev/null +++ b/dom/media/mediacontrol/tests/file_media_session_page.html @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/dom/media/mediacontrol/tests/head.js b/dom/media/mediacontrol/tests/head.js index 7e0688735c99..42bca1328167 100644 --- a/dom/media/mediacontrol/tests/head.js +++ b/dom/media/mediacontrol/tests/head.js @@ -214,6 +214,35 @@ function isCurrentMetadataEqualTo(metadata) { } } +/** + * Check if the given tab is using the default metadata. If the tab is being + * used in the private browsing mode, `isPrivateBrowsing` should be definded in + * the `options`. + */ +async function isUsingDefaultMetadata(tab, options = {}) { + let metadata = ChromeUtils.getCurrentActiveMediaMetadata(); + if (options.isPrivateBrowsing) { + is( + metadata.title, + "Firefox is playing media", + "Using generic title to not expose sensitive information" + ); + } else { + await SpecialPowers.spawn(tab.linkedBrowser, [metadata.title], title => { + is( + title, + content.document.title, + "Using website title as a default title" + ); + }); + } + is(metadata.artwork.length, 1, "Default metada contains one artwork"); + ok( + metadata.artwork[0].src.includes("defaultFavicon.svg"), + "Using default favicon as a default art work" + ); +} + /** * Wait until the main media controller changes its playback state, we would * observe that by listening for `media-displayed-playback-changed` @@ -226,6 +255,18 @@ function waitUntilDisplayedPlaybackChanged() { return BrowserUtils.promiseObserved("media-displayed-playback-changed"); } +/** + * Wait until the metadata that would be displayed on the virtual control + * interface changes. we would observe that by listening for + * `media-displayed-metadata-changed` notification. + * + * @return {Promise} + * Resolve when observing `media-displayed-metadata-changed` + */ +function waitUntilDisplayedMetadataChanged() { + return BrowserUtils.promiseObserved("media-displayed-metadata-changed"); +} + /** * Wait until the main media controller has been changed, we would observe that * by listening for the `main-media-controller-changed` notification. @@ -238,9 +279,12 @@ function waitUntilMainMediaControllerChanged() { } /** - * Wait until the main session controller changes its metadata, we would observe - * that by listening for `media-session-controller-metadata-changed` - * notification. + * Wait until any media controller updates its metadata even if it's not the + * main controller. The difference between this function and + * `waitUntilDisplayedMetadataChanged()` is that the changed metadata might come + * from non-main controller so it won't be show on the virtual control + * interface. we would observe that by listening for + * `media-session-controller-metadata-changed` notification. * * @return {Promise} * Resolve when observing `media-session-controller-metadata-changed`