diff --git a/src/zen/common/styles/zen-buttons.css b/src/zen/common/styles/zen-buttons.css index d94c2c367..a3dfd404a 100644 --- a/src/zen/common/styles/zen-buttons.css +++ b/src/zen/common/styles/zen-buttons.css @@ -38,6 +38,10 @@ dialog[defaultButton="accept"]::part(dialog-button) { padding-inline-end: 4em; } + @media (-moz-platform: linux) { + padding-inline-end: 3.1em; + } + &::after { border-radius: 4px; font-weight: 600; diff --git a/src/zen/tests/manifest.toml b/src/zen/tests/manifest.toml index 60c0975dc..18334b2f8 100644 --- a/src/zen/tests/manifest.toml +++ b/src/zen/tests/manifest.toml @@ -25,5 +25,9 @@ disable = [ "browser_setDesktopBackgroundPreview.js", ] +[tabMediaIndicator] +source = "browser/components/tabbrowser/test/browser/tabMediaIndicator" +is_direct_path = true + [tooltiptext] source = "toolkit/components/tooltiptext" diff --git a/src/zen/tests/media/browser.toml b/src/zen/tests/media/browser.toml new file mode 100644 index 000000000..52752f239 --- /dev/null +++ b/src/zen/tests/media/browser.toml @@ -0,0 +1,16 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[DEFAULT] +support-files = [ + "head.js", +] + +["browser_media_metadata.js"] + +["browser_media_mute.js"] + +["browser_media_next_track.js"] + +["browser_media_shows_on_tab_switch.js"] diff --git a/src/zen/tests/media/browser_media_metadata.js b/src/zen/tests/media/browser_media_metadata.js new file mode 100644 index 000000000..a048b8cac --- /dev/null +++ b/src/zen/tests/media/browser_media_metadata.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// User flow: +// 1. A page (think Spotify, YouTube Music) plays media and publishes +// title/artist via navigator.mediaSession.metadata. +// 2. User switches off that tab, media bar appears. +// 3. The title and artist labels in the bar show what the page published. +// 4. The page then updates the metadata mid-playback (next song starts). +// 5. The bar updates live, without the user having to switch tabs again. +// +// This is what makes the bar feel connected to the playing page instead of +// a generic "something is playing" indicator. + +add_task(async function test_media_bar_shows_metadata_from_page() { + const originalTab = gBrowser.selectedTab; + const mediaTab = await addMediaTab(); + await BrowserTestUtils.switchTab(gBrowser, mediaTab); + + try { + await setMediaSessionMetadata(mediaTab, { + title: "Sandstorm", + artist: "Darude", + }); + await playVideoIn(mediaTab); + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await waitForMediaBarVisible(); + + const titleEl = document.getElementById("zen-media-title"); + const artistEl = document.getElementById("zen-media-artist"); + + await BrowserTestUtils.waitForCondition( + () => titleEl.textContent === "Sandstorm", + "title label reflects the page's mediaSession metadata" + ); + Assert.equal( + artistEl.textContent, + "Darude", + "artist label reflects the page's mediaSession metadata" + ); + + // Page updates metadata mid-playback. + await setMediaSessionMetadata(mediaTab, { + title: "Levels", + artist: "Avicii", + }); + await BrowserTestUtils.waitForCondition( + () => titleEl.textContent === "Levels", + "title updates live when the page changes its mediaSession metadata" + ); + Assert.equal( + artistEl.textContent, + "Avicii", + "artist updates live alongside the title" + ); + } finally { + await pauseVideoIn(mediaTab); + BrowserTestUtils.removeTab(mediaTab); + gBrowser.selectedTab = originalTab; + } +}); diff --git a/src/zen/tests/media/browser_media_mute.js b/src/zen/tests/media/browser_media_mute.js new file mode 100644 index 000000000..766059fd0 --- /dev/null +++ b/src/zen/tests/media/browser_media_mute.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// User flow: +// 1. User plays a video, switches tabs, media bar appears. +// 2. User clicks the mute button on the Zen media bar. +// 3. The underlying tab actually goes silent (browser.audioMuted flips). +// 4. The media bar reflects that with the `muted` attribute so the icon +// changes. +// 5. Clicking again unmutes. +// +// If this breaks, the user sees a mute button that looks toggled but the +// audio keeps playing — or worse, the tab is muted but the button still +// says "unmuted". + +add_task(async function test_mute_from_media_bar() { + const originalTab = gBrowser.selectedTab; + const mediaTab = await addMediaTab(); + await BrowserTestUtils.switchTab(gBrowser, mediaTab); + + try { + await playVideoIn(mediaTab); + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await waitForMediaBarVisible(); + + ok( + !mediaTab.linkedBrowser.audioMuted, + "precondition: playing tab starts unmuted" + ); + ok( + !mediaBar().hasAttribute("muted"), + "precondition: media bar has no muted attribute" + ); + + clickMediaButton("zen-media-mute-button"); + await BrowserTestUtils.waitForCondition( + () => mediaTab.linkedBrowser.audioMuted, + "tab becomes muted after clicking the media bar mute button" + ); + ok( + mediaBar().hasAttribute("muted"), + "media bar reflects the muted state in its attribute" + ); + + clickMediaButton("zen-media-mute-button"); + await BrowserTestUtils.waitForCondition( + () => !mediaTab.linkedBrowser.audioMuted, + "clicking again unmutes the tab" + ); + ok( + !mediaBar().hasAttribute("muted"), + "media bar drops the muted attribute" + ); + } finally { + if (mediaTab.linkedBrowser.audioMuted) { + mediaTab.toggleMuteAudio(); + } + await pauseVideoIn(mediaTab); + BrowserTestUtils.removeTab(mediaTab); + gBrowser.selectedTab = originalTab; + } +}); diff --git a/src/zen/tests/media/browser_media_next_track.js b/src/zen/tests/media/browser_media_next_track.js new file mode 100644 index 000000000..f4797e7e0 --- /dev/null +++ b/src/zen/tests/media/browser_media_next_track.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// User flow: +// 1. A music page registers a "nexttrack" action handler (like most +// streaming sites do). +// 2. User is on another tab, media bar is showing with the next-track +// button enabled. +// 3. User clicks next-track. +// 4. The action fires inside the page — the page is responsible for +// loading the next song. Zen's job here is to relay the click. +// +// Also guards the button-enablement logic: if the page does NOT register a +// handler, the next-track button must be disabled. Otherwise clicks go +// nowhere and users think the bar is broken. + +add_task(async function test_next_track_relays_to_page() { + const originalTab = gBrowser.selectedTab; + const mediaTab = await addMediaTab(); + await BrowserTestUtils.switchTab(gBrowser, mediaTab); + + try { + await playVideoIn(mediaTab); + await setMediaSessionActionHandler(mediaTab, "nexttrack"); + + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await waitForMediaBarVisible(); + + const nextButton = document.getElementById("zen-media-nexttrack-button"); + + // supportedkeyschange propagates asynchronously; wait for the bar's + // next-track button to become enabled before clicking. + await BrowserTestUtils.waitForCondition( + () => !nextButton.disabled, + "next-track button becomes enabled once the page registers a handler" + ); + + const actionFired = waitForMediaSessionAction(mediaTab); + clickMediaButton("zen-media-nexttrack-button"); + + const result = await actionFired; + ok(result, "page's nexttrack MediaSession handler was invoked"); + } finally { + await pauseVideoIn(mediaTab); + BrowserTestUtils.removeTab(mediaTab); + gBrowser.selectedTab = originalTab; + } +}); + +add_task(async function test_next_track_button_disabled_without_handler() { + const originalTab = gBrowser.selectedTab; + const mediaTab = await addMediaTab(); + await BrowserTestUtils.switchTab(gBrowser, mediaTab); + + try { + // Deliberately do NOT install a nexttrack handler. + await playVideoIn(mediaTab); + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await waitForMediaBarVisible(); + + const nextButton = document.getElementById("zen-media-nexttrack-button"); + Assert.equal( + nextButton.disabled, + true, + "next-track button stays disabled when the page registers no handler" + ); + } finally { + await pauseVideoIn(mediaTab); + BrowserTestUtils.removeTab(mediaTab); + gBrowser.selectedTab = originalTab; + } +}); diff --git a/src/zen/tests/media/browser_media_shows_on_tab_switch.js b/src/zen/tests/media/browser_media_shows_on_tab_switch.js new file mode 100644 index 000000000..783a88af1 --- /dev/null +++ b/src/zen/tests/media/browser_media_shows_on_tab_switch.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// User flow: +// 1. User opens a page with audio and hits play. +// 2. User switches to a different tab. +// 3. The Zen media control bar should appear (so the user can still +// pause/skip without going back to the noisy tab). +// 4. User switches back to the audio tab. +// 5. The media bar should hide again — it's redundant next to the real +// page controls. +// +// This covers the real contract users see: the DOMAudioPlaybackStarted → +// TabSelect → showMediaControls chain in nsZenMediaController, plus the +// inverse path on selecting the playing tab. A regression anywhere in that +// chain (event wiring, the 500ms tab-switch debounce, the hidden attribute +// flip) surfaces as a bar that either never shows or never hides. + +// note: We keep setting timeouts because media player takes a bit to +// get removed (after the animation, more specifically) + +add_task(async function test_media_bar_shows_when_switching_off_playing_tab() { + gZenMediaController.onControllerClose(); + await BrowserTestUtils.waitForCondition( + () => !isMediaBarVisible(), + "media bar hides again once the playing tab regains focus" + ); + + const originalTab = gBrowser.selectedTab; + const mediaTab = await addMediaTab(); + await BrowserTestUtils.switchTab(gBrowser, mediaTab); + + ok( + !isMediaBarVisible(), + "media bar is hidden while the playing tab is the active tab" + ); + + try { + await playVideoIn(mediaTab); + + ok( + !isMediaBarVisible(), + "media bar remains hidden while focused on the playing tab" + ); + + // Switch away. The controller schedules showMediaControls() on a 500ms + // timer; wait for the visibility flip rather than racing it. + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await new Promise(r => setTimeout(r, 1000)); + await BrowserTestUtils.waitForCondition( + isMediaBarVisible, + "media bar becomes visible after switching off the playing tab" + ); + + Assert.equal( + gZenMediaController._currentBrowser?.browserId, + mediaTab.linkedBrowser.browserId, + "media controller is bound to the media tab's browser, not the selected tab" + ); + + await BrowserTestUtils.switchTab(gBrowser, mediaTab); + await new Promise(r => setTimeout(r, 1000)); + await BrowserTestUtils.waitForCondition( + () => !isMediaBarVisible(), + "media bar hides again once the playing tab regains focus" + ); + } finally { + await pauseVideoIn(mediaTab); + BrowserTestUtils.removeTab(mediaTab); + gBrowser.selectedTab = originalTab; + } +}); diff --git a/src/zen/tests/media/head.js b/src/zen/tests/media/head.js new file mode 100644 index 000000000..82de52c6c --- /dev/null +++ b/src/zen/tests/media/head.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Shared mozilla-central fixture from the Picture-in-Picture tests: an HTML +// page with two looping