diff options
Diffstat (limited to 'browser/base/content/test/tabs/browser_audioTabIcon.js')
-rw-r--r-- | browser/base/content/test/tabs/browser_audioTabIcon.js | 676 |
1 files changed, 676 insertions, 0 deletions
diff --git a/browser/base/content/test/tabs/browser_audioTabIcon.js b/browser/base/content/test/tabs/browser_audioTabIcon.js new file mode 100644 index 0000000000..3e3db58f06 --- /dev/null +++ b/browser/base/content/test/tabs/browser_audioTabIcon.js @@ -0,0 +1,676 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +const PAGE = + "https://example.com/browser/browser/base/content/test/tabs/file_mediaPlayback.html"; +const TABATTR_REMOVAL_PREFNAME = "browser.tabs.delayHidingAudioPlayingIconMS"; +const INITIAL_TABATTR_REMOVAL_DELAY_MS = Services.prefs.getIntPref( + TABATTR_REMOVAL_PREFNAME +); + +async function pause(tab, options) { + let extendedDelay = options && options.extendedDelay; + if (extendedDelay) { + // Use 10s to remove possibility of race condition with attr removal. + Services.prefs.setIntPref(TABATTR_REMOVAL_PREFNAME, 10000); + } + + try { + let browser = tab.linkedBrowser; + let awaitDOMAudioPlaybackStopped; + if (!browser.audioMuted) { + awaitDOMAudioPlaybackStopped = BrowserTestUtils.waitForEvent( + browser, + "DOMAudioPlaybackStopped", + "DOMAudioPlaybackStopped event should get fired after pause" + ); + } + await SpecialPowers.spawn(browser, [], async function () { + let audio = content.document.querySelector("audio"); + audio.pause(); + }); + + // If the tab has already be muted, it means the tab won't have soundplaying, + // so we don't need to check this attribute. + if (browser.audioMuted) { + return; + } + + if (extendedDelay) { + ok( + tab.hasAttribute("soundplaying"), + "The tab should still have the soundplaying attribute immediately after pausing" + ); + + await awaitDOMAudioPlaybackStopped; + ok( + tab.hasAttribute("soundplaying"), + "The tab should still have the soundplaying attribute immediately after DOMAudioPlaybackStopped" + ); + } + + await wait_for_tab_playing_event(tab, false); + ok( + !tab.hasAttribute("soundplaying"), + "The tab should not have the soundplaying attribute after the timeout has resolved" + ); + } finally { + // Make sure other tests don't timeout if an exception gets thrown above. + // Need to use setIntPref instead of clearUserPref because + // testing/profiles/common/user.js overrides the default value to help this and + // other tests run faster. + Services.prefs.setIntPref( + TABATTR_REMOVAL_PREFNAME, + INITIAL_TABATTR_REMOVAL_DELAY_MS + ); + } +} + +async function hide_tab(tab) { + let tabHidden = BrowserTestUtils.waitForEvent(tab, "TabHide"); + gBrowser.hideTab(tab); + return tabHidden; +} + +async function show_tab(tab) { + let tabShown = BrowserTestUtils.waitForEvent(tab, "TabShow"); + gBrowser.showTab(tab); + return tabShown; +} + +async function test_tooltip(icon, expectedTooltip, isActiveTab, tab) { + let tooltip = document.getElementById("tabbrowser-tab-tooltip"); + + let tabContent = tab.querySelector(".tab-content"); + await hover_icon(tabContent, tooltip); + + await hover_icon(icon, tooltip); + if (isActiveTab) { + // The active tab should have the keybinding shortcut in the tooltip. + // We check this by ensuring that the strings are not equal but the expected + // message appears in the beginning. + isnot( + tooltip.getAttribute("label"), + expectedTooltip, + "Tooltips should not be equal" + ); + is( + tooltip.getAttribute("label").indexOf(expectedTooltip), + 0, + "Correct tooltip expected" + ); + } else { + is( + tooltip.getAttribute("label"), + expectedTooltip, + "Tooltips should not be equal" + ); + } + leave_icon(icon); +} + +function get_tab_state(tab) { + return JSON.parse(SessionStore.getTabState(tab)); +} + +async function test_muting_using_menu(tab, expectMuted) { + // Show the popup menu + let contextMenu = document.getElementById("tabContextMenu"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(tab, { type: "contextmenu", button: 2 }); + await popupShownPromise; + + // Check the menu + let expectedLabel = expectMuted ? "Unmute Tab" : "Mute Tab"; + let expectedAccessKey = expectMuted ? "m" : "M"; + let toggleMute = document.getElementById("context_toggleMuteTab"); + is(toggleMute.label, expectedLabel, "Correct label expected"); + is(toggleMute.accessKey, expectedAccessKey, "Correct accessKey expected"); + + is( + toggleMute.hasAttribute("muted"), + expectMuted, + "Should have the correct state for the muted attribute" + ); + ok( + !toggleMute.hasAttribute("soundplaying"), + "Should not have the soundplaying attribute" + ); + + await play(tab); + + is( + toggleMute.hasAttribute("muted"), + expectMuted, + "Should have the correct state for the muted attribute" + ); + is( + !toggleMute.hasAttribute("soundplaying"), + expectMuted, + "The value of soundplaying attribute is incorrect" + ); + + await pause(tab); + + is( + toggleMute.hasAttribute("muted"), + expectMuted, + "Should have the correct state for the muted attribute" + ); + ok( + !toggleMute.hasAttribute("soundplaying"), + "Should not have the soundplaying attribute" + ); + + // Click on the menu and wait for the tab to be muted. + let mutedPromise = get_wait_for_mute_promise(tab, !expectMuted); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.activateItem(toggleMute); + await popupHiddenPromise; + await mutedPromise; +} + +async function test_playing_icon_on_tab(tab, browser, isPinned) { + let icon = isPinned ? tab.overlayIcon : tab.overlayIcon; + let isActiveTab = tab === gBrowser.selectedTab; + + await play(tab); + + await test_tooltip(icon, "Mute tab", isActiveTab, tab); + + ok( + !("muted" in get_tab_state(tab)), + "No muted attribute should be persisted" + ); + ok( + !("muteReason" in get_tab_state(tab)), + "No muteReason property should be persisted" + ); + + await test_mute_tab(tab, icon, true); + + ok("muted" in get_tab_state(tab), "Muted attribute should be persisted"); + ok( + "muteReason" in get_tab_state(tab), + "muteReason property should be persisted" + ); + + await test_tooltip(icon, "Unmute tab", isActiveTab, tab); + + await test_mute_tab(tab, icon, false); + + ok( + !("muted" in get_tab_state(tab)), + "No muted attribute should be persisted" + ); + ok( + !("muteReason" in get_tab_state(tab)), + "No muteReason property should be persisted" + ); + + await test_tooltip(icon, "Mute tab", isActiveTab, tab); + + await test_mute_tab(tab, icon, true); + + await pause(tab); + + ok( + tab.hasAttribute("muted") && !tab.hasAttribute("soundplaying"), + "Tab should still be muted but not playing" + ); + ok( + tab.muted && !tab.soundPlaying, + "Tab should still be muted but not playing" + ); + + await test_tooltip(icon, "Unmute tab", isActiveTab, tab); + + await test_mute_tab(tab, icon, false); + + ok( + !tab.hasAttribute("muted") && !tab.hasAttribute("soundplaying"), + "Tab should not be be muted or playing" + ); + ok(!tab.muted && !tab.soundPlaying, "Tab should not be be muted or playing"); + + // Make sure it's possible to mute using the context menu. + await test_muting_using_menu(tab, false); + + // Make sure it's possible to unmute using the context menu. + await test_muting_using_menu(tab, true); +} + +async function test_playing_icon_on_hidden_tab(tab) { + let oldSelectedTab = gBrowser.selectedTab; + let otherTabs = [ + await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true), + await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true), + ]; + let tabContainer = tab.container; + let alltabsButton = document.getElementById("alltabs-button"); + let alltabsBadge = alltabsButton.badgeLabel; + + function assertIconShowing() { + is( + getComputedStyle(alltabsBadge).backgroundImage, + 'url("chrome://browser/skin/tabbrowser/tab-audio-playing-small.svg")', + "The audio playing icon is shown" + ); + is( + tabContainer.getAttribute("hiddensoundplaying"), + "true", + "There are hidden audio tabs" + ); + } + + function assertIconHidden() { + is( + getComputedStyle(alltabsBadge).backgroundImage, + "none", + "The audio playing icon is hidden" + ); + ok( + !tabContainer.hasAttribute("hiddensoundplaying"), + "There are no hidden audio tabs" + ); + } + + // Keep the passed in tab selected. + gBrowser.selectedTab = tab; + + // Play sound in the other two (visible) tabs. + await play(otherTabs[0]); + await play(otherTabs[1]); + assertIconHidden(); + + // Hide one of the noisy tabs, we see the icon. + await hide_tab(otherTabs[0]); + assertIconShowing(); + + // Hiding the other tab keeps the icon. + await hide_tab(otherTabs[1]); + assertIconShowing(); + + // Pausing both tabs will hide the icon. + await pause(otherTabs[0]); + assertIconShowing(); + await pause(otherTabs[1]); + assertIconHidden(); + + // The icon returns when audio starts again. + await play(otherTabs[0]); + await play(otherTabs[1]); + assertIconShowing(); + + // There is still an icon after hiding one tab. + await show_tab(otherTabs[0]); + assertIconShowing(); + + // The icon is hidden when both of the tabs are shown. + await show_tab(otherTabs[1]); + assertIconHidden(); + + await BrowserTestUtils.removeTab(otherTabs[0]); + await BrowserTestUtils.removeTab(otherTabs[1]); + + // Make sure we didn't change the selected tab. + gBrowser.selectedTab = oldSelectedTab; +} + +async function test_swapped_browser_while_playing(oldTab, newBrowser) { + // The tab was muted so it won't have soundplaying attribute even it's playing. + ok( + oldTab.hasAttribute("muted"), + "Expected the correct muted attribute on the old tab" + ); + is( + oldTab.muteReason, + null, + "Expected the correct muteReason attribute on the old tab" + ); + ok( + !oldTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the old tab" + ); + + let newTab = gBrowser.getTabForBrowser(newBrowser); + let AttrChangePromise = BrowserTestUtils.waitForEvent( + newTab, + "TabAttrModified", + false, + event => { + return event.detail.changed.includes("muted"); + } + ); + + gBrowser.swapBrowsersAndCloseOther(newTab, oldTab); + await AttrChangePromise; + + ok( + newTab.hasAttribute("muted"), + "Expected the correct muted attribute on the new tab" + ); + is( + newTab.muteReason, + null, + "Expected the correct muteReason property on the new tab" + ); + ok( + !newTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the new tab" + ); + + await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab); +} + +async function test_swapped_browser_while_not_playing(oldTab, newBrowser) { + ok( + oldTab.hasAttribute("muted"), + "Expected the correct muted attribute on the old tab" + ); + is( + oldTab.muteReason, + null, + "Expected the correct muteReason property on the old tab" + ); + ok( + !oldTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the old tab" + ); + + let newTab = gBrowser.getTabForBrowser(newBrowser); + let AttrChangePromise = BrowserTestUtils.waitForEvent( + newTab, + "TabAttrModified", + false, + event => { + return event.detail.changed.includes("muted"); + } + ); + + let AudioPlaybackPromise = new Promise(resolve => { + let observer = (subject, topic, data) => { + ok(false, "Should not see an audio-playback notification"); + }; + Services.obs.addObserver(observer, "audio-playback"); + setTimeout(() => { + Services.obs.removeObserver(observer, "audio-playback"); + resolve(); + }, 100); + }); + + gBrowser.swapBrowsersAndCloseOther(newTab, oldTab); + await AttrChangePromise; + + ok( + newTab.hasAttribute("muted"), + "Expected the correct muted attribute on the new tab" + ); + is( + newTab.muteReason, + null, + "Expected the correct muteReason property on the new tab" + ); + ok( + !newTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the new tab" + ); + + // Wait to see if an audio-playback event is dispatched. + await AudioPlaybackPromise; + + ok( + newTab.hasAttribute("muted"), + "Expected the correct muted attribute on the new tab" + ); + is( + newTab.muteReason, + null, + "Expected the correct muteReason property on the new tab" + ); + ok( + !newTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the new tab" + ); + + await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab); +} + +async function test_browser_swapping(tab, browser) { + // First, test swapping with a playing but muted tab. + await play(tab); + + await test_mute_tab(tab, tab.overlayIcon, true); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async function (newBrowser) { + await test_swapped_browser_while_playing(tab, newBrowser); + + // Now, test swapping with a muted but not playing tab. + // Note that the tab remains muted, so we only need to pause playback. + tab = gBrowser.getTabForBrowser(newBrowser); + await pause(tab); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + secondAboutBlankBrowser => + test_swapped_browser_while_not_playing(tab, secondAboutBlankBrowser) + ); + } + ); +} + +async function test_click_on_pinned_tab_after_mute() { + async function taskFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + + gBrowser.selectedTab = originallySelectedTab; + isnot( + tab, + gBrowser.selectedTab, + "Sanity check, the tab should not be selected!" + ); + + // Steps to reproduce the bug: + // Pin the tab. + gBrowser.pinTab(tab); + + // Start playback and wait for it to finish. + await play(tab); + + // Mute the tab. + let icon = tab.overlayIcon; + await test_mute_tab(tab, icon, true); + + // Pause playback and wait for it to finish. + await pause(tab); + + // Unmute tab. + await test_mute_tab(tab, icon, false); + + // Now click on the tab. + EventUtils.synthesizeMouseAtCenter(tab.iconImage, { button: 0 }); + + is(tab, gBrowser.selectedTab, "Tab switch should be successful"); + + // Cleanup. + gBrowser.unpinTab(tab); + gBrowser.selectedTab = originallySelectedTab; + } + + let originallySelectedTab = gBrowser.selectedTab; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + taskFn + ); +} + +// This test only does something useful in e10s! +async function test_cross_process_load() { + async function taskFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + + // Start playback and wait for it to finish. + await play(tab); + + let soundPlayingStoppedPromise = BrowserTestUtils.waitForEvent( + tab, + "TabAttrModified", + false, + event => event.detail.changed.includes("soundplaying") + ); + + // Go to a different process. + BrowserTestUtils.loadURIString(browser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(browser); + + await soundPlayingStoppedPromise; + + ok( + !tab.hasAttribute("soundplaying"), + "Tab should not be playing sound any more" + ); + ok(!tab.soundPlaying, "Tab should not be playing sound any more"); + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + taskFn + ); +} + +async function test_mute_keybinding() { + async function test_muting_using_keyboard(tab) { + let mutedPromise = get_wait_for_mute_promise(tab, true); + EventUtils.synthesizeKey("m", { ctrlKey: true }); + await mutedPromise; + mutedPromise = get_wait_for_mute_promise(tab, false); + EventUtils.synthesizeKey("m", { ctrlKey: true }); + await mutedPromise; + } + async function taskFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + + // Make sure it's possible to mute before the tab is playing. + await test_muting_using_keyboard(tab); + + // Start playback and wait for it to finish. + await play(tab); + + // Make sure it's possible to mute after the tab is playing. + await test_muting_using_keyboard(tab); + + // Pause playback and wait for it to finish. + await pause(tab); + + // Make sure things work if the tab is pinned. + gBrowser.pinTab(tab); + + // Make sure it's possible to mute before the tab is playing. + await test_muting_using_keyboard(tab); + + // Start playback and wait for it to finish. + await play(tab); + + // Make sure it's possible to mute after the tab is playing. + await test_muting_using_keyboard(tab); + + gBrowser.unpinTab(tab); + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + taskFn + ); +} + +async function test_on_browser(browser) { + let tab = gBrowser.getTabForBrowser(browser); + + // Test the icon in a normal tab. + await test_playing_icon_on_tab(tab, browser, false); + + gBrowser.pinTab(tab); + + // Test the icon in a pinned tab. + await test_playing_icon_on_tab(tab, browser, true); + + gBrowser.unpinTab(tab); + + // Test the sound playing icon for hidden tabs. + await test_playing_icon_on_hidden_tab(tab); + + // Retest with another browser in the foreground tab + if (gBrowser.selectedBrowser.currentURI.spec == PAGE) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "data:text/html,test", + }, + () => test_on_browser(browser) + ); + } else { + await test_browser_swapping(tab, browser); + } +} + +async function test_delayed_tabattr_removal() { + async function taskFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + await play(tab); + + // Extend the delay to guarantee the soundplaying attribute + // is not removed from the tab when audio is stopped. Without + // the extended delay the attribute could be removed in the + // same tick and the test wouldn't catch that this broke. + await pause(tab, { extendedDelay: true }); + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + taskFn + ); +} + +requestLongerTimeout(2); +add_task(async function test_page() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + test_on_browser + ); +}); + +add_task(test_click_on_pinned_tab_after_mute); + +add_task(test_cross_process_load); + +add_task(test_mute_keybinding); + +add_task(test_delayed_tabattr_removal); |