mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-19 05:01:33 +00:00
gh-14843: Allow multiple media controllers (gh-14844)
This commit is contained in:
@@ -816,61 +816,57 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#zen-media-playpause-button {
|
||||
.zen-media-playpause-button {
|
||||
list-style-image: url("media-play.svg");
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar.playing #zen-media-playpause-button {
|
||||
.zen-media-card.playing .zen-media-playpause-button {
|
||||
list-style-image: url("media-pause.svg");
|
||||
}
|
||||
|
||||
#zen-media-nexttrack-button {
|
||||
.zen-media-nexttrack-button {
|
||||
list-style-image: url("media-next.svg");
|
||||
}
|
||||
|
||||
#zen-media-previoustrack-button {
|
||||
.zen-media-previoustrack-button {
|
||||
list-style-image: url("media-previous.svg");
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar[muted] #zen-media-mute-button {
|
||||
.zen-media-card[muted] .zen-media-mute-button {
|
||||
list-style-image: url("media-mute.svg");
|
||||
}
|
||||
|
||||
#zen-media-mute-button {
|
||||
.zen-media-mute-button {
|
||||
list-style-image: url("media-unmute.svg");
|
||||
}
|
||||
|
||||
#zen-media-close-button {
|
||||
.zen-media-close-button {
|
||||
list-style-image: url("close.svg");
|
||||
}
|
||||
|
||||
#zen-media-focus-button:hover {
|
||||
.zen-media-focus-button:hover {
|
||||
list-style-image: url("screen.svg");
|
||||
}
|
||||
|
||||
#zen-media-close-button {
|
||||
list-style-image: url("close.svg");
|
||||
}
|
||||
|
||||
#zen-media-mute-mic-button {
|
||||
.zen-media-mute-mic-button {
|
||||
list-style-image: url("microphone-fill.svg");
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar[mic-muted] #zen-media-mute-mic-button {
|
||||
.zen-media-card[mic-muted] .zen-media-mute-mic-button {
|
||||
list-style-image: url("microphone-blocked-fill.svg");
|
||||
fill: rgb(224, 41, 29);
|
||||
}
|
||||
|
||||
#zen-media-mute-camera-button {
|
||||
.zen-media-mute-camera-button {
|
||||
list-style-image: url("video-fill.svg");
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar[camera-muted] #zen-media-mute-camera-button {
|
||||
.zen-media-card[camera-muted] .zen-media-mute-camera-button {
|
||||
list-style-image: url("video-blocked-fill.svg");
|
||||
fill: rgb(224, 41, 29);
|
||||
}
|
||||
|
||||
#zen-media-pip-button {
|
||||
.zen-media-pip-button {
|
||||
list-style-image: url("chrome://global/skin/media/picture-in-picture-open.svg");
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@
|
||||
#zen-media-controls-toolbar {
|
||||
--progress-height: 4px;
|
||||
--button-spacing: 2px;
|
||||
--zen-media-stack-peek: 8px;
|
||||
--zen-media-stack-gap: 6px;
|
||||
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -18,50 +20,23 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#zen-media-buttons-hbox {
|
||||
.zen-media-buttons-hbox {
|
||||
align-items: start;
|
||||
margin-top: -4px;
|
||||
--toolbarbutton-padding-outer: 2px;
|
||||
}
|
||||
|
||||
&:not([media-sharing]) {
|
||||
#media-device-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#media-device-buttons {
|
||||
.media-device-buttons {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&[media-sharing] :is(#zen-media-playback-buttons, #zen-media-mute-button) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not([can-pip]) {
|
||||
#zen-media-info-vbox {
|
||||
width: calc(100% - 26px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#zen-media-pip-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-prev-button,
|
||||
#zen-media-play-pause-button,
|
||||
#zen-media-next-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
image.toolbarbutton-icon {
|
||||
padding: 5px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
#zen-media-progress-bar {
|
||||
.zen-media-progress-bar {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: var(--progress-height);
|
||||
@@ -109,56 +84,29 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
& #zen-media-info-vbox label[overflow] {
|
||||
& .zen-media-info-vbox label[overflow] {
|
||||
animation: zen-back-and-forth-text 10s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
& #zen-media-focus-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: url("chrome://browser/content/zen-images/note-indicator.svg") no-repeat;
|
||||
top: -70%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.8s ease;
|
||||
opacity: 1;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
&:is(:not(.playing:not([muted])), :hover) #zen-media-focus-button::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#zen-media-focus-button {
|
||||
align-self: center;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
& image {
|
||||
&:-moz-broken {
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3C/svg%3E") !important;
|
||||
background: color-mix(in srgb, var(--zen-primary-color) 70%, transparent 30%);
|
||||
}
|
||||
& .zen-media-card {
|
||||
transform: translateY(
|
||||
calc(var(--zen-media-card-index, 0) * (-100% - var(--zen-media-stack-gap)))
|
||||
);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& > toolbaritem {
|
||||
flex-grow: 1;
|
||||
transition: padding 0.3s ease-out;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 4px 6px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-shadow: var(--zen-sidebar-notification-shadow);
|
||||
background-color: var(--zen-sidebar-notification-bg);
|
||||
width: 100%;
|
||||
& .zen-media-focus-button::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .zen-media-card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--zen-media-stack-gap) + var(--zen-media-stack-peek));
|
||||
}
|
||||
}
|
||||
|
||||
.show-on-hover {
|
||||
@@ -174,8 +122,8 @@
|
||||
padding 0.2s ease;
|
||||
}
|
||||
|
||||
#zen-media-current-time,
|
||||
#zen-media-duration {
|
||||
.zen-media-current-time,
|
||||
.zen-media-duration {
|
||||
margin: 0 0 0 1px;
|
||||
font-size: x-small;
|
||||
opacity: 0.7;
|
||||
@@ -184,6 +132,101 @@
|
||||
}
|
||||
}
|
||||
|
||||
.zen-media-card {
|
||||
flex-grow: 1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-shadow: var(--zen-sidebar-notification-shadow);
|
||||
background-color: var(--zen-sidebar-notification-bg);
|
||||
transform-origin: top center;
|
||||
/* Collapsed stack: cards behind the front card peek out at the top,
|
||||
fading the deeper they sit */
|
||||
transform: translateY(calc(-1 * var(--zen-media-stack-peek) * var(--zen-media-card-peek-level, 0)))
|
||||
scale(calc(1 - 0.04 * var(--zen-media-card-peek-level, 0)));
|
||||
opacity: calc(1 - 0.2 * var(--zen-media-card-peek-level, 0));
|
||||
transition:
|
||||
transform 0.1s ease-out,
|
||||
opacity 0.2s ease-out,
|
||||
padding 0.3s ease-out;
|
||||
|
||||
&[stack-overflow] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not([media-sharing]) .media-device-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[media-sharing] :is(.zen-media-playback-buttons, .zen-media-mute-button) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not([can-pip]) {
|
||||
.zen-media-info-vbox {
|
||||
width: calc(100% - 26px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zen-media-pip-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[can-pip] {
|
||||
.zen-media-info-vbox {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.zen-media-pip-button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
& .zen-media-focus-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: url("chrome://browser/content/zen-images/note-indicator.svg") no-repeat;
|
||||
top: -70%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.8s ease;
|
||||
opacity: 1;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
&:not(.playing:not([muted])) .zen-media-focus-button::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Background cards never show the floating notes — only their top sliver
|
||||
is visible and the notes would poke through it */
|
||||
&[stacked-behind] .zen-media-focus-button::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.zen-media-focus-button {
|
||||
align-self: center;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
& image {
|
||||
&:-moz-broken {
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3C/svg%3E") !important;
|
||||
background: color-mix(in srgb, var(--zen-primary-color) 70%, transparent 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar {
|
||||
display: none;
|
||||
animation: none;
|
||||
@@ -191,19 +234,25 @@
|
||||
|
||||
&:not([hidden]) {
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
/* The collapsed height includes the room for the peeking stacked
|
||||
cards, so the stack never shifts the surrounding UI */
|
||||
height: calc(
|
||||
var(--zen-media-collapsed-height, 2.5rem) + var(--zen-media-stack-peek) *
|
||||
var(--zen-media-stack-behind, 0)
|
||||
);
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: height 0.2s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-title,
|
||||
#zen-media-artist {
|
||||
.zen-media-title,
|
||||
.zen-media-artist {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
#zen-media-artist {
|
||||
.zen-media-artist {
|
||||
opacity: 0.7;
|
||||
font-size: smaller;
|
||||
|
||||
@@ -212,19 +261,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-title {
|
||||
.zen-media-title {
|
||||
height: var(--size-item-small);
|
||||
font-size: math;
|
||||
}
|
||||
|
||||
#zen-media-main-vbox,
|
||||
#zen-media-info-vbox,
|
||||
#zen-media-progress-hbox {
|
||||
.zen-media-main-vbox,
|
||||
.zen-media-info-vbox,
|
||||
.zen-media-progress-hbox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#zen-media-info-vbox {
|
||||
#zen-media-controls-toolbar:not([media-position-hidden]) & {
|
||||
.zen-media-info-vbox {
|
||||
.zen-media-card:not([media-position-hidden]) & {
|
||||
transition-delay: 0.01s !important;
|
||||
}
|
||||
overflow-x: hidden;
|
||||
@@ -254,23 +303,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-main-vbox {
|
||||
.zen-media-main-vbox {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#zen-media-progress-hbox {
|
||||
.zen-media-progress-hbox {
|
||||
flex-grow: 1;
|
||||
height: var(--size-item-large);
|
||||
align-items: center;
|
||||
padding-top: 0 !important;
|
||||
|
||||
#zen-media-controls-toolbar[media-position-hidden] & {
|
||||
.zen-media-card[media-position-hidden] & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-controls-hbox {
|
||||
.zen-media-controls-hbox {
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
max-width: 100%;
|
||||
@@ -279,33 +328,23 @@
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
#zen-media-playback-buttons {
|
||||
.zen-media-playback-buttons {
|
||||
justify-content: space-between;
|
||||
max-width: 10em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide #zen-media-focus-button if it doesn't fit in the toolbar */
|
||||
/* Hide .zen-media-focus-button if it doesn't fit in the toolbar */
|
||||
@container media-controls (max-width: 165px) {
|
||||
#zen-media-focus-button {
|
||||
.zen-media-focus-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-info-container {
|
||||
.zen-media-info-container {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar[can-pip] {
|
||||
#zen-media-info-vbox {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
#zen-media-pip-button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
:root:not([zen-sidebar-expanded="true"]) {
|
||||
#zen-media-controls-toolbar {
|
||||
display: none;
|
||||
|
||||
@@ -8,48 +8,41 @@
|
||||
mode="icons"
|
||||
fullscreentoolbar="true"
|
||||
hidden="true">
|
||||
<toolbaritem>
|
||||
<vbox id="zen-media-main-vbox">
|
||||
<vbox>
|
||||
<hbox id="zen-media-info-container" class="show-on-hover">
|
||||
<vbox id="zen-media-info-vbox">
|
||||
<label id="zen-media-title" fadein="true"/>
|
||||
<label id="zen-media-artist" fadein="true"/>
|
||||
</vbox>
|
||||
<hbox id="zen-media-buttons-hbox">
|
||||
<toolbarbutton id="zen-media-pip-button"
|
||||
class="toolbarbutton-1" />
|
||||
<toolbarbutton id="zen-media-close-button"
|
||||
class="toolbarbutton-1" />
|
||||
<html:template id="zen-media-card-template">
|
||||
<toolbaritem class="zen-media-card">
|
||||
<vbox class="zen-media-main-vbox">
|
||||
<vbox>
|
||||
<hbox class="zen-media-info-container show-on-hover">
|
||||
<vbox class="zen-media-info-vbox">
|
||||
<label class="zen-media-title" fadein="true"/>
|
||||
<label class="zen-media-artist" fadein="true"/>
|
||||
</vbox>
|
||||
<hbox class="zen-media-buttons-hbox">
|
||||
<toolbarbutton class="zen-media-pip-button toolbarbutton-1" />
|
||||
<toolbarbutton class="zen-media-close-button toolbarbutton-1" />
|
||||
</hbox>
|
||||
</hbox>
|
||||
<hbox class="zen-media-progress-hbox show-on-hover">
|
||||
<label class="zen-media-current-time">0:00</label>
|
||||
<html:input type="range" class="zen-media-progress-bar"
|
||||
value="0" min="0" max="100" step="0.1"/>
|
||||
<label class="zen-media-duration">0:00</label>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<hbox class="zen-media-controls-hbox">
|
||||
<toolbarbutton class="zen-media-focus-button toolbarbutton-1" />
|
||||
<hbox class="zen-media-playback-buttons">
|
||||
<toolbarbutton class="zen-media-previoustrack-button toolbarbutton-1" />
|
||||
<toolbarbutton class="zen-media-playpause-button toolbarbutton-1" />
|
||||
<toolbarbutton class="zen-media-nexttrack-button toolbarbutton-1" />
|
||||
</hbox>
|
||||
<toolbarbutton class="zen-media-mute-button toolbarbutton-1" />
|
||||
<hbox class="media-device-buttons">
|
||||
<toolbarbutton class="zen-media-mute-mic-button toolbarbutton-1" />
|
||||
<toolbarbutton class="zen-media-mute-camera-button toolbarbutton-1" />
|
||||
</hbox>
|
||||
</hbox>
|
||||
<hbox id="zen-media-progress-hbox" class="show-on-hover">
|
||||
<label id="zen-media-current-time">0:00</label>
|
||||
<html:input type="range" id="zen-media-progress-bar"
|
||||
value="0" min="0" max="100" step="0.1"/>
|
||||
<label id="zen-media-duration">0:00</label>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<hbox id="zen-media-controls-hbox">
|
||||
<toolbarbutton id="zen-media-focus-button"
|
||||
class="toolbarbutton-1" />
|
||||
<hbox id="zen-media-playback-buttons">
|
||||
<toolbarbutton id="zen-media-previoustrack-button"
|
||||
class="toolbarbutton-1" />
|
||||
<toolbarbutton id="zen-media-playpause-button"
|
||||
class="toolbarbutton-1" />
|
||||
<toolbarbutton id="zen-media-nexttrack-button"
|
||||
class="toolbarbutton-1" />
|
||||
</hbox>
|
||||
<toolbarbutton id="zen-media-mute-button"
|
||||
class="toolbarbutton-1" />
|
||||
<hbox id="media-device-buttons">
|
||||
<toolbarbutton id="zen-media-mute-mic-button"
|
||||
class="toolbarbutton-1" />
|
||||
<toolbarbutton id="zen-media-mute-camera-button"
|
||||
class="toolbarbutton-1" />
|
||||
</hbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</toolbaritem>
|
||||
</toolbaritem>
|
||||
</html:template>
|
||||
</toolbar>
|
||||
|
||||
@@ -14,3 +14,5 @@ support-files = [
|
||||
["browser_media_next_track.js"]
|
||||
|
||||
["browser_media_shows_on_tab_switch.js"]
|
||||
|
||||
["browser_media_stack.js"]
|
||||
|
||||
@@ -3,17 +3,6 @@
|
||||
|
||||
"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();
|
||||
@@ -28,8 +17,9 @@ add_task(async function test_media_bar_shows_metadata_from_page() {
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
await waitForMediaBarVisible();
|
||||
|
||||
const titleEl = document.getElementById("zen-media-title");
|
||||
const artistEl = document.getElementById("zen-media-artist");
|
||||
const card = frontMediaCard().element;
|
||||
const titleEl = card.querySelector(".zen-media-title");
|
||||
const artistEl = card.querySelector(".zen-media-artist");
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => titleEl.textContent === "Sandstorm",
|
||||
|
||||
@@ -3,18 +3,6 @@
|
||||
|
||||
"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();
|
||||
@@ -29,9 +17,10 @@ add_task(async function test_mute_from_media_bar() {
|
||||
!mediaTab.linkedBrowser.audioMuted,
|
||||
"precondition: playing tab starts unmuted"
|
||||
);
|
||||
const card = frontMediaCard().element;
|
||||
ok(
|
||||
!mediaBar().hasAttribute("muted"),
|
||||
"precondition: media bar has no muted attribute"
|
||||
!card.hasAttribute("muted"),
|
||||
"precondition: media card has no muted attribute"
|
||||
);
|
||||
|
||||
clickMediaButton("zen-media-mute-button");
|
||||
@@ -40,8 +29,8 @@ add_task(async function test_mute_from_media_bar() {
|
||||
"tab becomes muted after clicking the media bar mute button"
|
||||
);
|
||||
ok(
|
||||
mediaBar().hasAttribute("muted"),
|
||||
"media bar reflects the muted state in its attribute"
|
||||
card.hasAttribute("muted"),
|
||||
"media card reflects the muted state in its attribute"
|
||||
);
|
||||
|
||||
clickMediaButton("zen-media-mute-button");
|
||||
@@ -49,10 +38,7 @@ add_task(async function test_mute_from_media_bar() {
|
||||
() => !mediaTab.linkedBrowser.audioMuted,
|
||||
"clicking again unmutes the tab"
|
||||
);
|
||||
ok(
|
||||
!mediaBar().hasAttribute("muted"),
|
||||
"media bar drops the muted attribute"
|
||||
);
|
||||
ok(!card.hasAttribute("muted"), "media card drops the muted attribute");
|
||||
} finally {
|
||||
if (mediaTab.linkedBrowser.audioMuted) {
|
||||
mediaTab.toggleMuteAudio();
|
||||
|
||||
@@ -3,19 +3,6 @@
|
||||
|
||||
"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();
|
||||
@@ -28,7 +15,9 @@ add_task(async function test_next_track_relays_to_page() {
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
await waitForMediaBarVisible();
|
||||
|
||||
const nextButton = document.getElementById("zen-media-nexttrack-button");
|
||||
const nextButton = frontMediaCard().element.querySelector(
|
||||
".zen-media-nexttrack-button"
|
||||
);
|
||||
|
||||
// supportedkeyschange propagates asynchronously; wait for the bar's
|
||||
// next-track button to become enabled before clicking.
|
||||
@@ -60,7 +49,9 @@ add_task(async function test_next_track_button_disabled_without_handler() {
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
await waitForMediaBarVisible();
|
||||
|
||||
const nextButton = document.getElementById("zen-media-nexttrack-button");
|
||||
const nextButton = frontMediaCard().element.querySelector(
|
||||
".zen-media-nexttrack-button"
|
||||
);
|
||||
Assert.equal(
|
||||
nextButton.disabled,
|
||||
true,
|
||||
|
||||
@@ -3,26 +3,8 @@
|
||||
|
||||
"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();
|
||||
gZenMediaController.closeAllCards();
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => !isMediaBarVisible(),
|
||||
"media bar hides again once the playing tab regains focus"
|
||||
@@ -55,9 +37,9 @@ add_task(async function test_media_bar_shows_when_switching_off_playing_tab() {
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
gZenMediaController._currentBrowser?.browserId,
|
||||
frontMediaCard()?.browser.browserId,
|
||||
mediaTab.linkedBrowser.browserId,
|
||||
"media controller is bound to the media tab's browser, not the selected tab"
|
||||
"media card is bound to the media tab's browser, not the selected tab"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.switchTab(gBrowser, mediaTab);
|
||||
|
||||
61
src/zen/tests/media/browser_media_stack.js
Normal file
61
src/zen/tests/media/browser_media_stack.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_media_stack_shows_multiple_cards() {
|
||||
const originalTab = gBrowser.selectedTab;
|
||||
const tabA = await addMediaTab();
|
||||
const tabB = await addMediaTab();
|
||||
|
||||
try {
|
||||
await BrowserTestUtils.switchTab(gBrowser, tabA);
|
||||
await setMediaSessionMetadata(tabA, { title: "Song A", artist: "A" });
|
||||
await playVideoIn(tabA);
|
||||
|
||||
await BrowserTestUtils.switchTab(gBrowser, tabB);
|
||||
await setMediaSessionMetadata(tabB, { title: "Song B", artist: "B" });
|
||||
await playVideoIn(tabB);
|
||||
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
await waitForMediaBarVisible();
|
||||
|
||||
// Cards reappear on the 500ms tab-switch debounce; wait for both.
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => visibleMediaCards().length === 2,
|
||||
"both playing tabs get their own card in the stack"
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
frontMediaCard().browser.browserId,
|
||||
tabB.linkedBrowser.browserId,
|
||||
"front card belongs to the most recently started media"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
const titles = visibleMediaCards().map(
|
||||
card => card.querySelector(".zen-media-title").textContent
|
||||
);
|
||||
return titles.includes("Song A") && titles.includes("Song B");
|
||||
}, "each card shows its own session's metadata");
|
||||
|
||||
// Closing the front card only removes that session.
|
||||
clickMediaButton("zen-media-close-button");
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => visibleMediaCards().length === 1,
|
||||
"closing the front card only removes that card"
|
||||
);
|
||||
ok(isMediaBarVisible(), "media bar stays visible with the remaining card");
|
||||
Assert.equal(
|
||||
frontMediaCard().browser.browserId,
|
||||
tabA.linkedBrowser.browserId,
|
||||
"remaining card takes the front slot"
|
||||
);
|
||||
} finally {
|
||||
await pauseVideoIn(tabA);
|
||||
await pauseVideoIn(tabB);
|
||||
BrowserTestUtils.removeTab(tabA);
|
||||
BrowserTestUtils.removeTab(tabB);
|
||||
gBrowser.selectedTab = originalTab;
|
||||
}
|
||||
});
|
||||
@@ -63,12 +63,21 @@ async function waitForMediaBarVisible() {
|
||||
);
|
||||
}
|
||||
|
||||
// Click a toolbarbutton on the media bar. We dispatch a "command" event
|
||||
// directly because that's what the controller listens for and it sidesteps
|
||||
// the flakiness of synthesizing a mouse click on a small toolbar button.
|
||||
function clickMediaButton(id) {
|
||||
const button = document.getElementById(id);
|
||||
ok(button, `media bar button ${id} exists`);
|
||||
function frontMediaCard() {
|
||||
return gZenMediaController.frontCard;
|
||||
}
|
||||
|
||||
function visibleMediaCards() {
|
||||
return [...mediaBar().querySelectorAll(".zen-media-card")].filter(
|
||||
card => !card.hidden
|
||||
);
|
||||
}
|
||||
|
||||
function clickMediaButton(className) {
|
||||
const card = frontMediaCard();
|
||||
ok(card, "front media card exists");
|
||||
const button = card.element.querySelector(`.${className}`);
|
||||
ok(button, `media bar button ${className} exists`);
|
||||
button.dispatchEvent(new Event("command", { bubbles: true }));
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ XPCOMUtils.defineLazyServiceGetter(
|
||||
// clicks commonly slide a few pixels (especially on trackpads), and once
|
||||
// the native move starts the OS swallows the mouseup — so keep this
|
||||
// comfortably above click jitter or clicks in the region get lost.
|
||||
const DRAG_START_THRESHOLD_PX = 10;
|
||||
const DRAG_START_THRESHOLD_PX = 4;
|
||||
|
||||
// Content that drives its own mouse interaction without being
|
||||
// interactive HTML content in the spec sense.
|
||||
@@ -256,8 +256,13 @@ export class ZenWindowDragChild extends JSWindowActorChild {
|
||||
if (event.originalTarget?.isNativeAnonymous) {
|
||||
return true;
|
||||
}
|
||||
let target = event.composedTarget;
|
||||
let target = event.explicitOriginalTarget;
|
||||
if (target?.nodeType === Node.TEXT_NODE) {
|
||||
// The hit-test landed on rendered text; a drag here should select
|
||||
// it instead, unless it isn't selectable.
|
||||
if (this.#isSelectableText(target)) {
|
||||
return true;
|
||||
}
|
||||
target = target.parentElement;
|
||||
}
|
||||
if (!target || target.nodeType !== Node.ELEMENT_NODE) {
|
||||
@@ -271,24 +276,10 @@ export class ZenWindowDragChild extends JSWindowActorChild {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return (
|
||||
this.#hasInteractiveCursor(target) || this.#isOverSelectableText(event)
|
||||
);
|
||||
return this.#hasInteractiveCursor(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* A drag starting over selectable text should select it, not move the
|
||||
* window. rangeParent is the caret position Gecko computed for the
|
||||
* event, so this also covers empty space on the same line, where
|
||||
* dragging extends a selection.
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
#isOverSelectableText(event) {
|
||||
const node = event.rangeParent;
|
||||
if (node?.nodeType !== Node.TEXT_NODE) {
|
||||
return false;
|
||||
}
|
||||
#isSelectableText(node) {
|
||||
const parent = node.parentElement;
|
||||
return (
|
||||
!parent ||
|
||||
|
||||
Reference in New Issue
Block a user