diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/pictureinpicture/tests | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/pictureinpicture/tests')
106 files changed, 11871 insertions, 0 deletions
diff --git a/toolkit/components/pictureinpicture/tests/browser.toml b/toolkit/components/pictureinpicture/tests/browser.toml new file mode 100644 index 0000000000..f28a105698 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser.toml @@ -0,0 +1,239 @@ +[DEFAULT] +support-files = [ + "click-event-helper.js", + "head.js", + "short.mp4", + "no-audio-track.webm", + "test-button-overlay.html", + "test-media-stream.html", + "test-opaque-overlay.html", + "test-page.html", + "test-page-without-audio.html", + "test-page-multiple-contexts.html", + "test-page-pipDisabled.html", + "test-page-with-iframe.html", + "test-page-with-sound.html", + "test-page-with-webvtt.html", + "test-pointer-events-none.html", + "test-reversed.html", + "test-transparent-nested-iframes.html", + "test-transparent-overlay-1.html", + "test-transparent-overlay-2.html", + "test-video.mp4", + "test-video-cropped.mp4", + "test-video-long.mp4", + "test-video-selection.html", + "test-video-vertical.mp4", + "test-webvtt-1.vtt", + "test-webvtt-2.vtt", + "test-webvtt-3.vtt", + "test-webvtt-4.vtt", + "test-webvtt-5.vtt", + "../../../../dom/media/test/gizmo.mp4", + "../../../../dom/media/test/owl.mp3", +] + +prefs = [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled=false", + "media.videocontrols.picture-in-picture.enabled=true", + "media.videocontrols.picture-in-picture.video-toggle.always-show=true", + "media.videocontrols.picture-in-picture.video-toggle.enabled=true", + "media.videocontrols.picture-in-picture.video-toggle.has-used=true", + "media.videocontrols.picture-in-picture.video-toggle.position=\"right\"", + "media.videocontrols.picture-in-picture.video-toggle.testing=true", + "media.videocontrols.picture-in-picture.urlbar-button.enabled=true", +] + +["browser_aaa_run_first_firstTimePiPToggleEvents.js"] + +["browser_aaa_telemetry_togglePiP.js"] + +["browser_audioScrubber.js"] + +["browser_backgroundTab.js"] + +["browser_cannotTriggerFromContent.js"] + +["browser_changePiPSrcInFullscreen.js"] + +["browser_closePipPause.js"] + +["browser_closePip_pageNavigationChanges.js"] + +["browser_closePlayer.js"] + +["browser_closeTab.js"] + +["browser_close_unpip_focus.js"] + +["browser_conflictingPips.js"] + +["browser_contextMenu.js"] +skip-if = ["os == 'linux' && bits == 64 && os_version == '18.04'"] # Bug 1569205 + +["browser_controlsHover.js"] + +["browser_cornerSnapping.js"] +run-if = ["os == 'mac'"] + +["browser_dblclickFullscreen.js"] + +["browser_disableSwipeGestures.js"] +skip-if = ["os == 'mac'"] # Bug 1840716 + +["browser_durationChange.js"] + +["browser_flipIconWithRTL.js"] +skip-if = [ + "os == 'linux' && ccov", # Bug 1678091 + "tsan", # Bug 1678091 +] + +["browser_fontSize_change.js"] + +["browser_fullscreen.js"] +skip-if = [ + "os == 'mac' && debug", # Bug 1566173 + "os == 'linux'", # Bug 1664667 +] + +["browser_improved_controls.js"] + +["browser_keyboardClosePIPwithESC.js"] + +["browser_keyboardFullScreenPIPShortcut.js"] + +["browser_keyboardShortcut.js"] + +["browser_keyboardShortcutClosePIP.js"] + +["browser_keyboardShortcutWithNanDuration.js"] +support-files = ["test-page-with-nan-video-duration.html"] + +["browser_keyboardToggle.js"] + +["browser_mediaStreamVideos.js"] + +["browser_mouseButtonVariation.js"] +skip-if = [ + "debug", + "os == 'linux' && bits == 64 && !debug", # Bug 1549875 +] + +["browser_multiPip.js"] + +["browser_nimbusDisplayDuration.js"] + +["browser_nimbusFirstTimeStyleVariant.js"] + +["browser_nimbusMessageFirstTimePip.js"] + +["browser_nimbusShowIconOnly.js"] + +["browser_noPlayerControlsOnMiddleRightClick.js"] + +["browser_noToggleOnAudio.js"] + +["browser_occluded_window.js"] + +["browser_playerControls.js"] + +["browser_preserveTabPipIconOverlay.js"] + +["browser_privateWindow.js"] + +["browser_removeVideoElement.js"] + +["browser_resizeVideo.js"] +skip-if = ["os == 'linux'"] # Bug 1594223 + +["browser_reversePiP.js"] + +["browser_saveLastPiPLoc.js"] +skip-if = [ + "os == 'linux'", # Bug 1673465 + "os == 'win' && bits == 64 && debug", # Bug 1683002 +] + +["browser_shortcutsAfterFocus.js"] +skip-if = ["os == 'win' && bits == 64 && debug"] # Bug 1683002 + +["browser_showMessage.js"] + +["browser_smallVideoLayout.js"] +skip-if = ["os == 'win' && bits == 64 && debug"] # Bug 1683002 + +["browser_stripVideoStyles.js"] + +["browser_subtitles_settings_panel.js"] + +["browser_tabIconOverlayPiP.js"] + +["browser_telemetry_enhancements.js"] + +["browser_text_tracks_webvtt_1.js"] + +["browser_text_tracks_webvtt_2.js"] + +["browser_text_tracks_webvtt_3.js"] + +["browser_thirdPartyIframe.js"] + +["browser_toggleAfterTabTearOutIn.js"] +skip-if = [ + "os == 'linux' && bits == 64", # Bug 1605546 + "os == 'mac' && !debug", # Bug 1605546 +] + +["browser_toggleButtonOnNanDuration.js"] +skip-if = ["os == 'linux' && !debug"] # Bug 1700504 +support-files = ["test-page-with-nan-video-duration.html"] + +["browser_toggleButtonOverlay.js"] +skip-if = ["true"] # Bug 1546455 + +["browser_toggleMode_2.js"] +skip-if = [ + "os == 'linux'", # Bug 1654971 + "os == 'mac'", # Bug 1654971 +] + +["browser_toggleOnInsertedVideo.js"] + +["browser_toggleOpaqueOverlay.js"] +skip-if = ["true"] # Bug 1546455 + +["browser_togglePointerEventsNone.js"] +skip-if = ["true"] # Bug 1664920, Bug 1628777 + +["browser_togglePolicies.js"] +skip-if = [ + "os == 'linux' && bits == 64", # Bug 1605565 + "apple_catalina && debug", # Bug 1605565 +] + +["browser_togglePositionChange.js"] +skip-if = ["os == 'linux' && bits == 64 && !debug"] # Bug 1738532 + +["browser_toggleSimple.js"] +skip-if = ["os == 'linux'"] # Bug 1546455 + +["browser_toggleTransparentOverlay-1.js"] +skip-if = ["os == 'linux' && bits == 64"] # Bug 1552288 + +["browser_toggleTransparentOverlay-2.js"] +skip-if = ["os == 'linux' && bits == 64 && os_version == '18.04'"] # Bug 1546930 + +["browser_toggle_enabled.js"] + +["browser_toggle_videocontrols.js"] + +["browser_toggle_without_audio.js"] + +["browser_touch_toggle_enablepip.js"] + +["browser_urlbar_toggle.js"] + +["browser_videoEmptied.js"] + +["browser_videoSelection.js"] diff --git a/toolkit/components/pictureinpicture/tests/browser_aaa_run_first_firstTimePiPToggleEvents.js b/toolkit/components/pictureinpicture/tests/browser_aaa_run_first_firstTimePiPToggleEvents.js new file mode 100644 index 0000000000..df6261584c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_aaa_run_first_firstTimePiPToggleEvents.js @@ -0,0 +1,314 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const FIRST_TIME_PIP_TOGGLE_STYLES = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-wrapper": DEFAULT_TOGGLE_OPACITY, + }, + hidden: [], + }, + + hoverToggle: { + opacities: { + ".pip-wrapper": 1.0, + }, + hidden: [], + }, + }, +}; + +const FIRST_CONTEXT_MENU_EXPECTED_EVENTS = [ + [ + "pictureinpicture", + "opened_method", + "contextMenu", + null, + { firstTimeToggle: "true" }, + ], +]; + +const SECOND_CONTEXT_MENU_EXPECTED_EVENTS = [ + [ + "pictureinpicture", + "opened_method", + "contextMenu", + null, + { firstTimeToggle: "false" }, + ], +]; + +const FIRST_TOGGLE_EXPECTED_EVENTS = [ + ["pictureinpicture", "saw_toggle", "toggle", null, { firstTime: "true" }], + [ + "pictureinpicture", + "opened_method", + "toggle", + null, + { firstTimeToggle: "true" }, + ], +]; + +const SECOND_TOGGLE_EXPECTED_EVENTS = [ + ["pictureinpicture", "saw_toggle", "toggle", null, { firstTime: "false" }], + [ + "pictureinpicture", + "opened_method", + "toggle", + null, + { firstTimeToggle: "false" }, + ], +]; + +/** + * This function will open the PiP window by clicking the toggle + * and then close the PiP window + * @param browser The current browser + * @param videoID The video element id + */ +async function openAndClosePipWithToggle(browser, videoID) { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + await prepareForToggleClick(browser, videoID); + + await clearAllContentEvents(); + + // Hover the mouse over the video to reveal the toggle, which is necessary + // if we want to click on the toggle. + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mousemove", + }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + info("Waiting for toggle to become visible"); + await toggleOpacityReachesThreshold( + browser, + videoID, + "hoverVideo", + FIRST_TIME_PIP_TOGGLE_STYLES + ); + + let toggleClientRect = await getToggleClientRect(browser, videoID); + + // The toggle center, because of how it slides out, is actually outside + // of the bounds of a click event. For now, we move the mouse in by a + // hard-coded 15 pixels along the x and y axis to achieve the hover. + let toggleLeft = toggleClientRect.left + 15; + let toggleTop = toggleClientRect.top + 15; + + info("Clicking on toggle, and expecting a Picture-in-Picture window to open"); + // We need to wait for the window to have completed loading before we + // can close it as the document's type required by closeWindow may not + // be available. + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleLeft, + toggleTop, + { + type: "mousedown", + }, + browser + ); + + await BrowserTestUtils.synthesizeMouseAtPoint( + 1, + 1, + { + type: "mouseup", + }, + browser + ); + + let win = await domWindowOpened; + ok(win, "A Picture-in-Picture window opened."); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + }); + + await BrowserTestUtils.closeWindow(win); + await assertSawClickEventOnly(browser); + + await BrowserTestUtils.synthesizeMouseAtPoint(1, 1, {}, browser); + await assertSawMouseEvents(browser, true); +} + +/** + * This function will open the PiP window by with the context menu + * @param browser The current browser + * @param videoID The video element id + */ +async function openAndClosePipWithContextMenu(browser, videoID) { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + let menu = document.getElementById("contentAreaContextMenu"); + let popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown"); + + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "contextmenu", + }, + browser + ); + + await popupshown; + let isContextMenuOpen = menu.state === "showing" || menu.state === "open"; + ok(isContextMenuOpen, "Context menu is open"); + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + + // clear content events + await clearAllContentEvents(); + + let hidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden"); + menu.activateItem(menu.querySelector("#context-video-pictureinpicture")); + await hidden; + + let win = await domWindowOpened; + ok(win, "A Picture-in-Picture window opened."); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + }); + + await BrowserTestUtils.closeWindow(win); +} + +async function clearAllContentEvents() { + // Clear everything. + await TestUtils.waitForCondition(() => { + Services.telemetry.clearEvents(); + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + return !events || !events.length; + }); +} + +add_task(async function test_eventTelemetry() { + Services.telemetry.clearEvents(); + await clearAllContentEvents(); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + Services.telemetry.setEventRecordingEnabled("pictureinpicture", true); + let videoID = "no-controls"; + + const PIP_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; + await SpecialPowers.pushPrefEnv({ + set: [[PIP_PREF, false]], + }); + + // open with context menu for first time + await openAndClosePipWithContextMenu(browser, videoID); + + let filter = { + category: "pictureinpicture", + method: "opened_method", + object: "contextMenu", + }; + await waitForTelemeryEvents( + filter, + FIRST_CONTEXT_MENU_EXPECTED_EVENTS.length, + "content" + ); + + TelemetryTestUtils.assertEvents( + FIRST_CONTEXT_MENU_EXPECTED_EVENTS, + filter, + { clear: true, process: "content" } + ); + + // open with toggle for first time + await SpecialPowers.pushPrefEnv({ + set: [[PIP_PREF, false]], + }); + + await openAndClosePipWithToggle(browser, videoID); + + filter = { + category: "pictureinpicture", + }; + await waitForTelemeryEvents( + filter, + FIRST_TOGGLE_EXPECTED_EVENTS.length, + "content" + ); + + TelemetryTestUtils.assertEvents(FIRST_TOGGLE_EXPECTED_EVENTS, filter, { + clear: true, + process: "content", + }); + + // open with toggle for not first time + await openAndClosePipWithToggle(browser, videoID); + + filter = { + category: "pictureinpicture", + }; + await waitForTelemeryEvents( + filter, + SECOND_TOGGLE_EXPECTED_EVENTS.length, + "content" + ); + + TelemetryTestUtils.assertEvents(SECOND_TOGGLE_EXPECTED_EVENTS, filter, { + clear: true, + process: "content", + }); + + // open with context menu for not first time + await openAndClosePipWithContextMenu(browser, videoID); + + filter = { + category: "pictureinpicture", + method: "opened_method", + object: "contextMenu", + }; + await waitForTelemeryEvents( + filter, + SECOND_CONTEXT_MENU_EXPECTED_EVENTS.length, + "content" + ); + + TelemetryTestUtils.assertEvents( + SECOND_CONTEXT_MENU_EXPECTED_EVENTS, + filter, + { true: false, process: "content" } + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_aaa_telemetry_togglePiP.js b/toolkit/components/pictureinpicture/tests/browser_aaa_telemetry_togglePiP.js new file mode 100644 index 0000000000..d6a7540e15 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_aaa_telemetry_togglePiP.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function getTelemetryToggleEnabled() { + const scalarData = Services.telemetry.getSnapshotForScalars( + "main", + false + ).parent; + return scalarData["pictureinpicture.toggle_enabled"]; +} + +/** + * Tests telemetry for user toggling on or off PiP. + */ +add_task(async () => { + const TOGGLE_PIP_ENABLED_PREF = + "media.videocontrols.picture-in-picture.video-toggle.enabled"; + + await SpecialPowers.pushPrefEnv({ + set: [[TOGGLE_PIP_ENABLED_PREF, true]], + }); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + let contextPiPDisable = document.getElementById( + "context_HidePictureInPictureToggle" + ); + contextPiPDisable.click(); + const enabled = Services.prefs.getBoolPref( + TOGGLE_PIP_ENABLED_PREF, + false + ); + + Assert.equal(enabled, false, "PiP is disabled."); + + await TestUtils.waitForCondition(() => { + return getTelemetryToggleEnabled() === false; + }); + + Assert.equal( + getTelemetryToggleEnabled(), + false, + "PiP is disabled according to Telemetry." + ); + + await SpecialPowers.pushPrefEnv({ + set: [[TOGGLE_PIP_ENABLED_PREF, true]], + }); + + await TestUtils.waitForCondition(() => { + return getTelemetryToggleEnabled() === true; + }); + + Assert.equal( + getTelemetryToggleEnabled(), + true, + "PiP is enabled according to Telemetry." + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_audioScrubber.js b/toolkit/components/pictureinpicture/tests/browser_audioScrubber.js new file mode 100644 index 0000000000..d13d0d13ad --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_audioScrubber.js @@ -0,0 +1,262 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const IMPROVED_CONTROLS_ENABLED_PREF = + "media.videocontrols.picture-in-picture.improved-video-controls.enabled"; + +const videoID = "with-controls"; + +async function getVideoVolume(browser, videoID) { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).volume; + }); +} + +async function getVideoMuted(browser, videoID) { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).muted; + }); +} + +add_task(async function test_audioScrubber() { + await SpecialPowers.pushPrefEnv({ + set: [[IMPROVED_CONTROLS_ENABLED_PREF, true]], + }); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let ratio = pipWin.innerWidth / pipWin.innerHeight; + let height = 750; + let width = Math.floor(ratio * height); + if (pipWin.innerHeight < height || pipWin.innerWidth < width) { + // Resize the PiP window so we know the audio scrubber is visible + let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize"); + pipWin.resizeTo(width, height); + await resizePromise; + } + + let audioScrubber = pipWin.document.getElementById("audio-scrubber"); + audioScrubber.focus(); + + // Volume should be 1 and video should not be muted when opening this video + let actualVolume = await getVideoVolume(browser, videoID); + let expectedVolume = 1; + + let actualMuted = await getVideoMuted(browser, videoID); + + isfuzzy( + actualVolume, + expectedVolume, + Number.EPSILON, + `The actual volume ${actualVolume}. The expected volume ${expectedVolume}` + ); + + ok(!actualMuted, "Video is not muted"); + + let volumeChangePromise = BrowserTestUtils.waitForContentEvent( + browser, + "volumechange", + true + ); + + // Test that left arrow key changes volume by -0.1 + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, pipWin); + + await volumeChangePromise; + + actualVolume = await getVideoVolume(browser, videoID); + expectedVolume = 0.9; + + isfuzzy( + actualVolume, + expectedVolume, + Number.EPSILON, + `The actual volume ${actualVolume}. The expected volume ${expectedVolume}` + ); + + // Test that right arrow key changes volume by +0.1 and does not exceed 1 + EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin); + + actualVolume = await getVideoVolume(browser, videoID); + expectedVolume = 1; + + isfuzzy( + actualVolume, + expectedVolume, + Number.EPSILON, + `The actual volume ${actualVolume}. The expected volume ${expectedVolume}` + ); + + // Test that the mouse can move the audio scrubber to 0.5 + let rect = audioScrubber.getBoundingClientRect(); + + volumeChangePromise = BrowserTestUtils.waitForContentEvent( + browser, + "volumechange", + true + ); + + EventUtils.synthesizeMouse( + audioScrubber, + rect.width / 2, + rect.height / 2, + {}, + pipWin + ); + + await volumeChangePromise; + + actualVolume = await getVideoVolume(browser, videoID); + expectedVolume = 0.5; + + isfuzzy( + actualVolume, + expectedVolume, + 0.01, + `The actual volume ${actualVolume}. The expected volume ${expectedVolume}` + ); + + // Test muting and unmuting the video + let muteButton = pipWin.document.getElementById("audio"); + volumeChangePromise = BrowserTestUtils.waitForContentEvent( + browser, + "volumechange", + true + ); + muteButton.click(); + await volumeChangePromise; + + ok( + await getVideoMuted(browser, videoID), + "The video is muted aftering clicking the mute button" + ); + is( + audioScrubber.max, + "0", + "The max of the audio scrubber is 0, so the volume slider appears that the volume is 0" + ); + + volumeChangePromise = BrowserTestUtils.waitForContentEvent( + browser, + "volumechange", + true + ); + muteButton.click(); + await volumeChangePromise; + + ok( + !(await getVideoMuted(browser, videoID)), + "The video is muted aftering clicking the mute button" + ); + isfuzzy( + actualVolume, + expectedVolume, + 0.01, + `The volume is still ${actualVolume}.` + ); + + volumeChangePromise = BrowserTestUtils.waitForContentEvent( + browser, + "volumechange", + true + ); + + // Test that the audio scrubber can be dragged from 0.5 to 0 and the video gets muted + EventUtils.synthesizeMouse( + audioScrubber, + rect.width / 2, + rect.height / 2, + { type: "mousedown" }, + pipWin + ); + EventUtils.synthesizeMouse( + audioScrubber, + 0, + rect.height / 2, + { type: "mousemove" }, + pipWin + ); + EventUtils.synthesizeMouse( + audioScrubber, + 0, + rect.height / 2, + { type: "mouseup" }, + pipWin + ); + + await volumeChangePromise; + + actualVolume = await getVideoVolume(browser, videoID); + expectedVolume = 0; + + isfuzzy( + actualVolume, + expectedVolume, + Number.EPSILON, + `The actual volume ${actualVolume}. The expected volume ${expectedVolume}` + ); + + ok( + await getVideoMuted(browser, videoID), + "Video is now muted because slider was moved to 0" + ); + + // Test that the left arrow key does not unmute the video and the volume remains at 0 + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, pipWin); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, pipWin); + + actualVolume = await getVideoVolume(browser, videoID); + + isfuzzy( + actualVolume, + expectedVolume, + Number.EPSILON, + `The actual volume ${actualVolume}. The expected volume ${expectedVolume}` + ); + + ok( + await getVideoMuted(browser, videoID), + "Video is now muted because slider is still at 0" + ); + + volumeChangePromise = BrowserTestUtils.waitForContentEvent( + browser, + "volumechange", + true + ); + + // Test that the right arrow key will increase the volume by +0.1 and will unmute the video + EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin); + + await volumeChangePromise; + + actualVolume = await getVideoVolume(browser, videoID); + expectedVolume = 0.1; + + isfuzzy( + actualVolume, + expectedVolume, + Number.EPSILON, + `The actual volume ${actualVolume}. The expected volume ${expectedVolume}` + ); + + ok( + !(await getVideoMuted(browser, videoID)), + "Video is no longer muted because we moved the slider" + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_backgroundTab.js b/toolkit/components/pictureinpicture/tests/browser_backgroundTab.js new file mode 100644 index 0000000000..e1f96748a5 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_backgroundTab.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/** + * This test creates a PiP window, then switches to another tab and confirms + * that the PiP tab is still active. + */ +add_task(async () => { + let videoID = "no-controls"; + let firstTab = gBrowser.selectedTab; + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let originatingTab = gBrowser.getTabForBrowser(browser); + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + await BrowserTestUtils.switchTab(gBrowser, firstTab); + + let switcher = gBrowser._getSwitcher(); + + Assert.equal( + switcher.getTabState(originatingTab), + switcher.STATE_LOADED, + "The originating browser tab should be in STATE_LOADED." + ); + Assert.equal( + browser.docShellIsActive, + true, + "The docshell should be active in the originating tab" + ); + + // We need to destroy the current AsyncTabSwitcher to avoid + // tabrowser.shouldActivateDocShell going in the + // AsyncTabSwitcher.shouldActivateDocShell code path which isn't PiP aware. + switcher.destroy(); + + // Closing with window.close doesn't actually pause the video, so click + // the close button instead. + pipWin.document.getElementById("close").click(); + await BrowserTestUtils.windowClosed(pipWin); + + Assert.equal( + browser.docShellIsActive, + false, + "The docshell should be inactive in the originating tab" + ); + } + ); +}); + +/** + * This test creates a PiP window, then minimizes the browser and confirms + * that the PiP tab is still active. + */ +add_task(async () => { + let videoID = "no-controls"; + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let originatingTab = gBrowser.getTabForBrowser(browser); + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let promiseSizeModeChange = BrowserTestUtils.waitForEvent( + window, + "sizemodechange" + ); + window.minimize(); + await promiseSizeModeChange; + + let switcher = gBrowser._getSwitcher(); + + Assert.equal( + switcher.getTabState(originatingTab), + switcher.STATE_LOADED, + "The originating browser tab should be in STATE_LOADED." + ); + + await BrowserTestUtils.closeWindow(pipWin); + + // Workaround bug 1782134. + window.restore(); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_cannotTriggerFromContent.js b/toolkit/components/pictureinpicture/tests/browser_cannotTriggerFromContent.js new file mode 100644 index 0000000000..4c3d32b474 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_cannotTriggerFromContent.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the MozTogglePictureInPicture event is ignored if + * fired by unprivileged web content. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + // For now, the easiest way to ensure that this didn't happen is to fail + // if we receive the PictureInPicture:Request message. + const MESSAGE = "PictureInPicture:Request"; + let sawMessage = false; + let listener = msg => { + sawMessage = true; + }; + browser.messageManager.addMessageListener(MESSAGE, listener); + await SpecialPowers.spawn(browser, [], async () => { + content.wrappedJSObject.fireEvents(); + }); + browser.messageManager.removeMessageListener(MESSAGE, listener); + ok(!sawMessage, "Got PictureInPicture:Request message unexpectedly."); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_changePiPSrcInFullscreen.js b/toolkit/components/pictureinpicture/tests/browser_changePiPSrcInFullscreen.js new file mode 100644 index 0000000000..e971f17296 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_changePiPSrcInFullscreen.js @@ -0,0 +1,511 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const NEW_VIDEO_ASPECT_RATIO = 1.334; + +async function switchVideoSource(browser, src) { + await ContentTask.spawn(browser, { src }, async ({ src }) => { + let doc = content.document; + let video = doc.getElementById("no-controls"); + video.src = src; + }); +} + +/** + * + * @param {Object} actual The actual size and position of the window + * @param {Object} expected The expected size and position of the window + * @param {String} message A message to print before asserting the size and position + */ +function assertEvent(actual, expected, message) { + info(message); + isfuzzy( + actual.width, + expected.width, + ACCEPTABLE_DIFFERENCE, + `The actual width: ${actual.width}. The expected width: ${expected.width}` + ); + isfuzzy( + actual.height, + expected.height, + ACCEPTABLE_DIFFERENCE, + `The actual height: ${actual.height}. The expected height: ${expected.height}` + ); + isfuzzy( + actual.left, + expected.left, + ACCEPTABLE_DIFFERENCE, + `The actual left: ${actual.left}. The expected left: ${expected.left}` + ); + isfuzzy( + actual.top, + expected.top, + ACCEPTABLE_DIFFERENCE, + `The actual top: ${actual.top}. The expected top: ${expected.top}` + ); +} + +/** + * This test is our control test. This tests that when the PiP window exits + * fullscreen it will return to the size it was before being fullscreened. + */ +add_task(async function testNoSrcChangeFullscreen() { + // After opening the PiP window, it is resized to 640x360. There is a change + // the PiP window will open with that size. To prevent that we override the + // last saved position so we open at (0, 0) and 300x300. + overrideSavedPosition(0, 0, 300, 300); + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + let pipWin = await triggerPictureInPicture(browser, "no-controls"); + let controls = pipWin.document.getElementById("controls"); + const screen = pipWin.screen; + + let resizeEventArray = []; + pipWin.addEventListener("resize", event => { + let win = event.target; + let obj = { + width: win.innerWidth, + height: win.innerHeight, + left: win.screenLeft, + top: win.screenTop, + }; + resizeEventArray.push(obj); + }); + + // Move the PiP window to an unsaved location + let left = 100; + let top = 100; + pipWin.moveTo(left, top); + + await BrowserTestUtils.waitForCondition( + () => pipWin.screenLeft === 100 && pipWin.screenTop === 100, + "Waiting for PiP to move to 100, 100" + ); + + let width = 640; + let height = 360; + + let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize"); + pipWin.resizeTo(width, height); + await resizePromise; + + Assert.equal( + resizeEventArray.length, + 1, + "resizeEventArray should have 1 event" + ); + + let actualEvent = resizeEventArray.splice(0, 1)[0]; + let expectedEvent = { width, height, left, top }; + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly positioned before fullscreen" + ); + + await promiseFullscreenEntered(pipWin, async () => { + EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.equal( + pipWin.document.fullscreenElement, + pipWin.document.body, + "Double-click caused us to enter fullscreen." + ); + + await BrowserTestUtils.waitForCondition( + () => resizeEventArray.length === 1, + "Waiting for resizeEventArray to have 1 event" + ); + + actualEvent = resizeEventArray.splice(0, 1)[0]; + expectedEvent = { + width: screen.width, + height: screen.height, + left: screen.left, + top: screen.top, + }; + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly fullscreened before switching source" + ); + + await promiseFullscreenExited(pipWin, async () => { + EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.ok( + !pipWin.document.fullscreenElement, + "Double-click caused us to exit fullscreen." + ); + + await BrowserTestUtils.waitForCondition( + () => resizeEventArray.length >= 1, + "Waiting for resizeEventArray to have 1 event, got " + + resizeEventArray.length + ); + + actualEvent = resizeEventArray.splice(0, 1)[0]; + expectedEvent = { width, height, left, top }; + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly positioned after exiting fullscreen" + ); + + await ensureMessageAndClosePiP(browser, "no-controls", pipWin, false); + + clearSavedPosition(); + } + ); +}); + +/** + * This function tests changing the src of a Picture-in-Picture player while + * the player is fullscreened and then ensuring the that video stays + * fullscreened after the src change and that the player will resize to the new + * video size. + */ +add_task(async function testChangingSameSizeVideoSrcFullscreen() { + // After opening the PiP window, it is resized to 640x360. There is a change + // the PiP window will open with that size. To prevent that we override the + // last saved position so we open at (0, 0) and 300x300. + overrideSavedPosition(0, 0, 300, 300); + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + let pipWin = await triggerPictureInPicture(browser, "no-controls"); + let controls = pipWin.document.getElementById("controls"); + const screen = pipWin.screen; + let sandbox = sinon.createSandbox(); + let resizeToVideoSpy = sandbox.spy(pipWin, "resizeToVideo"); + + let resizeEventArray = []; + pipWin.addEventListener("resize", event => { + let win = event.target; + let obj = { + width: win.innerWidth, + height: win.innerHeight, + left: win.screenLeft, + top: win.screenTop, + }; + resizeEventArray.push(obj); + }); + + // Move the PiP window to an unsaved location + let left = 100; + let top = 100; + pipWin.moveTo(left, top); + + await BrowserTestUtils.waitForCondition( + () => pipWin.screenLeft === 100 && pipWin.screenTop === 100, + "Waiting for PiP to move to 100, 100" + ); + + let width = 640; + let height = 360; + + let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize"); + pipWin.resizeTo(width, height); + await resizePromise; + + Assert.equal( + resizeEventArray.length, + 1, + "resizeEventArray should have 1 event" + ); + + let actualEvent = resizeEventArray.splice(0, 1)[0]; + let expectedEvent = { width, height, left, top }; + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly positioned before fullscreen" + ); + + await promiseFullscreenEntered(pipWin, async () => { + EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.equal( + pipWin.document.fullscreenElement, + pipWin.document.body, + "Double-click caused us to enter fullscreen." + ); + + await BrowserTestUtils.waitForCondition( + () => resizeEventArray.length === 1, + "Waiting for resizeEventArray to have 1 event" + ); + + actualEvent = resizeEventArray.splice(0, 1)[0]; + expectedEvent = { + width: screen.width, + height: screen.height, + left: screen.left, + top: screen.top, + }; + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly fullscreened before switching source" + ); + + await switchVideoSource(browser, "test-video.mp4"); + + await BrowserTestUtils.waitForCondition( + () => resizeToVideoSpy.calledOnce, + "Waiting for deferredResize to be updated" + ); + + await promiseFullscreenExited(pipWin, async () => { + EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.ok( + !pipWin.document.fullscreenElement, + "Double-click caused us to exit fullscreen." + ); + + await BrowserTestUtils.waitForCondition( + () => resizeEventArray.length >= 1, + "Waiting for resizeEventArray to have 1 event, got " + + resizeEventArray.length + ); + + actualEvent = resizeEventArray.splice(0, 1)[0]; + expectedEvent = { width, height, left, top }; + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly positioned after exiting fullscreen" + ); + + sandbox.restore(); + await ensureMessageAndClosePiP(browser, "no-controls", pipWin, false); + + clearSavedPosition(); + } + ); +}); + +/** + * This is similar to the previous test but in this test we switch to a video + * with a different aspect ratio to confirm that the PiP window will take the + * new aspect ratio after exiting fullscreen. We also exit fullscreen with the + * escape key instead of double clicking in this test. + */ +add_task(async function testChangingDifferentSizeVideoSrcFullscreen() { + // After opening the PiP window, it is resized to 640x360. There is a change + // the PiP window will open with that size. To prevent that we override the + // last saved position so we open at (0, 0) and 300x300. + overrideSavedPosition(0, 0, 300, 300); + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + let pipWin = await triggerPictureInPicture(browser, "no-controls"); + let controls = pipWin.document.getElementById("controls"); + const screen = pipWin.screen; + let sandbox = sinon.createSandbox(); + let resizeToVideoSpy = sandbox.spy(pipWin, "resizeToVideo"); + + let resizeEventArray = []; + pipWin.addEventListener("resize", event => { + let win = event.target; + let obj = { + width: win.innerWidth, + height: win.innerHeight, + left: win.screenLeft, + top: win.screenTop, + }; + resizeEventArray.push(obj); + }); + + // Move the PiP window to an unsaved location + let left = 100; + let top = 100; + pipWin.moveTo(left, top); + + await BrowserTestUtils.waitForCondition( + () => pipWin.screenLeft === 100 && pipWin.screenTop === 100, + "Waiting for PiP to move to 100, 100" + ); + + let width = 640; + let height = 360; + + let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize"); + pipWin.resizeTo(width, height); + await resizePromise; + + Assert.equal( + resizeEventArray.length, + 1, + "resizeEventArray should have 1 event" + ); + + let actualEvent = resizeEventArray.splice(0, 1)[0]; + let expectedEvent = { width, height, left, top }; + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly positioned before fullscreen" + ); + + await promiseFullscreenEntered(pipWin, async () => { + EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.equal( + pipWin.document.fullscreenElement, + pipWin.document.body, + "Double-click caused us to enter fullscreen." + ); + + await BrowserTestUtils.waitForCondition( + () => resizeEventArray.length === 1, + "Waiting for resizeEventArray to have 1 event" + ); + + actualEvent = resizeEventArray.splice(0, 1)[0]; + expectedEvent = { + width: screen.width, + height: screen.height, + left: screen.left, + top: screen.top, + }; + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly fullscreened before switching source" + ); + + let previousWidth = pipWin.getDeferredResize().width; + + await switchVideoSource(browser, "test-video-long.mp4"); + + // Confirm that we are updating the `deferredResize` and not actually resizing + await BrowserTestUtils.waitForCondition( + () => resizeToVideoSpy.calledOnce, + "Waiting for deferredResize to be updated" + ); + + // Confirm that we updated the deferredResize to the new width + await BrowserTestUtils.waitForCondition( + () => previousWidth !== pipWin.getDeferredResize().width, + "Waiting for deferredResize to update" + ); + + await promiseFullscreenExited(pipWin, async () => { + EventUtils.synthesizeKey("KEY_Escape", {}, pipWin); + }); + + Assert.ok( + !pipWin.document.fullscreenElement, + "Escape key caused us to exit fullscreen." + ); + + await BrowserTestUtils.waitForCondition( + () => resizeEventArray.length >= 1, + "Waiting for resizeEventArray to have 1 event, got " + + resizeEventArray.length + ); + + actualEvent = resizeEventArray.splice(0, 1)[0]; + expectedEvent = { + width: height * NEW_VIDEO_ASPECT_RATIO, + height, + left, + top, + }; + + // When two resize events happen very close together we optimize by + // "coalescing" the two resizes into a single resize event. Sometimes + // the events aren't "coalesced" together (I don't know why) so I check + // if the most recent event is what we are looking for and if it is not + // then I'll wait for the resize event we are looking for. + if ( + Math.abs( + actualEvent.width - expectedEvent.width <= ACCEPTABLE_DIFFERENCE + ) + ) { + // The exit fullscreen resize events were "coalesced". + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly positioned after exiting fullscreen" + ); + } else { + // For some reason the exit fullscreen resize events weren't "coalesced" + // so we have to wait for the next resize event. + await BrowserTestUtils.waitForCondition( + () => resizeEventArray.length === 1, + "Waiting for resizeEventArray to have 1 event" + ); + + actualEvent = resizeEventArray.splice(0, 1)[0]; + + assertEvent( + actualEvent, + expectedEvent, + "The PiP window has been correctly positioned after exiting fullscreen" + ); + } + + sandbox.restore(); + await ensureMessageAndClosePiP(browser, "no-controls", pipWin, false); + + clearSavedPosition(); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_closePipPause.js b/toolkit/components/pictureinpicture/tests/browser_closePipPause.js new file mode 100644 index 0000000000..b87888e411 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_closePipPause.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks that MediaStream videos are not paused when closing + * the PiP window. + */ +add_task(async function test_close_mediaStreamVideos() { + await BrowserTestUtils.withNewTab( + { + url: TEST_ROOT + "test-media-stream.html", + gBrowser, + }, + async browser => { + await SpecialPowers.spawn(browser, [], async () => { + // Construct a new video element, and capture a stream from it + // to redirect to both testing videos + let newVideo = content.document.createElement("video"); + newVideo.src = "test-video.mp4"; + newVideo.id = "media-stream-video"; + content.document.body.appendChild(newVideo); + newVideo.loop = true; + }); + await ensureVideosReady(browser); + + // Modify both the "with-controls" and "no-controls" videos so that they mirror + // the new video that we just added via MediaStream. + await SpecialPowers.spawn(browser, [], async () => { + let newVideo = content.document.getElementById("media-stream-video"); + newVideo.play(); + + for (let videoID of ["with-controls", "no-controls"]) { + let testedVideo = content.document.createElement("video"); + testedVideo.id = videoID; + testedVideo.srcObject = newVideo.mozCaptureStream().clone(); + content.document.body.prepend(testedVideo); + if ( + testedVideo.readyState < content.HTMLMediaElement.HAVE_ENOUGH_DATA + ) { + info(`Waiting for 'canplaythrough' for '${testedVideo.id}'`); + await ContentTaskUtils.waitForEvent(testedVideo, "canplaythrough"); + } + testedVideo.play(); + } + }); + + for (let videoID of ["with-controls", "no-controls"]) { + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + ok( + !(await isVideoPaused(browser, videoID)), + "The video is not paused in PiP window." + ); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + ok( + !(await isVideoPaused(browser, videoID)), + "The video is not paused after closing PiP window." + ); + } + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_closePip_pageNavigationChanges.js b/toolkit/components/pictureinpicture/tests/browser_closePip_pageNavigationChanges.js new file mode 100644 index 0000000000..c64b5f2b14 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_closePip_pageNavigationChanges.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the pip window closes when the pagehide page lifecycle event + * is not detected and if a video is not loaded with a src. + */ +add_task(async function test_close_empty_pip_window() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let videoID = "with-controls"; + + await ensureVideosReady(browser); + + let emptied = SpecialPowers.spawn(browser, [{ videoID }], async args => { + let video = content.document.getElementById(args.videoID); + info("Waiting for emptied event to be called"); + await ContentTaskUtils.waitForEvent(video, "emptied"); + }); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + await SpecialPowers.spawn(browser, [{ videoID }], async args => { + let video = content.document.getElementById(args.videoID); + video.removeAttribute("src"); + video.load(); + }); + await emptied; + await pipClosed; + } + ); +}); + +/** + * Tests that the pip window closes when navigating to another page + * via the pagehide page lifecycle event. + */ +add_task(async function test_close_pagehide() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let videoID = "with-controls"; + + await ensureVideosReady(browser); + await SpecialPowers.spawn(browser, [{ videoID }], async args => { + let video = content.document.getElementById(args.videoID); + video.onemptied = () => { + // Since we handle pagehide first, handle emptied should not be invoked + ok(false, "emptied not expected to be called"); + }; + }); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + await SpecialPowers.spawn(browser, [{ videoID }], async args => { + content.location.href = "otherpage.html"; + }); + + await pipClosed; + } + ); +}); + +/** + * Tests that the pip window remains open if the pagehide page lifecycle + * event is not detected and if the video is still loaded with a src. + */ +add_task(async function test_open_pip_window_history_nav() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let videoID = "with-controls"; + + await ensureVideosReady(browser); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + await SpecialPowers.spawn(browser, [{ videoID }], async args => { + let popStatePromise = ContentTaskUtils.waitForEvent( + content, + "popstate" + ); + content.history.pushState({}, "new page", "test-page-with-sound.html"); + content.history.back(); + await popStatePromise; + }); + + ok(!pipWin.closed, "pip windows should still be open"); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_closePlayer.js b/toolkit/components/pictureinpicture/tests/browser_closePlayer.js new file mode 100644 index 0000000000..9b1b0a0047 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_closePlayer.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that closing with unpip leaves the video playing but the close button + * will pause the video. + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + let playVideo = () => { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).play(); + }); + }; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + let browser = tab.linkedBrowser; + await playVideo(); + + // Try the unpip button. + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused"); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let unpipButton = pipWin.document.getElementById("unpip"); + EventUtils.synthesizeMouseAtCenter(unpipButton, {}, pipWin); + await pipClosed; + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused"); + + // Try the close button. + pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused"); + + pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + ok(await isVideoPaused(browser, videoID), "The video is paused"); + + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_closeTab.js b/toolkit/components/pictureinpicture/tests/browser_closeTab.js new file mode 100644 index 0000000000..e8c9f59d82 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_closeTab.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if the tab that's hosting a <video> that's opened in a + * Picture-in-Picture window is closed, that the Picture-in-Picture + * window is also closed. + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + let browser = tab.linkedBrowser; + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + BrowserTestUtils.removeTab(tab); + await pipClosed; + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_close_unpip_focus.js b/toolkit/components/pictureinpicture/tests/browser_close_unpip_focus.js new file mode 100644 index 0000000000..f535add96a --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_close_unpip_focus.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that closing a pip window will not focus on the originating video's window. +add_task(async function test_close_button_focus() { + // initialize + let win1 = await BrowserTestUtils.openNewBrowserWindow(); + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + // Open PiP + let videoID = "with-controls"; + let pipTab = await BrowserTestUtils.openNewForegroundTab( + win1.gBrowser, + TEST_PAGE + ); + let browser = pipTab.linkedBrowser; + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let focus = BrowserTestUtils.waitForEvent(win2, "focus", true); + win2.focus(); + await focus; + + // Close PiP + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + let oldFocus = win1.focus; + win1.focus = () => { + ok(false, "Window is not supposed to be focused on"); + }; + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + ok(true, "Window did not get focus"); + + win1.focus = oldFocus; + // close windows + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); +}); + +// Tests that pressing the unpip button will focus on the originating video's window. +add_task(async function test_unpip_button_focus() { + // initialize + let win1 = await BrowserTestUtils.openNewBrowserWindow(); + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + // Open PiP + let videoID = "with-controls"; + let pipTab = await BrowserTestUtils.openNewForegroundTab( + win1.gBrowser, + TEST_PAGE + ); + let browser = pipTab.linkedBrowser; + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let focus = BrowserTestUtils.waitForEvent(win2, "focus", true); + win2.focus(); + await focus; + + // Close PiP + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("unpip"); + let pipWinFocusedPromise = BrowserTestUtils.waitForEvent(win1, "focus", true); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + + await pipClosed; + await pipWinFocusedPromise; + ok(true, "Originating window got focus"); + + // close windows + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_conflictingPips.js b/toolkit/components/pictureinpicture/tests/browser_conflictingPips.js new file mode 100644 index 0000000000..cf9e27e327 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_conflictingPips.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * If multiple PiPs try to open in the same place, they should not overlap. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let firstPip = await triggerPictureInPicture(browser, "with-controls"); + ok(firstPip, "Got first PiP window"); + + await ensureMessageAndClosePiP(browser, "with-controls", firstPip, false); + info("Closed first PiP to save location"); + + let secondPip = await triggerPictureInPicture(browser, "with-controls"); + ok(secondPip, "Got second PiP window"); + + let thirdPip = await triggerPictureInPicture(browser, "no-controls"); + ok(thirdPip, "Got third PiP window"); + + Assert.ok( + secondPip.screenX != thirdPip.screenX || + secondPip.screenY != thirdPip.screenY, + "Conflicting PiPs were successfully opened in different locations" + ); + + await ensureMessageAndClosePiP( + browser, + "with-controls", + secondPip, + false + ); + info("Second PiP was still open and is now closed"); + + await ensureMessageAndClosePiP(browser, "no-controls", thirdPip, false); + info("Third PiP was still open and is now closed"); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_contextMenu.js b/toolkit/components/pictureinpicture/tests/browser_contextMenu.js new file mode 100644 index 0000000000..ce6a85e91c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_contextMenu.js @@ -0,0 +1,238 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Opens up the content area context menu on a video loaded in a + * browser. + * + * @param {Element} browser The <xul:browser> hosting the <video> + * + * @param {String} videoID The ID of the video to open the context + * menu with. + * + * @returns Promise + * @resolves With the context menu DOM node once opened. + */ +async function openContextMenu(browser, videoID) { + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + videoID, + { type: "contextmenu", button: 2 }, + browser + ); + await popupShownPromise; + return contextMenu; +} + +/** + * Closes the content area context menu. + * + * @param {Element} contextMenu The content area context menu opened with + * openContextMenu. + * + * @returns Promise + * @resolves With undefined + */ +async function closeContextMenu(contextMenu) { + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; +} + +/** + * Tests that Picture-in-Picture can be opened and closed through the + * context menu + */ +add_task(async () => { + for (const videoId of ["with-controls", "no-controls"]) { + info(`Testing ${videoId} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let contextMenu = await openContextMenu(browser, videoId); + + info("Context menu is open."); + + const pipMenuItemId = "context-video-pictureinpicture"; + let menuItem = document.getElementById(pipMenuItemId); + + Assert.ok( + !menuItem.hidden, + "Should show Picture-in-Picture menu item." + ); + Assert.equal( + menuItem.getAttribute("checked"), + "false", + "Picture-in-Picture should be unchecked." + ); + + contextMenu.activateItem(menuItem); + + await SpecialPowers.spawn(browser, [videoId], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video has started being cloned."); + }); + + info("PiP player is now open."); + + contextMenu = await openContextMenu(browser, videoId); + + info("Context menu is open again."); + + contextMenu.activateItem(menuItem); + + await SpecialPowers.spawn(browser, [videoId], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return !video.isCloningElementVisually; + }, "Video has stopped being cloned."); + }); + } + ); + } +}); + +/** + * Tests that the Picture-in-Picture context menu is correctly updated + * based on the Picture-in-Picture state of the video. + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let menuItem = document.getElementById( + "context-video-pictureinpicture" + ); + let menu = await openContextMenu(browser, videoID); + Assert.ok( + !menuItem.hidden, + "Should show Picture-in-Picture menu item." + ); + Assert.equal( + menuItem.getAttribute("checked"), + "false", + "Picture-in-Picture should be unchecked." + ); + await closeContextMenu(menu); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video has started being cloned."); + }); + + menu = await openContextMenu(browser, videoID); + Assert.ok( + !menuItem.hidden, + "Should show Picture-in-Picture menu item." + ); + Assert.equal( + menuItem.getAttribute("checked"), + "true", + "Picture-in-Picture should be checked." + ); + await closeContextMenu(menu); + + let videoNotCloning = SpecialPowers.spawn( + browser, + [videoID], + async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return !video.isCloningElementVisually; + }, "Video has stopped being cloned."); + } + ); + pipWin.close(); + await videoNotCloning; + + menu = await openContextMenu(browser, videoID); + Assert.ok( + !menuItem.hidden, + "Should show Picture-in-Picture menu item." + ); + Assert.equal( + menuItem.getAttribute("checked"), + "false", + "Picture-in-Picture should be unchecked." + ); + await closeContextMenu(menu); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + // Construct a new video element, and capture a stream from it + // to redirect to the video that we're testing. + let newVideo = content.document.createElement("video"); + content.document.body.appendChild(newVideo); + + let testedVideo = content.document.getElementById(videoID); + newVideo.src = testedVideo.src; + + testedVideo.srcObject = newVideo.mozCaptureStream(); + await newVideo.play(); + await testedVideo.play(); + + await newVideo.pause(); + await testedVideo.pause(); + }); + menu = await openContextMenu(browser, videoID); + Assert.ok( + !menuItem.hidden, + "Should be showing Picture-in-Picture menu item." + ); + Assert.equal( + menuItem.getAttribute("checked"), + "false", + "Picture-in-Picture should be unchecked." + ); + await closeContextMenu(menu); + + pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video has started being cloned."); + }); + + menu = await openContextMenu(browser, videoID); + Assert.ok( + !menuItem.hidden, + "Should show Picture-in-Picture menu item." + ); + Assert.equal( + menuItem.getAttribute("checked"), + "true", + "Picture-in-Picture should be checked." + ); + await closeContextMenu(menu); + } + ); + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_controlsHover.js b/toolkit/components/pictureinpicture/tests/browser_controlsHover.js new file mode 100644 index 0000000000..7a3a33733f --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_controlsHover.js @@ -0,0 +1,191 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests functionality for the hover states of the various controls for the Picture-in-Picture + * video window. + */ +add_task(async () => { + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let waitForVideoEvent = eventType => { + return BrowserTestUtils.waitForContentEvent(browser, eventType, true); + }; + + await ensureVideosReady(browser); + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + }); + + // Open the video in PiP + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + const l10n = new Localization( + ["toolkit/pictureinpicture/pictureinpicture.ftl"], + true + ); + + let [ + close, + play, + unmute, + unpip, + subtitles, + pause, + mute, + fullscreenEnter, + fullscreenExit, + ] = l10n.formatMessagesSync([ + { + id: "pictureinpicture-close-btn", + args: { + shortcut: ShortcutUtils.prettifyShortcut( + pipWin.document.getElementById("closeShortcut") + ), + }, + }, + { id: "pictureinpicture-play-btn" }, + { + id: "pictureinpicture-unmute-btn", + args: { + shortcut: ShortcutUtils.prettifyShortcut( + pipWin.document.getElementById("unMuteShortcut") + ), + }, + }, + { id: "pictureinpicture-unpip-btn" }, + { id: "pictureinpicture-subtitles-btn" }, + { id: "pictureinpicture-pause-btn" }, + { + id: "pictureinpicture-mute-btn", + args: { + shortcut: ShortcutUtils.prettifyShortcut( + pipWin.document.getElementById("muteShortcut") + ), + }, + }, + { + id: "pictureinpicture-fullscreen-btn2", + args: { + shortcut: ShortcutUtils.prettifyShortcut( + pipWin.document.getElementById("fullscreenToggleShortcut") + ), + }, + }, + { + id: "pictureinpicture-exit-fullscreen-btn2", + args: { + shortcut: ShortcutUtils.prettifyShortcut( + pipWin.document.getElementById("fullscreenToggleShortcut") + ), + }, + }, + ]); + + let closeButton = pipWin.document.getElementById("close"); + let playPauseButton = pipWin.document.getElementById("playpause"); + let unpipButton = pipWin.document.getElementById("unpip"); + let muteUnmuteButton = pipWin.document.getElementById("audio"); + let subtitlesButton = pipWin.document.getElementById("closed-caption"); + let fullscreenButton = pipWin.document.getElementById("fullscreen"); + + // checks hover title for close button + await pipWin.document.l10n.translateFragment(closeButton); + Assert.equal( + close.attributes[1].value, + closeButton.getAttribute("tooltip"), + "The close button title matches Fluent string" + ); + + // checks hover title for play button + await pipWin.document.l10n.translateFragment(playPauseButton); + Assert.equal( + pause.attributes[1].value, + playPauseButton.getAttribute("tooltip"), + "The play button title matches Fluent string" + ); + + // checks hover title for unpip button + await pipWin.document.l10n.translateFragment(unpipButton); + Assert.equal( + unpip.attributes[1].value, + unpipButton.getAttribute("tooltip"), + "The unpip button title matches Fluent string" + ); + + // checks hover title for subtitles button + await pipWin.document.l10n.translateFragment(subtitlesButton); + Assert.equal( + subtitles.attributes[1].value, + subtitlesButton.getAttribute("tooltip"), + "The subtitles button title matches Fluent string" + ); + + // checks hover title for unmute button + await pipWin.document.l10n.translateFragment(muteUnmuteButton); + Assert.equal( + mute.attributes[1].value, + muteUnmuteButton.getAttribute("tooltip"), + "The Unmute button title matches Fluent string" + ); + + // Pause the video + let pausedPromise = waitForVideoEvent("pause"); + EventUtils.synthesizeMouseAtCenter(playPauseButton, {}, pipWin); + await pausedPromise; + ok(await isVideoPaused(browser, videoID), "The video is paused."); + + // checks hover title for pause button + await pipWin.document.l10n.translateFragment(playPauseButton); + Assert.equal( + play.attributes[1].value, + playPauseButton.getAttribute("tooltip"), + "The pause button title matches Fluent string" + ); + + // Mute the video + let mutedPromise = waitForVideoEvent("volumechange"); + EventUtils.synthesizeMouseAtCenter(muteUnmuteButton, {}, pipWin); + await mutedPromise; + ok(await isVideoMuted(browser, videoID), "The audio is muted."); + + // checks hover title for mute button + await pipWin.document.l10n.translateFragment(muteUnmuteButton); + Assert.equal( + unmute.attributes[1].value, + muteUnmuteButton.getAttribute("tooltip"), + "The mute button title matches Fluent string" + ); + + // checks hover title for enter fullscreen button + await pipWin.document.l10n.translateFragment(fullscreenButton); + Assert.equal( + fullscreenEnter.attributes[1].value, + fullscreenButton.getAttribute("tooltip"), + "The enter fullscreen button title matches Fluent string" + ); + + // enable fullscreen + await promiseFullscreenEntered(pipWin, async () => { + EventUtils.synthesizeMouseAtCenter(fullscreenButton, {}, pipWin); + }); + + // checks hover title for exit fullscreen button + await pipWin.document.l10n.translateFragment(fullscreenButton); + Assert.equal( + fullscreenExit.attributes[1].value, + fullscreenButton.getAttribute("tooltip"), + "The exit fullscreen button title matches Fluent string" + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_cornerSnapping.js b/toolkit/components/pictureinpicture/tests/browser_cornerSnapping.js new file mode 100644 index 0000000000..7ebfafc721 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_cornerSnapping.js @@ -0,0 +1,271 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +const FLOAT_OFFSET = 50; +const CHANGE_OFFSET = 30; +const DECREASE_OFFSET = FLOAT_OFFSET - CHANGE_OFFSET; +const INCREASE_OFFSET = FLOAT_OFFSET + CHANGE_OFFSET; +/** + * This function tests the PiP corner snapping feature. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, "no-controls"); + let controls = pipWin.document.getElementById("controls"); + + /** + * pipWin floating in top left corner(quadrant 2), dragged left + * should snap into top left corner + */ + pipWin.moveTo( + pipWin.screen.availLeft + FLOAT_OFFSET, + pipWin.screen.availTop + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent({ type: "mouseup" }, controls, pipWin); + pipWin.moveTo( + pipWin.screen.availLeft + DECREASE_OFFSET, + pipWin.screen.availTop + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent( + { + type: "mouseup", + metaKey: true, + }, + controls, + pipWin + ); + Assert.equal( + pipWin.screenX, + pipWin.screen.availLeft, + "Window should be on the left" + ); + Assert.equal( + pipWin.screenY, + pipWin.screen.availTop, + "Window should be on the top" + ); + + /** + * pipWin floating in top left corner(quadrant 2), dragged up + * should snap into top left corner + */ + pipWin.moveTo( + pipWin.screen.availLeft + FLOAT_OFFSET, + pipWin.screen.availTop + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent({ type: "mouseup" }, controls, pipWin); + pipWin.moveTo( + pipWin.screen.availLeft + FLOAT_OFFSET, + pipWin.screen.availTop + DECREASE_OFFSET + ); + EventUtils.sendMouseEvent( + { + type: "mouseup", + metaKey: true, + }, + controls, + pipWin + ); + Assert.equal( + pipWin.screenX, + pipWin.screen.availLeft, + "Window should be on the left" + ); + Assert.equal( + pipWin.screenY, + pipWin.screen.availTop, + "Window should be on the top" + ); + + /** + * pipWin floating in top left corner(quadrant 2), dragged right + * should snap into top right corner + */ + pipWin.moveTo( + pipWin.screen.availLeft + FLOAT_OFFSET, + pipWin.screen.availTop + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent({ type: "mouseup" }, controls, pipWin); + pipWin.moveTo( + pipWin.screen.availLeft + INCREASE_OFFSET, + pipWin.screen.availTop + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent( + { + type: "mouseup", + metaKey: true, + }, + controls, + pipWin + ); + Assert.equal( + pipWin.screenX, + pipWin.screen.availLeft + pipWin.screen.availWidth - pipWin.innerWidth, + "Window should be on the right" + ); + Assert.equal( + pipWin.screenY, + pipWin.screen.availTop, + "Window should be on the top" + ); + + /** + * pipWin floating in top left corner(quadrant 2), dragged down + * should snap into bottom left corner + */ + pipWin.moveTo( + pipWin.screen.availLeft + FLOAT_OFFSET, + pipWin.screen.availTop + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent({ type: "mouseup" }, controls, pipWin); + pipWin.moveTo( + pipWin.screen.availLeft + FLOAT_OFFSET, + pipWin.screen.availTop + INCREASE_OFFSET + ); + EventUtils.sendMouseEvent( + { + type: "mouseup", + metaKey: true, + }, + controls, + pipWin + ); + Assert.equal( + pipWin.screenX, + pipWin.screen.availLeft, + "Window should be on the left" + ); + Assert.equal( + pipWin.screenY, + pipWin.screen.availTop + pipWin.screen.availHeight - pipWin.innerHeight, + "Window should be on the bottom" + ); + + /** + * pipWin floating in top right corner(quadrant 1), dragged down + * should snap into bottom right corner + */ + pipWin.moveTo( + pipWin.screen.availLeft + + pipWin.screen.availWidth - + pipWin.innerWidth - + FLOAT_OFFSET, + pipWin.screen.availTop + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent({ type: "mouseup" }, controls, pipWin); + pipWin.moveTo( + pipWin.screen.availLeft + + pipWin.screen.availWidth - + pipWin.innerWidth - + FLOAT_OFFSET, + pipWin.screen.availTop + INCREASE_OFFSET + ); + EventUtils.sendMouseEvent( + { + type: "mouseup", + metaKey: true, + }, + controls, + pipWin + ); + Assert.equal( + pipWin.screenX, + pipWin.screen.availLeft + pipWin.screen.availWidth - pipWin.innerWidth, + "Window should be on the right" + ); + Assert.equal( + pipWin.screenY, + pipWin.screen.availTop + pipWin.screen.availHeight - pipWin.innerHeight, + "Window should be on the bottom" + ); + + /** + * pipWin floating in top left corner(quadrant 4), dragged left + * should snap into bottom left corner + */ + pipWin.moveTo( + pipWin.screen.availLeft + + pipWin.screen.availWidth - + pipWin.innerWidth - + FLOAT_OFFSET, + pipWin.screen.availTop + + pipWin.screen.availHeight - + pipWin.innerHeight - + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent({ type: "mouseup" }, controls, pipWin); + pipWin.moveTo( + pipWin.screen.availLeft + + pipWin.screen.availWidth - + pipWin.innerWidth - + INCREASE_OFFSET, + pipWin.screen.availTop + + pipWin.screen.availHeight - + pipWin.innerHeight - + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent( + { + type: "mouseup", + metaKey: true, + }, + controls, + pipWin + ); + Assert.equal( + pipWin.screenX, + pipWin.screen.availLeft, + "Window should be on the left" + ); + Assert.equal( + pipWin.screenY, + pipWin.screen.availTop + pipWin.screen.availHeight - pipWin.innerHeight, + "Window should be on the bottom" + ); + + /** + * pipWin floating in top left corner(quadrant 3), dragged up + * should snap into top left corner + */ + pipWin.moveTo( + pipWin.screen.availLeft + FLOAT_OFFSET, + pipWin.screen.availTop + + pipWin.screen.availHeight - + pipWin.innerHeight - + FLOAT_OFFSET + ); + EventUtils.sendMouseEvent({ type: "mouseup" }, controls, pipWin); + pipWin.moveTo( + pipWin.screen.availLeft + FLOAT_OFFSET, + pipWin.screen.availTop + + pipWin.screen.availHeight - + pipWin.innerHeight - + INCREASE_OFFSET + ); + EventUtils.sendMouseEvent( + { + type: "mouseup", + metaKey: true, + }, + controls, + pipWin + ); + Assert.equal( + pipWin.screenX, + pipWin.screen.availLeft, + "Window should be on the left" + ); + Assert.equal( + pipWin.screenY, + pipWin.screen.availTop, + "Window should be on the top" + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_dblclickFullscreen.js b/toolkit/components/pictureinpicture/tests/browser_dblclickFullscreen.js new file mode 100644 index 0000000000..c30d316fe3 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_dblclickFullscreen.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that double-clicking on the Picture-in-Picture player window + * causes it to fullscreen, and that pressing Escape allows us to exit + * fullscreen. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, "no-controls"); + let controls = pipWin.document.getElementById("controls"); + + await promiseFullscreenEntered(pipWin, async () => { + EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.equal( + pipWin.document.fullscreenElement, + pipWin.document.body, + "Double-click caused us to enter fullscreen." + ); + + await BrowserTestUtils.waitForCondition( + () => { + let close = pipWin.document.getElementById("close"); + let opacity = parseFloat(pipWin.getComputedStyle(close).opacity); + return opacity == 0.0; + }, + "Close button in player should have reached 0.0 opacity", + 100, + 100 + ); + + // First, we'll test exiting fullscreen by double-clicking again + // on the document body. + + await promiseFullscreenExited(pipWin, async () => { + EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.ok( + !pipWin.document.fullscreenElement, + "Double-click caused us to exit fullscreen." + ); + + // Now we double-click to re-enter fullscreen. + + await promiseFullscreenEntered(pipWin, async () => { + EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.equal( + pipWin.document.fullscreenElement, + pipWin.document.body, + "Double-click caused us to re-enter fullscreen." + ); + + // Finally, we check that hitting Escape lets the user leave + // fullscreen. + + await promiseFullscreenExited(pipWin, async () => { + EventUtils.synthesizeKey("KEY_Escape", {}, pipWin); + }); + + Assert.ok( + !pipWin.document.fullscreenElement, + "Pressing Escape caused us to exit fullscreen." + ); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + pipWin.close(); + await pipClosed; + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_disableSwipeGestures.js b/toolkit/components/pictureinpicture/tests/browser_disableSwipeGestures.js new file mode 100644 index 0000000000..b8eddf33f2 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_disableSwipeGestures.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, "with-controls"); + + let receivedSwipeGestureEvents = false; + pipWin.addEventListener("MozSwipeGestureMayStart", () => { + receivedSwipeGestureEvents = true; + }); + + const wheelEventPromise = BrowserTestUtils.waitForEvent(pipWin, "wheel"); + + // Try swiping left to right. + await panLeftToRightBegin(pipWin, 100, 100, 100); + await panLeftToRightEnd(pipWin, 100, 100, 100); + + // Wait a wheel event and a couple of frames to give a chance to receive + // the MozSwipeGestureMayStart event. + await wheelEventPromise; + await new Promise(resolve => requestAnimationFrame(resolve)); + await new Promise(resolve => requestAnimationFrame(resolve)); + + Assert.ok( + !receivedSwipeGestureEvents, + "No swipe gesture events observed" + ); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + pipWin.close(); + await pipClosed; + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_durationChange.js b/toolkit/components/pictureinpicture/tests/browser_durationChange.js new file mode 100644 index 0000000000..69397d8959 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_durationChange.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the visibility of the toggle will be + * recomputed after durationchange events fire. + */ +add_task(async function test_durationChange() { + // Most of the Picture-in-Picture tests run with the always-show + // preference set to true to avoid the toggle visibility heuristics. + // Since this test actually exercises those heuristics, we have + // to temporarily disable that pref. + // + // We also reduce the minimum video length for displaying the toggle + // to 5 seconds to avoid having to include or generate a 45 second long + // video (which is the default minimum length). + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.video-toggle.always-show", + false, + ], + ["media.videocontrols.picture-in-picture.video-toggle.min-video-secs", 5], + ], + }); + + // First, ensure that the toggle doesn't show up for these + // short videos by default. + await testToggle(TEST_PAGE, { + "with-controls": { canToggle: false }, + "no-controls": { canToggle: false }, + }); + + // Now cause the video to change sources, which should fire a + // durationchange event. The longer video should qualify us for + // displaying the toggle. + await testToggle( + TEST_PAGE, + { + "with-controls": { canToggle: true }, + "no-controls": { canToggle: true }, + }, + async browser => { + await SpecialPowers.spawn(browser, [], async () => { + for (let videoID of ["with-controls", "no-controls"]) { + let video = content.document.getElementById(videoID); + video.src = "gizmo.mp4"; + let durationChangePromise = ContentTaskUtils.waitForEvent( + video, + "durationchange" + ); + + video.load(); + await durationChangePromise; + } + }); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_flipIconWithRTL.js b/toolkit/components/pictureinpicture/tests/browser_flipIconWithRTL.js new file mode 100644 index 0000000000..cfb91440f5 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_flipIconWithRTL.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * The goal of this test is to check that the icon on the PiP button mirrors + * and the explainer text that shows up before the first time PiP is used + * right aligns when the browser is set to a RtL mode + * + * The browser will create a tab and open a video using PiP + * then the tests check that the components change their appearance accordingly + * + */ + +/** + * This test ensures that the default ltr is working as intended + */ +add_task(async function test_ltr_toggle() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await ensureVideosReady(browser); + for (let videoId of ["with-controls", "no-controls"]) { + let localeDir = await SpecialPowers.spawn(browser, [videoId], id => { + let video = content.document.getElementById(id); + let videocontrols = video.openOrClosedShadowRoot.firstChild; + return videocontrols.getAttribute("localedir"); + }); + + Assert.equal(localeDir, "ltr", "Got the right localedir"); + } + } + ); +}); + +/** + * This test ensures that the components flip correctly when rtl is set + */ +add_task(async function test_rtl_toggle() { + await SpecialPowers.pushPrefEnv({ + set: [["intl.l10n.pseudo", "bidi"]], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await ensureVideosReady(browser); + for (let videoId of ["with-controls", "no-controls"]) { + let localeDir = await SpecialPowers.spawn(browser, [videoId], id => { + let video = content.document.getElementById(id); + let videocontrols = video.openOrClosedShadowRoot.firstChild; + return videocontrols.getAttribute("localedir"); + }); + + Assert.equal(localeDir, "rtl", "Got the right localedir"); + } + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_fontSize_change.js b/toolkit/components/pictureinpicture/tests/browser_fontSize_change.js new file mode 100644 index 0000000000..df0fa4f403 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_fontSize_change.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const videoID = "with-controls"; +const TEXT_TRACK_FONT_SIZE = + "media.videocontrols.picture-in-picture.display-text-tracks.size"; +const ACCEPTABLE_DIFF = 1; + +const checkFontSize = (actual, expected, str) => { + let fs1 = actual.substring(0, actual.length - 2); + let fs2 = expected; + let diff = Math.abs(fs1 - fs2); + info(`Actual font size: ${fs1}. Expected font size: ${fs2}`); + Assert.lessOrEqual(diff, ACCEPTABLE_DIFF, str); +}; + +function getFontSize(pipBrowser) { + return SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + return content.window.getComputedStyle(textTracks).fontSize; + }); +} + +function promiseResize(win, width, height) { + if (win.outerWidth == width && win.outerHeight == height) { + return Promise.resolve(); + } + return new Promise(resolve => { + // More than one "resize" might be received if the window was recently + // created. + win.addEventListener("resize", () => { + if (win.outerWidth == width && win.outerHeight == height) { + resolve(); + } + }); + win.resizeTo(width, height); + }); +} + +/** + * Tests the font size is correct for PiP windows + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE_WITH_WEBVTT, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // move PiP window to 0, 0 so resizing the window doesn't go offscreen + pipWin.moveTo(0, 0); + + let width = pipWin.innerWidth; + let height = pipWin.innerHeight; + + await promiseResize(pipWin, Math.round(250 * (width / height)), 250); + + width = pipWin.innerWidth; + height = pipWin.innerHeight; + + let pipBrowser = pipWin.document.getElementById("browser"); + + let fontSize = await getFontSize(pipBrowser); + checkFontSize( + fontSize, + Math.round(height * 0.06 * 10) / 10, + "The medium font size is .06 of the PiP window height" + ); + + await ensureMessageAndClosePiP(browser, videoID, pipWin, false); + + // change font size to small + await SpecialPowers.pushPrefEnv({ + set: [[TEXT_TRACK_FONT_SIZE, "small"]], + }); + + pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + pipBrowser = pipWin.document.getElementById("browser"); + + fontSize = await getFontSize(pipBrowser); + checkFontSize(fontSize, 14, "The small font size is the minimum 14px"); + + await ensureMessageAndClosePiP(browser, videoID, pipWin, false); + + // change font size to large + await SpecialPowers.pushPrefEnv({ + set: [[TEXT_TRACK_FONT_SIZE, "large"]], + }); + + pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + pipBrowser = pipWin.document.getElementById("browser"); + + fontSize = await getFontSize(pipBrowser); + checkFontSize( + fontSize, + Math.round(height * 0.09 * 10) / 10, + "The large font size is .09 of the PiP window height" + ); + + // resize PiP window to a larger size + width = pipWin.innerWidth * 2; + height = pipWin.innerHeight * 2; + await promiseResize(pipWin, width, height); + + fontSize = await getFontSize(pipBrowser); + checkFontSize(fontSize, 40, "The large font size is the max of 40px"); + + await ensureMessageAndClosePiP(browser, videoID, pipWin, false); + + // change font size to small + await SpecialPowers.pushPrefEnv({ + set: [[TEXT_TRACK_FONT_SIZE, "small"]], + }); + + pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + pipBrowser = pipWin.document.getElementById("browser"); + + fontSize = await getFontSize(pipBrowser); + checkFontSize( + fontSize, + Math.round(height * 0.03 * 10) / 10, + "The small font size is .03 of the PiP window height" + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_fullscreen.js b/toolkit/components/pictureinpicture/tests/browser_fullscreen.js new file mode 100644 index 0000000000..439901c0f3 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_fullscreen.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const VIDEOS = ["with-controls", "no-controls"]; + +/** + * Tests that the Picture-in-Picture toggle is hidden when + * a video with or without controls is made fullscreen. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + for (let videoID of VIDEOS) { + info(`Start test of video fullscreen for video ${videoID}.`); + await promiseFullscreenEntered(window, () => { + return SpecialPowers.spawn(browser, [videoID], videoID => { + let video = this.content.document.getElementById(videoID); + return video.requestFullscreen(); + }); + }); + + info(`Entered video fullscreen, about to mouseover the video.`); + + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + info(`Mouseover complete.`); + + let args = { videoID, toggleID: DEFAULT_TOGGLE_STYLES.rootID }; + + await promiseFullscreenExited(window, () => { + return SpecialPowers.spawn(browser, [args], args => { + let { videoID, toggleID } = args; + let video = this.content.document.getElementById(videoID); + let toggle = video.openOrClosedShadowRoot.getElementById(toggleID); + ok( + ContentTaskUtils.isHidden(toggle), + `Toggle should be hidden in video fullscreen mode for video ${videoID}.` + ); + return this.content.document.exitFullscreen(); + }); + }); + + info(`Exited video fullscreen.`); + } + } + ); +}); + +/** + * Tests that the Picture-in-Picture toggle is hidden if an + * ancestor of a video (in this case, the document body) is made + * to be the fullscreen element. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + info(`Start test of browser fullscreen.`); + await promiseFullscreenEntered(window, () => { + return SpecialPowers.spawn(browser, [], () => { + return this.content.document.body.requestFullscreen(); + }); + }); + + info(`Entered browser fullscreen.`); + + for (let videoID of VIDEOS) { + info(`About to mouseover for video ${videoID}.`); + + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + info(`Mouseover complete.`); + + let args = { videoID, toggleID: DEFAULT_TOGGLE_STYLES.rootID }; + + await SpecialPowers.spawn(browser, [args], async args => { + let { videoID, toggleID } = args; + let video = this.content.document.getElementById(videoID); + let toggle = video.openOrClosedShadowRoot.getElementById(toggleID); + ok( + ContentTaskUtils.isHidden(toggle), + `Toggle should be hidden in body fullscreen mode for video ${videoID}.` + ); + }); + } + + await promiseFullscreenExited(window, () => { + return SpecialPowers.spawn(browser, [], () => { + return this.content.document.exitFullscreen(); + }); + }); + + info(`Exited browser fullscreen.`); + } + ); +}); + +/** + * Tests that the Picture-In-Picture window is closed when something + * is fullscreened + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await ensureVideosReady(browser); + + for (let videoID of VIDEOS) { + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, `Got Picture-In-Picture window for video ${videoID}.`); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + + // need to focus first, since fullscreen request will be blocked otherwise + await SimpleTest.promiseFocus(window); + + await promiseFullscreenEntered(window, () => { + return SpecialPowers.spawn(browser, [], () => { + return this.content.document.body.requestFullscreen(); + }); + }); + + await pipClosed; + ok( + pipWin.closed, + `Picture-In-Picture successfully closed for video ${videoID}.` + ); + + await promiseFullscreenExited(window, () => { + return SpecialPowers.spawn(browser, [], () => { + return this.content.document.exitFullscreen(); + }); + }); + } + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_improved_controls.js b/toolkit/components/pictureinpicture/tests/browser_improved_controls.js new file mode 100644 index 0000000000..deadc8d53b --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_improved_controls.js @@ -0,0 +1,303 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const TEST_PAGE_LONG = TEST_ROOT + "test-video-selection.html"; + +const IMPROVED_CONTROLS_ENABLED_PREF = + "media.videocontrols.picture-in-picture.improved-video-controls.enabled"; + +async function getVideoCurrentTime(browser, videoID) { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).currentTime; + }); +} + +async function getVideoDuration(browser, videoID) { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).duration; + }); +} + +async function timestampUpdated(timestampEl, expectedTimestamp) { + await BrowserTestUtils.waitForMutationCondition( + timestampEl, + { childList: true }, + () => { + return expectedTimestamp === timestampEl.textContent; + } + ); +} + +function checkTimeCloseEnough(actual, expected, message) { + let equal = Math.abs(actual - expected); + if (equal <= 0.5) { + is(equal <= 0.5, true, message); + } else { + is(actual, expected, message); + } +} + +/** + * Tests the functionality of improved Picture-in-picture + * playback controls. + */ +add_task(async () => { + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let waitForVideoEvent = eventType => { + return BrowserTestUtils.waitForContentEvent(browser, eventType, true); + }; + + await ensureVideosReady(browser); + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [[IMPROVED_CONTROLS_ENABLED_PREF, true]], + }); + + // Open the video in PiP + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let fullscreenButton = pipWin.document.getElementById("fullscreen"); + let seekForwardButton = pipWin.document.getElementById("seekForward"); + let seekBackwardButton = pipWin.document.getElementById("seekBackward"); + + // Try seek forward button + let seekedForwardPromise = waitForVideoEvent("seeked"); + EventUtils.synthesizeMouseAtCenter(seekForwardButton, {}, pipWin); + ok(await seekedForwardPromise, "The Forward button triggers"); + + // Try seek backward button + let seekedBackwardPromise = waitForVideoEvent("seeked"); + EventUtils.synthesizeMouseAtCenter(seekBackwardButton, {}, pipWin); + ok(await seekedBackwardPromise, "The Backward button triggers"); + + // The Fullsreen button appears when the pref is enabled and the fullscreen hidden property is set to false + Assert.ok(!fullscreenButton.hidden, "The Fullscreen button is visible"); + + // The seek Forward button appears when the pref is enabled and the seek forward button hidden property is set to false + Assert.ok(!seekForwardButton.hidden, "The Forward button is visible"); + + // The seek Backward button appears when the pref is enabled and the seek backward button hidden property is set to false + Assert.ok(!seekBackwardButton.hidden, "The Backward button is visible"); + + // CLose the PIP window + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + + await SpecialPowers.pushPrefEnv({ + set: [[IMPROVED_CONTROLS_ENABLED_PREF, false]], + }); + + // Open the video in PiP + pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + fullscreenButton = pipWin.document.getElementById("fullscreen"); + seekForwardButton = pipWin.document.getElementById("seekForward"); + seekBackwardButton = pipWin.document.getElementById("seekBackward"); + + // The Fullsreen button disappears when the pref is disabled and the fullscreen hidden property is set to true + Assert.ok( + fullscreenButton.hidden, + "The Fullscreen button is not visible" + ); + + // The seek Forward button disappears when the pref is disabled and the seek forward button hidden property is set to true + Assert.ok(seekForwardButton.hidden, "The Forward button is not visible"); + + // The seek Backward button disappears when the pref is disabled and the seek backward button hidden property is set to true + Assert.ok( + seekBackwardButton.hidden, + "The Backward button is not visible" + ); + } + ); +}); + +/** + * Tests the functionality of Picture-in-picture + * video scrubber + */ +add_task(async function testVideoScrubber() { + let videoID = "long"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_LONG, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + await SpecialPowers.pushPrefEnv({ + set: [[IMPROVED_CONTROLS_ENABLED_PREF, true]], + }); + + // Open the video in PiP + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let scrubber = pipWin.document.getElementById("scrubber"); + scrubber.focus(); + + let currentTime = await getVideoCurrentTime(browser, videoID); + let expectedVideoTime = 0; + const duration = await getVideoDuration(browser, videoID); + checkTimeCloseEnough( + currentTime, + expectedVideoTime, + "Video current time is 0" + ); + + let timestampEl = pipWin.document.getElementById("timestamp"); + let expectedTimestamp = "0:00 / 0:08"; + + // Wait for the timestamp to update + await timestampUpdated(timestampEl, expectedTimestamp); + let actualTimestamp = timestampEl.textContent; + is(actualTimestamp, expectedTimestamp, "Timestamp reads 0:00 / 0:08"); + + EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin); + + currentTime = await getVideoCurrentTime(browser, videoID); + expectedVideoTime = 5; + checkTimeCloseEnough( + currentTime, + expectedVideoTime, + "Video current time is 5" + ); + + expectedTimestamp = "0:05 / 0:08"; + await timestampUpdated(timestampEl, expectedTimestamp); + actualTimestamp = timestampEl.textContent; + is(actualTimestamp, expectedTimestamp, "Timestamp reads 0:05 / 0:08"); + + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, pipWin); + + currentTime = await getVideoCurrentTime(browser, videoID); + expectedVideoTime = 0; + checkTimeCloseEnough( + currentTime, + expectedVideoTime, + "Video current time is 0" + ); + + expectedTimestamp = "0:00 / 0:08"; + await timestampUpdated(timestampEl, expectedTimestamp); + actualTimestamp = timestampEl.textContent; + is(actualTimestamp, expectedTimestamp, "Timestamp reads 0:00 / 0:08"); + + let rect = scrubber.getBoundingClientRect(); + + EventUtils.synthesizeMouse( + scrubber, + rect.width / 2, + rect.height / 2, + {}, + pipWin + ); + + expectedVideoTime = duration / 2; + currentTime = await getVideoCurrentTime(browser, videoID); + checkTimeCloseEnough( + currentTime, + expectedVideoTime, + "Video current time is 3.98..." + ); + + expectedTimestamp = "0:04 / 0:08"; + await timestampUpdated(timestampEl, expectedTimestamp); + actualTimestamp = timestampEl.textContent; + is(actualTimestamp, expectedTimestamp, "Timestamp reads 0:04 / 0:08"); + + EventUtils.synthesizeMouse( + scrubber, + rect.width / 2, + rect.height / 2, + { type: "mousedown" }, + pipWin + ); + + EventUtils.synthesizeMouse( + scrubber, + rect.width, + rect.height / 2, + { type: "mousemove" }, + pipWin + ); + + EventUtils.synthesizeMouse( + scrubber, + rect.width, + rect.height / 2, + { type: "mouseup" }, + pipWin + ); + + expectedVideoTime = duration; + currentTime = await getVideoCurrentTime(browser, videoID); + checkTimeCloseEnough( + currentTime, + expectedVideoTime, + "Video current time is 7.96..." + ); + + await ensureMessageAndClosePiP(browser, videoID, pipWin, false); + } + ); +}); + +/** + * Tests the behavior of the scrubber and position/duration indicator for a + * video with an invalid/non-finite duration. + */ +add_task(async function testInvalidDuration() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_NAN_VIDEO_DURATION, + gBrowser, + }, + async browser => { + const videoID = "nan-duration"; + + // This tests skips calling ensureVideosReady, because canplaythrough + // will never fire for the NaN duration video. + + await SpecialPowers.pushPrefEnv({ + set: [[IMPROVED_CONTROLS_ENABLED_PREF, true]], + }); + + // Open the video in PiP + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // Both the scrubber and the duration should be hidden. + let timestampEl = pipWin.document.getElementById("timestamp"); + ok(timestampEl.hidden, "Timestamp in the PIP window should be hidden."); + + let scrubberEl = pipWin.document.getElementById("scrubber"); + ok( + scrubberEl.hidden, + "Scrubber control in the PIP window should be hidden" + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_keyboardClosePIPwithESC.js b/toolkit/components/pictureinpicture/tests/browser_keyboardClosePIPwithESC.js new file mode 100644 index 0000000000..14dd3d1b7a --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_keyboardClosePIPwithESC.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * tests that the ESC key would stop the player and close the PiP player floating window + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + let playVideo = () => { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).play(); + }); + }; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + let browser = tab.linkedBrowser; + + await playVideo(); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "The Picture-in-Picture window is not there."); + ok( + !(await isVideoPaused(browser, videoID)), + "The video is paused, but should not." + ); + ok( + !pipWin.document.fullscreenElement, + "PiP should not yet be in fullscreen." + ); + + let controls = pipWin.document.getElementById("controls"); + await promiseFullscreenEntered(pipWin, async () => { + EventUtils.sendMouseEvent({ type: "dblclick" }, controls, pipWin); + }); + + Assert.equal( + pipWin.document.fullscreenElement, + pipWin.document.body, + "Double-click should have caused to enter fullscreen." + ); + + await promiseFullscreenExited(pipWin, async () => { + EventUtils.synthesizeKey("KEY_Escape", {}, pipWin); + }); + + ok( + !pipWin.document.fullscreenElement, + "ESC should have caused to leave fullscreen." + ); + ok( + !(await isVideoPaused(browser, videoID)), + "The video is paused, but should not." + ); + + // Try to close the PiP window via the ESC button, since now it is not in fullscreen anymore. + EventUtils.synthesizeKey("KEY_Escape", {}, pipWin); + + // then the PiP should have been closed + ok(pipWin.closed, "Picture-in-Picture window is not closed, but should."); + // and the video should not be playing anymore + ok( + await isVideoPaused(browser, videoID), + "The video is not paused, but should." + ); + + await BrowserTestUtils.removeTab(tab); + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_keyboardFullScreenPIPShortcut.js b/toolkit/components/pictureinpicture/tests/browser_keyboardFullScreenPIPShortcut.js new file mode 100644 index 0000000000..da4191cb0e --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_keyboardFullScreenPIPShortcut.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the F-key would enter and exit full screen mode in PiP for the default locale (en-US). + */ +add_task(async () => { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + let browser = tab.linkedBrowser; + let videoID = "with-controls"; + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "The Picture-in-Picture window is there."); + + ok( + !pipWin.document.fullscreenElement, + "PiP should not yet be in fullscreen." + ); + + await promiseFullscreenEntered(pipWin, async () => { + EventUtils.synthesizeKey("f", {}, pipWin); + }); + + Assert.equal( + pipWin.document.fullscreenElement, + pipWin.document.body, + "F-key should have caused to enter fullscreen." + ); + + await promiseFullscreenExited(pipWin, async () => { + EventUtils.synthesizeKey("f", {}, pipWin); + }); + + ok( + !pipWin.document.fullscreenElement, + "F-key should have caused to leave fullscreen." + ); + + await BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_keyboardShortcut.js b/toolkit/components/pictureinpicture/tests/browser_keyboardShortcut.js new file mode 100644 index 0000000000..a81d99e18c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_keyboardShortcut.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const PIP_SHORTCUT_OPEN_EVENTS = [ + { + category: "pictureinpicture", + method: "opened_method", + object: "shortcut", + }, +]; + +const PIP_SHORTCUT_CLOSE_EVENTS = [ + { + category: "pictureinpicture", + method: "closed_method", + object: "shortcut", + }, +]; + +/** + * Tests that if the user keys in the keyboard shortcut for + * Picture-in-Picture, then the first video on the currently + * focused page will be opened in the player window. + */ +add_task(async function test_pip_keyboard_shortcut() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + Services.telemetry.clearEvents(); + await ensureVideosReady(browser); + + // In test-page.html, the "with-controls" video is the first one that + // appears in the DOM, so this is what we expect to open via the keyboard + // shortcut. + const VIDEO_ID = "with-controls"; + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + let videoReady = SpecialPowers.spawn( + browser, + [VIDEO_ID], + async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + } + ); + + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("]", { + accelKey: true, + shiftKey: true, + altKey: true, + }); + } else { + EventUtils.synthesizeKey("]", { accelKey: true, shiftKey: true }); + } + + let pipWin = await domWindowOpened; + await videoReady; + + ok(pipWin, "Got Picture-in-Picture window."); + + await ensureMessageAndClosePiP(browser, VIDEO_ID, pipWin, false); + + let openFilter = { + category: "pictureinpicture", + method: "opened_method", + object: "shortcut", + }; + await waitForTelemeryEvents( + openFilter, + PIP_SHORTCUT_OPEN_EVENTS.length, + "content" + ); + TelemetryTestUtils.assertEvents(PIP_SHORTCUT_OPEN_EVENTS, openFilter, { + clear: true, + process: "content", + }); + + // Reopen PiP Window + pipWin = await triggerPictureInPicture(browser, VIDEO_ID); + await videoReady; + + ok(pipWin, "Got Picture-in-Picture window."); + + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey( + "]", + { + accelKey: true, + shiftKey: true, + altKey: true, + }, + pipWin + ); + } else { + EventUtils.synthesizeKey( + "]", + { accelKey: true, shiftKey: true }, + pipWin + ); + } + + await BrowserTestUtils.windowClosed(pipWin); + + ok(pipWin.closed, "Picture-in-Picture window closed."); + + let closeFilter = { + category: "pictureinpicture", + method: "closed_method", + object: "shortcut", + }; + await waitForTelemeryEvents( + closeFilter, + PIP_SHORTCUT_CLOSE_EVENTS.length, + "parent" + ); + TelemetryTestUtils.assertEvents(PIP_SHORTCUT_CLOSE_EVENTS, closeFilter, { + clear: true, + process: "parent", + }); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_keyboardShortcutClosePIP.js b/toolkit/components/pictureinpicture/tests/browser_keyboardShortcutClosePIP.js new file mode 100644 index 0000000000..a85cdd5a63 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_keyboardShortcutClosePIP.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that keyboard shortcut ctr + w / cmd + w closing PIP window + */ + +add_task(async function test_pip_close_keyboard_shortcut() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + const VIDEO_ID = "with-controls"; + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + let videoReady = SpecialPowers.spawn( + browser, + [VIDEO_ID], + async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + } + ); + + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("]", { + accelKey: true, + shiftKey: true, + altKey: true, + }); + } else { + EventUtils.synthesizeKey("]", { accelKey: true, shiftKey: true }); + } + + let pipWin = await domWindowOpened; + await videoReady; + + ok(pipWin, "Got Picture-in-Picture window."); + + EventUtils.synthesizeKey("w", { accelKey: true }, pipWin); + await BrowserTestUtils.windowClosed(pipWin); + ok(await isVideoPaused(browser, VIDEO_ID), "The video is paused"); + ok(pipWin.closed, "Closed PIP"); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_keyboardShortcutWithNanDuration.js b/toolkit/components/pictureinpicture/tests/browser_keyboardShortcutWithNanDuration.js new file mode 100644 index 0000000000..c812dc3e4e --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_keyboardShortcutWithNanDuration.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_pip_keyboard_shortcut_with_nan_video_duration() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_NAN_VIDEO_DURATION, + gBrowser, + }, + async browser => { + const VIDEO_ID = "test-video"; + + await SpecialPowers.spawn(browser, [VIDEO_ID], async videoID => { + let video = content.document.getElementById(videoID); + if (video.readyState < content.HTMLMediaElement.HAVE_ENOUGH_DATA) { + info(`Waiting for 'canplaythrough' for ${videoID}`); + await ContentTaskUtils.waitForEvent(video, "canplaythrough"); + } + }); + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + + let videoReady = SpecialPowers.spawn( + browser, + [VIDEO_ID], + async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + } + ); + + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("]", { + accelKey: true, + shiftKey: true, + altKey: true, + }); + } else { + EventUtils.synthesizeKey("]", { accelKey: true, shiftKey: true }); + } + + let pipWin = await domWindowOpened; + await videoReady; + + ok(pipWin, "Got Picture-in-Picture window."); + + pipWin.close(); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_keyboardToggle.js b/toolkit/components/pictureinpicture/tests/browser_keyboardToggle.js new file mode 100644 index 0000000000..a70100de85 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_keyboardToggle.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests to ensure that tabbing to the pip button and pressing space works + * to open the picture-in-picture window. + */ +add_task(async () => { + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + // Open the video in PiP + let pipWin = await triggerPictureInPicture(browser, videoID, () => { + EventUtils.synthesizeKey("KEY_Tab", {}); // play button + EventUtils.synthesizeKey("KEY_Tab", {}); // pip button + EventUtils.synthesizeKey(" ", {}); + }); + ok(pipWin, "Got Picture-in-Picture window."); + + await BrowserTestUtils.closeWindow(pipWin); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_mediaStreamVideos.js b/toolkit/components/pictureinpicture/tests/browser_mediaStreamVideos.js new file mode 100644 index 0000000000..46b36a3a6e --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_mediaStreamVideos.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks that the media stream video format has functional + * support for PiP + */ +add_task(async function test_mediaStreamVideos() { + await testToggle( + TEST_ROOT + "test-media-stream.html", + { + "with-controls": { canToggle: true }, + "no-controls": { canToggle: true }, + }, + async browser => { + await SpecialPowers.spawn(browser, [], async () => { + // Construct a new video element, and capture a stream from it + // to redirect to both testing videos. Create the captureStreams after + // we have metadata so tracks are immediately available, but wait with + // playback until the setup is done. + + function logEvent(element, ev) { + element.addEventListener(ev, () => + info( + `${element.id} got event ${ev}. currentTime=${element.currentTime}` + ) + ); + } + + const newVideo = content.document.createElement("video"); + newVideo.id = "new-video"; + newVideo.src = "test-video.mp4"; + newVideo.preload = "auto"; + logEvent(newVideo, "timeupdate"); + logEvent(newVideo, "ended"); + content.document.body.appendChild(newVideo); + await ContentTaskUtils.waitForEvent(newVideo, "loadedmetadata"); + + const mediastreamPlayingPromises = []; + for (let videoID of ["with-controls", "no-controls"]) { + const testedVideo = content.document.createElement("video"); + testedVideo.id = videoID; + testedVideo.srcObject = newVideo.mozCaptureStream(); + testedVideo.play(); + mediastreamPlayingPromises.push( + new Promise(r => (testedVideo.onplaying = r)) + ); + content.document.body.prepend(testedVideo); + } + + await newVideo.play(); + await Promise.all(mediastreamPlayingPromises); + newVideo.pause(); + }); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_mouseButtonVariation.js b/toolkit/components/pictureinpicture/tests/browser_mouseButtonVariation.js new file mode 100644 index 0000000000..5744c83496 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_mouseButtonVariation.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if the user mousedown's on a Picture-in-Picture toggle, + * but then mouseup's on something completely different, that we still + * open a Picture-in-Picture window, and that the mouse button events are + * all suppressed. Also ensures that a subsequent click on the document + * body results in all mouse button events firing normally. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + let videoID = "no-controls"; + + await prepareForToggleClick(browser, videoID); + + // Hover the mouse over the video to reveal the toggle, which is necessary + // if we want to click on the toggle. + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mousemove", + }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + info("Waiting for toggle to become visible"); + await toggleOpacityReachesThreshold(browser, videoID, "hoverVideo"); + + let toggleClientRect = await getToggleClientRect(browser, videoID); + + // The toggle center, because of how it slides out, is actually outside + // of the bounds of a click event. For now, we move the mouse in by a + // hard-coded 15 pixels along the x and y axis to achieve the hover. + let toggleLeft = toggleClientRect.left + 15; + let toggleTop = toggleClientRect.top + 15; + + info( + "Clicking on toggle, and expecting a Picture-in-Picture window to open" + ); + // We need to wait for the window to have completed loading before we + // can close it as the document's type required by closeWindow may not + // be available. + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleLeft, + toggleTop, + { + type: "mousedown", + }, + browser + ); + + await BrowserTestUtils.synthesizeMouseAtPoint( + 1, + 1, + { + type: "mouseup", + }, + browser + ); + + let win = await domWindowOpened; + ok(win, "A Picture-in-Picture window opened."); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + }); + + await BrowserTestUtils.closeWindow(win); + await assertSawClickEventOnly(browser); + + await BrowserTestUtils.synthesizeMouseAtPoint(1, 1, {}, browser); + await assertSawMouseEvents(browser, true); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_multiPip.js b/toolkit/components/pictureinpicture/tests/browser_multiPip.js new file mode 100644 index 0000000000..2821c0d484 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_multiPip.js @@ -0,0 +1,225 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function createTab() { + return BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PAGE, + waitForLoad: true, + }); +} + +function getTelemetryMaxPipCount(resetMax = false) { + const scalarData = Services.telemetry.getSnapshotForScalars( + "main", + resetMax + ).parent; + return scalarData["pictureinpicture.most_concurrent_players"]; +} + +/** + * Tests that multiple PiPs can be opened and closed in a single tab + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let firstPip = await triggerPictureInPicture(browser, "with-controls"); + ok(firstPip, "Got first PiP window"); + + let secondPip = await triggerPictureInPicture(browser, "no-controls"); + ok(secondPip, "Got second PiP window"); + + await ensureMessageAndClosePiP(browser, "with-controls", firstPip, false); + info("First PiP was still open and is now closed"); + + await ensureMessageAndClosePiP(browser, "no-controls", secondPip, false); + info("Second PiP was still open and is now closed"); + } + ); +}); + +/** + * Tests that multiple PiPs can be opened and closed across different tabs + */ +add_task(async () => { + let firstTab = await createTab(); + let secondTab = await createTab(); + + gBrowser.selectedTab = firstTab; + + let firstPip = await triggerPictureInPicture( + firstTab.linkedBrowser, + "with-controls" + ); + ok(firstPip, "Got first PiP window"); + + gBrowser.selectedTab = secondTab; + + let secondPip = await triggerPictureInPicture( + secondTab.linkedBrowser, + "with-controls" + ); + ok(secondPip, "Got second PiP window"); + + await ensureMessageAndClosePiP( + firstTab.linkedBrowser, + "with-controls", + firstPip, + false + ); + info("First Picture-in-Picture window was open and is now closed."); + + await ensureMessageAndClosePiP( + secondTab.linkedBrowser, + "with-controls", + secondPip, + false + ); + info("Second Picture-in-Picture window was open and is now closed."); + + BrowserTestUtils.removeTab(firstTab); + BrowserTestUtils.removeTab(secondTab); +}); + +/** + * Tests that when a tab is closed; that only PiPs originating from this tab + * are closed as well + */ +add_task(async () => { + let firstTab = await createTab(); + let secondTab = await createTab(); + + let firstPip = await triggerPictureInPicture( + firstTab.linkedBrowser, + "with-controls" + ); + ok(firstPip, "Got first PiP window"); + + let secondPip = await triggerPictureInPicture( + secondTab.linkedBrowser, + "with-controls" + ); + ok(secondPip, "Got second PiP window"); + + let firstClosed = BrowserTestUtils.domWindowClosed(firstPip); + BrowserTestUtils.removeTab(firstTab); + await firstClosed; + info("First PiP closed after closing the first tab"); + + await assertVideoIsBeingCloned(secondTab.linkedBrowser, "#with-controls"); + info("Second PiP is still open after first tab close"); + + let secondClosed = BrowserTestUtils.domWindowClosed(secondPip); + BrowserTestUtils.removeTab(secondTab); + await secondClosed; + info("Second PiP closed after closing the second tab"); +}); + +/** + * Check that correct number of pip players are recorded for Telemetry + * tracking + */ +add_task(async () => { + // run this to flush recorded values from previous tests + getTelemetryMaxPipCount(true); + + let firstTab = await createTab(); + let secondTab = await createTab(); + + gBrowser.selectedTab = firstTab; + + let firstPip = await triggerPictureInPicture( + firstTab.linkedBrowser, + "with-controls" + ); + ok(firstPip, "Got first PiP window"); + + Assert.equal( + getTelemetryMaxPipCount(true), + 1, + "There should only be 1 PiP window" + ); + + let secondPip = await triggerPictureInPicture( + firstTab.linkedBrowser, + "no-controls" + ); + ok(secondPip, "Got second PiP window"); + + Assert.equal( + getTelemetryMaxPipCount(true), + 2, + "There should be 2 PiP windows" + ); + + await ensureMessageAndClosePiP( + firstTab.linkedBrowser, + "no-controls", + secondPip, + false + ); + info("Second PiP was open and is now closed"); + + gBrowser.selectedTab = secondTab; + + let thirdPip = await triggerPictureInPicture( + secondTab.linkedBrowser, + "with-controls" + ); + ok(thirdPip, "Got third PiP window"); + + let fourthPip = await triggerPictureInPicture( + secondTab.linkedBrowser, + "no-controls" + ); + ok(fourthPip, "Got fourth PiP window"); + + Assert.equal( + getTelemetryMaxPipCount(false), + 3, + "There should now be 3 PiP windows" + ); + + gBrowser.selectedTab = firstTab; + + await ensureMessageAndClosePiP( + firstTab.linkedBrowser, + "with-controls", + firstPip, + false + ); + info("First PiP was open, it is now closed."); + + gBrowser.selectedTab = secondTab; + + await ensureMessageAndClosePiP( + secondTab.linkedBrowser, + "with-controls", + thirdPip, + false + ); + info("Third PiP was open, it is now closed."); + + await ensureMessageAndClosePiP( + secondTab.linkedBrowser, + "no-controls", + fourthPip, + false + ); + info("Fourth PiP was open, it is now closed."); + + Assert.equal( + getTelemetryMaxPipCount(false), + 3, + "Max PiP count should still be 3" + ); + + BrowserTestUtils.removeTab(firstTab); + BrowserTestUtils.removeTab(secondTab); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_nimbusDisplayDuration.js b/toolkit/components/pictureinpicture/tests/browser_nimbusDisplayDuration.js new file mode 100644 index 0000000000..a2e7e66b40 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_nimbusDisplayDuration.js @@ -0,0 +1,214 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +const TOGGLE_HAS_USED_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; +const TOGGLE_FIRST_SEEN_PREF = + "media.videocontrols.picture-in-picture.video-toggle.first-seen-secs"; + +/** + * This tests that the first-time toggle doesn't change to the icon toggle. + */ +add_task(async function test_experiment_control_displayDuration() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + await SpecialPowers.pushPrefEnv({ + set: [ + [TOGGLE_FIRST_SEEN_PREF, 0], + [TOGGLE_HAS_USED_PREF, false], + ], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + const hasUsed = Services.prefs.getBoolPref(TOGGLE_HAS_USED_PREF); + const firstSeen = Services.prefs.getIntPref(TOGGLE_FIRST_SEEN_PREF); + + Assert.ok(!hasUsed, "has-used is false and toggle is not icon"); + Assert.notEqual(firstSeen, 0, "First seen should not be 0"); + } + ); +}); + +/** + * This tests that the first-time toggle changes to the icon toggle + * if the displayDuration end date is reached or passed. + */ +add_task(async function test_experiment_displayDuration_end_date_was_reached() { + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "pictureinpicture", + value: { + displayDuration: 1, + }, + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + await SpecialPowers.pushPrefEnv({ + set: [ + [TOGGLE_FIRST_SEEN_PREF, 222], + [TOGGLE_HAS_USED_PREF, false], + ], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + const hasUsed = Services.prefs.getBoolPref(TOGGLE_HAS_USED_PREF); + const firstSeen = Services.prefs.getIntPref(TOGGLE_FIRST_SEEN_PREF); + + Assert.ok(hasUsed, "has-used is true and toggle is icon"); + Assert.equal(firstSeen, 222, "First seen should remain unchanged"); + } + ); + + await doExperimentCleanup(); +}); + +/** + * This tests that the first-time toggle does not change to the icon toggle + * if the displayDuration end date is not yet reached or passed. + */ +add_task(async function test_experiment_displayDuration_end_date_not_reached() { + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "pictureinpicture", + value: { + displayDuration: 5, + }, + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + const currentDateSec = Math.round(Date.now() / 1000); + + await SpecialPowers.pushPrefEnv({ + set: [ + [TOGGLE_FIRST_SEEN_PREF, currentDateSec], + [TOGGLE_HAS_USED_PREF, false], + ], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + const hasUsed = Services.prefs.getBoolPref(TOGGLE_HAS_USED_PREF); + const firstSeen = Services.prefs.getIntPref(TOGGLE_FIRST_SEEN_PREF); + + Assert.ok(!hasUsed, "has-used is false and toggle is not icon"); + Assert.equal( + firstSeen, + currentDateSec, + "First seen should remain unchanged" + ); + } + ); + + await doExperimentCleanup(); +}); + +/** + * This tests that the toggle does not change to the icon toggle if duration is negative. + */ +add_task(async function test_experiment_displayDuration_negative_duration() { + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "pictureinpicture", + value: { + displayDuration: -1, + }, + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + await SpecialPowers.pushPrefEnv({ + set: [ + [TOGGLE_FIRST_SEEN_PREF, 0], + [TOGGLE_HAS_USED_PREF, false], + ], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + const hasUsed = Services.prefs.getBoolPref(TOGGLE_HAS_USED_PREF); + const firstSeen = Services.prefs.getIntPref(TOGGLE_FIRST_SEEN_PREF); + + Assert.ok(!hasUsed, "has-used is false and toggle is not icon"); + Assert.notEqual(firstSeen, 0, "First seen should not be 0"); + } + ); + + await doExperimentCleanup(); +}); + +/** + * This tests that first-seen is only recorded for the first-time toggle. + */ +add_task(async function test_experiment_displayDuration_already_icon() { + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "pictureinpicture", + value: { + displayDuration: 1, + }, + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + await SpecialPowers.pushPrefEnv({ + set: [ + [TOGGLE_FIRST_SEEN_PREF, 0], + [TOGGLE_HAS_USED_PREF, true], + ], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + const firstSeen = Services.prefs.getIntPref(TOGGLE_FIRST_SEEN_PREF); + Assert.equal(firstSeen, 0, "First seen should be 0"); + } + ); + + await doExperimentCleanup(); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_nimbusFirstTimeStyleVariant.js b/toolkit/components/pictureinpicture/tests/browser_nimbusFirstTimeStyleVariant.js new file mode 100644 index 0000000000..492dc6851d --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_nimbusFirstTimeStyleVariant.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +const EXPERIMENT_CLASS_NAME = "experiment"; + +/** + * This tests that the original PiP toggle design is shown. + */ +add_task(async function test_experiment_control_toggle_style() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + const PIP_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; + await SpecialPowers.pushPrefEnv({ + set: [[PIP_PREF, false]], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + await SpecialPowers.spawn( + browser, + [EXPERIMENT_CLASS_NAME], + async EXPERIMENT_CLASS_NAME => { + let video = content.document.getElementById("with-controls"); + let shadowRoot = video.openOrClosedShadowRoot; + + let controlsContainer = + shadowRoot.querySelector(".controlsContainer"); + let pipWrapper = shadowRoot.querySelector(".pip-wrapper"); + let pipExplainer = shadowRoot.querySelector(".pip-explainer"); + + Assert.ok( + !controlsContainer.classList.contains(EXPERIMENT_CLASS_NAME) + ); + Assert.ok(!pipWrapper.classList.contains(EXPERIMENT_CLASS_NAME)); + Assert.ok( + ContentTaskUtils.isVisible(pipExplainer), + "The PiP message should be visible on the toggle" + ); + } + ); + } + ); +}); + +/** + * This tests that the variant PiP toggle design is shown if Nimbus + * variable `oldToggle` is false. + */ +add_task(async function test_experiment_toggle_style() { + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "pictureinpicture", + value: { + oldToggle: false, + }, + }); + + registerCleanupFunction(async function () { + await doExperimentCleanup(); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + const PIP_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; + await SpecialPowers.pushPrefEnv({ + set: [[PIP_PREF, false]], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + await SpecialPowers.spawn( + browser, + [EXPERIMENT_CLASS_NAME], + async EXPERIMENT_CLASS_NAME => { + let video = content.document.getElementById("with-controls"); + let shadowRoot = video.openOrClosedShadowRoot; + + let controlsContainer = + shadowRoot.querySelector(".controlsContainer"); + let pipWrapper = shadowRoot.querySelector(".pip-wrapper"); + let pipExplainer = shadowRoot.querySelector(".pip-explainer"); + + Assert.ok( + controlsContainer.classList.contains(EXPERIMENT_CLASS_NAME) + ); + Assert.ok(pipWrapper.classList.contains(EXPERIMENT_CLASS_NAME)); + Assert.ok( + ContentTaskUtils.isHidden(pipExplainer), + "The PiP message should not be visible on the toggle" + ); + } + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_nimbusMessageFirstTimePip.js b/toolkit/components/pictureinpicture/tests/browser_nimbusMessageFirstTimePip.js new file mode 100644 index 0000000000..e998b4f65d --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_nimbusMessageFirstTimePip.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +const PIP_EXPERIMENT_MESSAGE = "Hello world message"; +const PIP_EXPERIMENT_TITLE = "Hello world title"; + +/** + * This tests that the original DTD string is shown for the PiP toggle + */ +add_task(async function test_experiment_control() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + const l10n = new Localization( + ["branding/brand.ftl", "toolkit/global/videocontrols.ftl"], + true + ); + + let pipExplainerMessage = l10n.formatValueSync( + "videocontrols-picture-in-picture-explainer3" + ); + + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + const PIP_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; + await SpecialPowers.pushPrefEnv({ + set: [[PIP_PREF, false]], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + await SpecialPowers.spawn( + browser, + [pipExplainerMessage], + async function (pipExplainerMessage) { + let video = content.document.getElementById("with-controls"); + let shadowRoot = video.openOrClosedShadowRoot; + let pipButton = shadowRoot.querySelector(".pip-explainer"); + + Assert.equal( + pipButton.textContent.trim(), + pipExplainerMessage, + "The PiP explainer is default" + ); + } + ); + } + ); +}); + +/** + * This tests that the experiment message is shown for the PiP toggle + */ +add_task(async function test_experiment_message() { + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "pictureinpicture", + value: { + title: PIP_EXPERIMENT_TITLE, + message: PIP_EXPERIMENT_MESSAGE, + }, + }); + + registerCleanupFunction(async function () { + await doExperimentCleanup(); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + const PIP_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; + await SpecialPowers.pushPrefEnv({ + set: [[PIP_PREF, false]], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + await SpecialPowers.spawn( + browser, + [PIP_EXPERIMENT_MESSAGE, PIP_EXPERIMENT_TITLE], + async function (PIP_EXPERIMENT_MESSAGE, PIP_EXPERIMENT_TITLE) { + let video = content.document.getElementById("with-controls"); + let shadowRoot = video.openOrClosedShadowRoot; + let pipExplainer = shadowRoot.querySelector(".pip-explainer"); + let pipLabel = shadowRoot.querySelector(".pip-label"); + + Assert.equal( + pipExplainer.textContent.trim(), + PIP_EXPERIMENT_MESSAGE, + "The PiP explainer is being overridden by the experiment" + ); + + Assert.equal( + pipLabel.textContent.trim(), + PIP_EXPERIMENT_TITLE, + "The PiP label is being overridden by the experiment" + ); + } + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_nimbusShowIconOnly.js b/toolkit/components/pictureinpicture/tests/browser_nimbusShowIconOnly.js new file mode 100644 index 0000000000..7c6dcea01b --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_nimbusShowIconOnly.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +const PIP_EXPERIMENT_MESSAGE = "Hello world message"; +const PIP_EXPERIMENT_TITLE = "Hello world title"; + +/** + * This tests that the original DTD string is shown for the PiP toggle + */ +add_task(async function test_experiment_control() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + const l10n = new Localization( + ["branding/brand.ftl", "toolkit/global/videocontrols.ftl"], + true + ); + + let pipExplainerMessage = l10n.formatValueSync( + "videocontrols-picture-in-picture-explainer3" + ); + + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + const PIP_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; + await SpecialPowers.pushPrefEnv({ + set: [[PIP_PREF, false]], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + await SpecialPowers.spawn( + browser, + [pipExplainerMessage], + async function (pipExplainerMessage) { + let video = content.document.getElementById("with-controls"); + let shadowRoot = video.openOrClosedShadowRoot; + let pipButton = shadowRoot.querySelector(".pip-explainer"); + + Assert.equal( + pipButton.textContent.trim(), + pipExplainerMessage, + "The PiP explainer is default" + ); + } + ); + } + ); +}); + +/** + * This tests that the experiment is showing the icon only + */ +add_task(async function test_experiment_iconOnly() { + let experimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "pictureinpicture", + value: { + showIconOnly: true, + }, + }); + + registerCleanupFunction(async function () { + await experimentCleanup(); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await SimpleTest.promiseFocus(browser); + await ensureVideosReady(browser); + + const PIP_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; + await SpecialPowers.pushPrefEnv({ + set: [[PIP_PREF, false]], + }); + + let videoID = "with-controls"; + await hoverToggle(browser, videoID); + + await SpecialPowers.spawn(browser, [], async function () { + let video = content.document.getElementById("with-controls"); + let shadowRoot = video.openOrClosedShadowRoot; + let pipExpanded = shadowRoot.querySelector(".pip-expanded"); + let pipIcon = shadowRoot.querySelector("div.pip-icon"); + + Assert.ok( + ContentTaskUtils.isHidden(pipExpanded), + "The PiP explainer hidden by the experiment" + ); + + Assert.ok( + ContentTaskUtils.isVisible(pipIcon), + "The PiP icon is visible by the experiment" + ); + }); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_noPlayerControlsOnMiddleRightClick.js b/toolkit/components/pictureinpicture/tests/browser_noPlayerControlsOnMiddleRightClick.js new file mode 100644 index 0000000000..e021900cad --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_noPlayerControlsOnMiddleRightClick.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that no player controls are triggered by middle or + * right click. + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.videocontrols.picture-in-picture.audio-toggle.enabled", true], + ], + }); + let videoID = "with-controls"; + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + }); + + // Open the video in PiP + let pipWin = await triggerPictureInPicture(browser, videoID); + + let playPause = pipWin.document.getElementById("playpause"); + let audioButton = pipWin.document.getElementById("audio"); + + // Middle click the pause button + EventUtils.synthesizeMouseAtCenter(playPause, { button: 1 }, pipWin); + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused."); + + // Right click the pause button + EventUtils.synthesizeMouseAtCenter(playPause, { button: 2 }, pipWin); + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused."); + + // Middle click the mute button + EventUtils.synthesizeMouseAtCenter(audioButton, { button: 1 }, pipWin); + ok(!(await isVideoMuted(browser, videoID)), "The audio is not muted."); + + // Right click the mute button + EventUtils.synthesizeMouseAtCenter(audioButton, { button: 2 }, pipWin); + ok(!(await isVideoMuted(browser, videoID)), "The audio is not muted."); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_noToggleOnAudio.js b/toolkit/components/pictureinpicture/tests/browser_noToggleOnAudio.js new file mode 100644 index 0000000000..b6d434cf8c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_noToggleOnAudio.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if a <video> element only has audio, and no video + * frames, that we do not show the toggle. + */ +add_task(async function test_no_toggle_on_audio() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_ROOT + "owl.mp3", + }, + async browser => { + await ensureVideosReady(browser); + await SimpleTest.promiseFocus(browser); + + // The media player document we create for owl.mp3 inserts a <video> + // element pointed at the .mp3 file, which is what we're trying to + // test for. The <video> element does not get an ID created for it + // though, so we sneak one in with SpecialPowers.spawn so that we + // can use testToggleHelper (which requires an ID). + // + // testToggleHelper also wants click-event-helper.js loaded in the + // document, so we insert that too. + const VIDEO_ID = "video-element"; + const SCRIPT_SRC = "click-event-helper.js"; + await SpecialPowers.spawn( + browser, + [VIDEO_ID, SCRIPT_SRC], + async function (videoID, scriptSrc) { + let video = content.document.querySelector("video"); + video.id = videoID; + + let script = content.document.createElement("script"); + script.src = scriptSrc; + content.document.head.appendChild(script); + } + ); + + await testToggleHelper(browser, VIDEO_ID, false); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_occluded_window.js b/toolkit/components/pictureinpicture/tests/browser_occluded_window.js new file mode 100644 index 0000000000..6ee66f2ade --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_occluded_window.js @@ -0,0 +1,258 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the forceAppWindowActive flag is correctly set whenever a PiP window is opened + * and closed across multiple tabs on the same browser window. + */ +add_task(async function forceActiveMultiPiPTabs() { + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + let bc = browser.ownerGlobal.browsingContext; + info("is window active: " + bc.isActive); + + info("Opening new tab"); + let newTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE + ); + let newTabBrowser = newTab.linkedBrowser; + await ensureVideosReady(newTabBrowser); + + ok(!bc.forceAppWindowActive, "Forced window active should be false"); + info("is window active: " + bc.isActive); + + info("Now opening PiP windows"); + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + ok( + bc.forceAppWindowActive, + "Forced window active should be true since PiP is open" + ); + info("is window active: " + bc.isActive); + + let newTabPiPWin = await triggerPictureInPicture(newTabBrowser, videoID); + ok(newTabPiPWin, "Got Picture-in-Picture window in new tab"); + ok( + bc.forceAppWindowActive, + "Force window active should still be true after opening a new PiP window in new tab" + ); + info("is window active: " + bc.isActive); + + let pipClosedNewTab = BrowserTestUtils.domWindowClosed(newTabPiPWin); + let pipUnloadedNewTab = BrowserTestUtils.waitForEvent( + newTabPiPWin, + "unload" + ); + let closeButtonNewTab = newTabPiPWin.document.getElementById("close"); + info("Selecting close button"); + EventUtils.synthesizeMouseAtCenter(closeButtonNewTab, {}, newTabPiPWin); + info("Waiting for PiP window to close"); + await pipUnloadedNewTab; + await pipClosedNewTab; + + ok( + bc.forceAppWindowActive, + "Force window active should still be true after removing new tab's PiP window" + ); + + info("is window active: " + bc.isActive); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let pipUnloaded = BrowserTestUtils.waitForEvent(pipWin, "unload"); + let closeButton = pipWin.document.getElementById("close"); + info("Selecting close button"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + info("Waiting for PiP window to close"); + await pipUnloaded; + await pipClosed; + + ok( + !bc.forceAppWindowActive, + "Force window active should now be false after removing the last PiP window" + ); + + info("is window active: " + bc.isActive); + + await BrowserTestUtils.removeTab(newTab); + } + ); +}); + +/** + * Tests that the forceAppWindowActive flag is correctly set when a tab with PiP enabled is + * moved to another window. + */ +add_task(async function forceActiveMovePiPToWindow() { + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + info("Opening first tab"); + + await ensureVideosReady(browser); + + let tab = gBrowser.getTabForBrowser(browser); + let bc = browser.ownerGlobal.browsingContext; + + info("is window active: " + bc.isActive); + + ok(!bc.forceAppWindowActive, "Forced window active should be false"); + info("is window active: " + bc.isActive); + + info("Now opening PiP windows"); + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window in first tab."); + + ok( + bc.forceAppWindowActive, + "Forced window active should be true since PiP is open" + ); + info("is window active: " + bc.isActive); + + let swapDocShellsPromise = BrowserTestUtils.waitForEvent( + browser, + "SwapDocShells" + ); + let tabClosePromise = BrowserTestUtils.waitForEvent(tab, "TabClose"); + let tabSwapPiPPromise = BrowserTestUtils.waitForEvent( + tab, + "TabSwapPictureInPicture" + ); + info("Replacing tab with window"); + let newWindow = gBrowser.replaceTabWithWindow(tab); + let newWinLoadedPromise = BrowserTestUtils.waitForEvent( + newWindow, + "load" + ); + + info("Waiting for new window to initialize after swap"); + await Promise.all([ + tabSwapPiPPromise, + swapDocShellsPromise, + tabClosePromise, + newWinLoadedPromise, + ]); + + let newWindowBC = newWindow.browsingContext; + tab = newWindow.gBrowser.selectedTab; + + ok( + !bc.forceAppWindowActive, + "Force window active should no longer be true after moving the previous tab to a new window" + ); + info("is window active: " + bc.isActive); + ok( + newWindowBC.forceAppWindowActive, + "Force window active should be true for new window since PiP is open" + ); + info("is secondary window active: " + newWindowBC.isActive); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let pipUnloaded = BrowserTestUtils.waitForEvent(pipWin, "unload"); + let closeButton = pipWin.document.getElementById("close"); + info("Selecting close button"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + info("Waiting for PiP window to close"); + await pipUnloaded; + await pipClosed; + + ok( + !newWindowBC.forceAppWindowActive, + "Force window active should now be false for new window after removing the last PiP window" + ); + info("is secondary window active: " + newWindowBC.isActive); + + await BrowserTestUtils.removeTab(tab); + } + ); +}); + +/** + * Tests that the forceAppWindowActive flag is correctly set when multiple PiP + * windows are created for a single PiP window. + */ +add_task(async function forceActiveMultiPiPSamePage() { + let videoID1 = "with-controls"; + let videoID2 = "no-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + let bc = browser.ownerGlobal.browsingContext; + + ok( + !bc.forceAppWindowActive, + "Forced window active should be false at the start of the test" + ); + info("is window active: " + bc.isActive); + + let pipWin1 = await triggerPictureInPicture(browser, videoID1); + ok(pipWin1, "Got Picture-in-Picture window 1."); + + ok( + bc.forceAppWindowActive, + "Forced window active should be true since PiP is open" + ); + info("is window active: " + bc.isActive); + + let pipWin2 = await triggerPictureInPicture(browser, videoID2); + ok(pipWin2, "Got Picture-in-Picture window 2."); + + ok( + bc.forceAppWindowActive, + "Forced window active should be true after opening another PiP window on the same page" + ); + info("is window active: " + bc.isActive); + + let pipClosed1 = BrowserTestUtils.domWindowClosed(pipWin1); + let pipUnloaded1 = BrowserTestUtils.waitForEvent(pipWin1, "unload"); + let closeButton1 = pipWin1.document.getElementById("close"); + info("Selecting close button"); + EventUtils.synthesizeMouseAtCenter(closeButton1, {}, pipWin1); + info("Waiting for PiP window to close"); + await pipUnloaded1; + await pipClosed1; + + ok( + bc.forceAppWindowActive, + "Force window active should still be true after removing PiP window 1" + ); + info("is window active: " + bc.isActive); + + let pipClosed2 = BrowserTestUtils.domWindowClosed(pipWin2); + let pipUnloaded2 = BrowserTestUtils.waitForEvent(pipWin2, "unload"); + let closeButton2 = pipWin2.document.getElementById("close"); + info("Selecting close button"); + EventUtils.synthesizeMouseAtCenter(closeButton2, {}, pipWin2); + info("Waiting for PiP window to close"); + await pipUnloaded2; + await pipClosed2; + + ok( + !bc.forceAppWindowActive, + "Force window active should now be false after removing PiP window 2" + ); + info("is window active: " + bc.isActive); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_playerControls.js b/toolkit/components/pictureinpicture/tests/browser_playerControls.js new file mode 100644 index 0000000000..9929da597a --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_playerControls.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests functionality of the various controls for the Picture-in-Picture + * video window. + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.videocontrols.picture-in-picture.audio-toggle.enabled", true], + ], + }); + let videoID = "with-controls"; + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let waitForVideoEvent = eventType => { + return BrowserTestUtils.waitForContentEvent(browser, eventType, true); + }; + + await ensureVideosReady(browser); + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + }); + + // Open the video in PiP + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused."); + + let playPause = pipWin.document.getElementById("playpause"); + let audioButton = pipWin.document.getElementById("audio"); + + // Try the pause button + let pausedPromise = waitForVideoEvent("pause"); + EventUtils.synthesizeMouseAtCenter(playPause, {}, pipWin); + await pausedPromise; + ok(await isVideoPaused(browser, videoID), "The video is paused."); + + // Try the play button + let playPromise = waitForVideoEvent("play"); + EventUtils.synthesizeMouseAtCenter(playPause, {}, pipWin); + await playPromise; + ok(!(await isVideoPaused(browser, videoID)), "The video is playing."); + + // Try the mute button + let mutedPromise = waitForVideoEvent("volumechange"); + ok(!(await isVideoMuted(browser, videoID)), "The audio is playing."); + EventUtils.synthesizeMouseAtCenter(audioButton, {}, pipWin); + await mutedPromise; + ok(await isVideoMuted(browser, videoID), "The audio is muted."); + + // Try the unmute button + let unmutedPromise = waitForVideoEvent("volumechange"); + EventUtils.synthesizeMouseAtCenter(audioButton, {}, pipWin); + await unmutedPromise; + ok(!(await isVideoMuted(browser, videoID)), "The audio is playing."); + + // Try the unpip button. + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let unpipButton = pipWin.document.getElementById("unpip"); + EventUtils.synthesizeMouseAtCenter(unpipButton, {}, pipWin); + await pipClosed; + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused."); + + // Try the close button. + pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused."); + + pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + ok(await isVideoPaused(browser, videoID), "The video is paused."); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_preserveTabPipIconOverlay.js b/toolkit/components/pictureinpicture/tests/browser_preserveTabPipIconOverlay.js new file mode 100644 index 0000000000..55cd003a2c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_preserveTabPipIconOverlay.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +const EVENTUTILS_URL = + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js"; +var EventUtils = {}; + +Services.scriptloader.loadSubScript(EVENTUTILS_URL, EventUtils); + +async function detachTab(tab) { + let newWindowPromise = new Promise((resolve, reject) => { + let observe = (win, topic, data) => { + Services.obs.removeObserver(observe, "domwindowopened"); + resolve(win); + }; + Services.obs.addObserver(observe, "domwindowopened"); + }); + + await EventUtils.synthesizePlainDragAndDrop({ + srcElement: tab, + + // destElement is null because tab detaching happens due + // to a drag'n'drop on an invalid drop target. + destElement: null, + + // don't move horizontally because that could cause a tab move + // animation, and there's code to prevent a tab detaching if + // the dragged tab is released while the animation is running. + stepX: 0, + stepY: 100, + }); + + return newWindowPromise; +} + +/** + * Tests that tabs dragged between windows with PiP open, the pip attribute stays + */ +add_task(async function test_dragging_pip_to_other_window() { + // initialize + let win1 = await BrowserTestUtils.openNewBrowserWindow(); + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + + let pipTab = await BrowserTestUtils.openNewForegroundTab( + win1.gBrowser, + TEST_PAGE + ); + let destTab = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser); + + let awaitCloseEventPromise = BrowserTestUtils.waitForEvent( + pipTab, + "TabClose" + ); + let tabSwapPictureInPictureEventPromise = BrowserTestUtils.waitForEvent( + pipTab, + "TabSwapPictureInPicture" + ); + + // Open PiP + let videoID = "with-controls"; + let browser = pipTab.linkedBrowser; + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // tear out window + let effect = EventUtils.synthesizeDrop( + pipTab, + destTab, + [[{ type: TAB_DROP_TYPE, data: pipTab }]], + null, + win1, + win2 + ); + is(effect, "move", "Tab should be moved from win1 to win2."); + + let closeEvent = await awaitCloseEventPromise; + let swappedPipTabsEvent = await tabSwapPictureInPictureEventPromise; + + is( + closeEvent.detail.adoptedBy, + swappedPipTabsEvent.detail, + "Pip tab adopted by new tab created when original tab closed" + ); + + // make sure we reassign the pip tab to the new one + pipTab = swappedPipTabsEvent.detail; + + // check PiP attribute + ok(pipTab.hasAttribute("pictureinpicture"), "Tab should have PiP attribute"); + + // end PiP + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + + // ensure PiP attribute is gone + await TestUtils.waitForCondition( + () => !pipTab.hasAttribute("pictureinpicture"), + "pictureinpicture attribute was removed" + ); + + ok(true, "pictureinpicture attribute successfully cleared"); + + // close windows + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); +}); + +/** + * Tests that tabs torn out into a new window with PiP open, the pip attribute stays + */ +add_task(async function test_dragging_pip_into_new_window() { + // initialize + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + // Create PiP + let videoID = "with-controls"; + let pipTab = gBrowser.getTabForBrowser(browser); + + let pipWin = await triggerPictureInPicture(browser, videoID); + + let tabSwapPictureInPictureEventPromise = BrowserTestUtils.waitForEvent( + pipTab, + "TabSwapPictureInPicture" + ); + + // tear out into new window + let newWin = await detachTab(pipTab); + + let swappedPipTabsEvent = await tabSwapPictureInPictureEventPromise; + pipTab = swappedPipTabsEvent.detail; + + // check PiP attribute + ok( + pipTab.hasAttribute("pictureinpicture"), + "Tab should have PiP attribute" + ); + + // end PiP + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + + // ensure pip attribute is gone + await TestUtils.waitForCondition( + () => !pipTab.hasAttribute("pictureinpicture"), + "pictureinpicture attribute was removed" + ); + ok(true, "pictureinpicture attribute successfully cleared"); + + // close windows + await BrowserTestUtils.closeWindow(newWin); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_privateWindow.js b/toolkit/components/pictureinpicture/tests/browser_privateWindow.js new file mode 100644 index 0000000000..26d051b14a --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_privateWindow.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that a Picture-in-Picture window opened by a Private browsing + * window has the "private" feature set on its window (which is important + * for some things, eg: taskbar grouping on Windows). + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let pipTab = await BrowserTestUtils.openNewForegroundTab( + privateWin.gBrowser, + TEST_PAGE + ); + let browser = pipTab.linkedBrowser; + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + Assert.equal( + pipWin.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags & + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + "Picture-in-Picture window should be marked as private" + ); + + await BrowserTestUtils.closeWindow(privateWin); + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_removeVideoElement.js b/toolkit/components/pictureinpicture/tests/browser_removeVideoElement.js new file mode 100644 index 0000000000..d9622dc3f0 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_removeVideoElement.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if a <video> element is being displayed in a + * Picture-in-Picture window, that the window closes if that + * original <video> is ever removed from the DOM. + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, videoID); + Assert.ok(pipWin, "Got PiP window."); + + // First, let's make sure that removing the _other_ video doesn't cause + // the special event to fire, nor the PiP window to close. + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let doc = content.document; + let otherVideo = doc.querySelector(`video:not([id="${videoID}"])`); + let eventFired = false; + + let listener = e => { + eventFired = true; + }; + + docShell.chromeEventHandler.addEventListener( + "MozStopPictureInPicture", + listener, + { + capture: true, + } + ); + otherVideo.remove(); + Assert.ok( + !eventFired, + "Should not have seen MozStopPictureInPicture for other video" + ); + docShell.chromeEventHandler.removeEventListener( + "MozStopPictureInPicture", + listener, + { + capture: true, + } + ); + }); + + Assert.ok(!pipWin.closed, "PiP window should still be open."); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let doc = content.document; + let video = doc.querySelector(`#${videoID}`); + + let promise = ContentTaskUtils.waitForEvent( + docShell.chromeEventHandler, + "MozStopPictureInPicture", + { capture: true } + ); + video.remove(); + await promise; + }); + + try { + await BrowserTestUtils.waitForCondition( + () => pipWin.closed, + "Player window closed." + ); + } finally { + if (!pipWin.closed) { + await BrowserTestUtils.closeWindow(pipWin); + } + } + } + ); + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_resizeVideo.js b/toolkit/components/pictureinpicture/tests/browser_resizeVideo.js new file mode 100644 index 0000000000..9254ca10cc --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_resizeVideo.js @@ -0,0 +1,293 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Global values for the left and top edge pixel coordinates. These will be written to +// during the add_setup function in this test file. +let gLeftEdge = 0; +let gTopEdge = 0; + +/** + * Run the resize test on a player window. + * + * @param browser (xul:browser) + * The browser that has the source video. + * + * @param videoID (string) + * The id of the video in the browser to test. + * + * @param pipWin (player window) + * A player window to run the tests on. + * + * @param opts (object) + * The options for the test. + * + * pinX (boolean): + * If true, the video's X position shouldn't change when resized. + * + * pinY (boolean): + * If true, the video's Y position shouldn't change when resized. + */ +async function testVideo(browser, videoID, pipWin, { pinX, pinY } = {}) { + async function switchVideoSource(src) { + let videoResized = BrowserTestUtils.waitForEvent(pipWin, "resize"); + await ContentTask.spawn( + browser, + { src, videoID }, + async ({ src, videoID }) => { + let doc = content.document; + let video = doc.getElementById(videoID); + video.src = src; + } + ); + await videoResized; + } + + /** + * Check the new screen position against the previous one. When + * pinX or pinY is true then the top left corner is checked in that + * dimension. Otherwise, the bottom right corner is checked. + * + * The video position is determined by the screen edge it's closest + * to, so in the default bottom right its bottom right corner should + * match the previous video's bottom right corner. For the top left, + * the top left corners should match. + */ + function checkPosition( + previousScreenX, + previousScreenY, + previousWidth, + previousHeight, + newScreenX, + newScreenY, + newWidth, + newHeight + ) { + if (pinX || previousScreenX == gLeftEdge) { + Assert.equal( + previousScreenX, + newScreenX, + "New video is still in the same X position" + ); + } else { + Assert.less( + Math.abs(previousScreenX + previousWidth - (newScreenX + newWidth)), + 2, + "New video ends at the same screen X position (within 1 pixel)" + ); + } + if (pinY) { + Assert.equal( + previousScreenY, + newScreenY, + "New video is still in the same Y position" + ); + } else { + Assert.equal( + previousScreenY + previousHeight, + newScreenY + newHeight, + "New video ends at the same screen Y position" + ); + } + } + + Assert.ok(pipWin, "Got PiP window."); + + let initialWidth = pipWin.innerWidth; + let initialHeight = pipWin.innerHeight; + let initialAspectRatio = initialWidth / initialHeight; + Assert.equal( + Math.floor(initialAspectRatio * 100), + 177, // 16 / 9 = 1.777777777 + "Original aspect ratio is 16:9" + ); + + // Store the window position for later. + let initialScreenX = pipWin.mozInnerScreenX; + let initialScreenY = pipWin.mozInnerScreenY; + + await switchVideoSource("test-video-cropped.mp4"); + + let resizedWidth = pipWin.innerWidth; + let resizedHeight = pipWin.innerHeight; + let resizedAspectRatio = resizedWidth / resizedHeight; + Assert.equal( + Math.floor(resizedAspectRatio * 100), + 133, // 4 / 3 = 1.333333333 + "Resized aspect ratio is 4:3" + ); + Assert.less(resizedWidth, initialWidth, "Resized video has smaller width"); + Assert.equal( + resizedHeight, + initialHeight, + "Resized video is the same vertically" + ); + + let resizedScreenX = pipWin.mozInnerScreenX; + let resizedScreenY = pipWin.mozInnerScreenY; + checkPosition( + initialScreenX, + initialScreenY, + initialWidth, + initialHeight, + resizedScreenX, + resizedScreenY, + resizedWidth, + resizedHeight + ); + + await switchVideoSource("test-video-vertical.mp4"); + + let verticalWidth = pipWin.innerWidth; + let verticalHeight = pipWin.innerHeight; + let verticalAspectRatio = verticalWidth / verticalHeight; + + if (verticalWidth == 136) { + // The video is minimun width allowed + Assert.equal( + Math.floor(verticalAspectRatio * 100), + 56, // 1 / 2 = 0.5 + "Vertical aspect ratio is 1:2" + ); + } else { + Assert.equal( + Math.floor(verticalAspectRatio * 100), + 50, // 1 / 2 = 0.5 + "Vertical aspect ratio is 1:2" + ); + } + + Assert.less(verticalWidth, resizedWidth, "Vertical video width shrunk"); + Assert.equal( + verticalHeight, + initialHeight, + "Vertical video height matches previous height" + ); + + let verticalScreenX = pipWin.mozInnerScreenX; + let verticalScreenY = pipWin.mozInnerScreenY; + checkPosition( + resizedScreenX, + resizedScreenY, + resizedWidth, + resizedHeight, + verticalScreenX, + verticalScreenY, + verticalWidth, + verticalHeight + ); + + await switchVideoSource("test-video.mp4"); + + let restoredWidth = pipWin.innerWidth; + let restoredHeight = pipWin.innerHeight; + let restoredAspectRatio = restoredWidth / restoredHeight; + Assert.equal( + Math.floor(restoredAspectRatio * 100), + 177, + "Restored aspect ratio is still 16:9" + ); + Assert.less( + Math.abs(initialWidth - pipWin.innerWidth), + 2, + "Restored video has its original width" + ); + Assert.equal( + initialHeight, + pipWin.innerHeight, + "Restored video has its original height" + ); + + let restoredScreenX = pipWin.mozInnerScreenX; + let restoredScreenY = pipWin.mozInnerScreenY; + checkPosition( + initialScreenX, + initialScreenY, + initialWidth, + initialHeight, + restoredScreenX, + restoredScreenY, + restoredWidth, + restoredHeight + ); +} + +add_setup(async () => { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + // Reset the saved PiP location to top-left edge of the screen, wherever + // that may be. We record the top-left edge of the screen coordinates into + // global variables to do later coordinate comparisons after resizes. + let clearWin = await triggerPictureInPicture(browser, "with-controls"); + let initialScreenX = clearWin.mozInnerScreenX; + let initialScreenY = clearWin.mozInnerScreenY; + let PiPScreen = PictureInPicture.getWorkingScreen( + initialScreenX, + initialScreenY + ); + [gLeftEdge, gTopEdge] = PictureInPicture.getAvailScreenSize(PiPScreen); + clearWin.moveTo(gLeftEdge, gTopEdge); + + await BrowserTestUtils.closeWindow(clearWin); + } + ); +}); + +/** + * Tests that if a <video> element is resized the Picture-in-Picture window + * will be resized to match the new dimensions. + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, videoID); + + await testVideo(browser, videoID, pipWin); + + pipWin.moveTo(gLeftEdge, gTopEdge); + + await testVideo(browser, videoID, pipWin, { pinX: true, pinY: true }); + + await BrowserTestUtils.closeWindow(pipWin); + } + ); + } +}); + +/** + * Tests that the RTL video starts on the left and is pinned in the X dimension. + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ set: [["intl.l10n.pseudo", "bidi"]] }); + + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, videoID); + + await testVideo(browser, videoID, pipWin, { pinX: true }); + + await BrowserTestUtils.closeWindow(pipWin); + } + ); + } + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_reversePiP.js b/toolkit/components/pictureinpicture/tests/browser_reversePiP.js new file mode 100644 index 0000000000..a8ae20166f --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_reversePiP.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the PiP toggle button is not flipped + * on certain websites (such as whereby.com). + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_ROOT + "test-reversed.html", + }, + async browser => { + await ensureVideosReady(browser); + + let videoID = "reversed"; + + // Test the toggle button + await prepareForToggleClick(browser, videoID); + + // Hover the mouse over the video to reveal the toggle. + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mousemove", + }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + let toggleFlippedAttribute = await SpecialPowers.spawn( + browser, + [videoID], + async videoID => { + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let controlsOverlay = shadowRoot.querySelector(".controlsOverlay"); + + await ContentTaskUtils.waitForCondition(() => { + return controlsOverlay.classList.contains("hovering"); + }, "Waiting for the hovering state to be set on the video."); + + return shadowRoot.firstChild.getAttribute("flipped"); + } + ); + + // The "flipped" attribute should be set on the toggle button (when applicable). + Assert.equal(toggleFlippedAttribute, "true"); + } + ); +}); + +/** + * Tests that the "This video is playing in Picture-in-Picture" message + * as well as the video playing in PiP are both not flipped on certain sites + * (such as whereby.com) + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_ROOT + "test-reversed.html", + }, + async browser => { + /** + * A helper function used to get the "flipped" attribute of the video's shadowRoot's first child. + * @param {Element} browser The <xul:browser> hosting the <video> + * @param {String} videoID The ID of the video being checked + */ + async function getFlippedAttribute(browser, videoID) { + let videoFlippedAttribute = await SpecialPowers.spawn( + browser, + [videoID], + async videoID => { + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + return shadowRoot.firstChild.getAttribute("flipped"); + } + ); + return videoFlippedAttribute; + } + + /** + * A helper function that returns the transform.a of the video being played in PiP. + * @param {Element} playerBrowser The <xul:browser> of the PiP window + */ + async function getPiPVideoTransform(playerBrowser) { + let pipVideoTransform = await SpecialPowers.spawn( + playerBrowser, + [], + async () => { + let video = content.document.querySelector("video"); + return video.getTransformToViewport().a; + } + ); + return pipVideoTransform; + } + + await ensureVideosReady(browser); + + let videoID = "reversed"; + + let videoFlippedAttribute = await getFlippedAttribute(browser, videoID); + Assert.equal(videoFlippedAttribute, null); // The "flipped" attribute should not be set initially. + + let pipWin = await triggerPictureInPicture(browser, videoID); + + videoFlippedAttribute = await getFlippedAttribute(browser, "reversed"); + Assert.equal(videoFlippedAttribute, "true"); // The "flipped" value should be set once the PiP window is opened (when applicable). + + let playerBrowser = pipWin.document.getElementById("browser"); + let pipVideoTransform = await getPiPVideoTransform(playerBrowser); + Assert.equal(pipVideoTransform, -1); + + await ensureMessageAndClosePiP(browser, videoID, pipWin, false); + + videoFlippedAttribute = await getFlippedAttribute(browser, "reversed"); + Assert.equal(videoFlippedAttribute, null); // The "flipped" attribute should be removed after closing PiP. + + // Now we want to test that regular (not-reversed) videos are unaffected + videoID = "not-reversed"; + videoFlippedAttribute = await getFlippedAttribute(browser, videoID); + Assert.equal(videoFlippedAttribute, null); + + pipWin = await triggerPictureInPicture(browser, videoID); + + videoFlippedAttribute = await getFlippedAttribute(browser, videoID); + Assert.equal(videoFlippedAttribute, null); + + playerBrowser = pipWin.document.getElementById("browser"); + pipVideoTransform = await getPiPVideoTransform(playerBrowser); + + await ensureMessageAndClosePiP(browser, videoID, pipWin, false); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_saveLastPiPLoc.js b/toolkit/components/pictureinpicture/tests/browser_saveLastPiPLoc.js new file mode 100644 index 0000000000..1e787dafc7 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_saveLastPiPLoc.js @@ -0,0 +1,398 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This function tests that the browser saves the last location of size of + * the PiP window and will open the next PiP window in the same location + * with the size. It adjusts for aspect ratio by keeping the same height and + * adjusting the width of the PiP window. + */ +async function doTest() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + // Function to switch video source. + async browser => { + async function switchVideoSource(src) { + let videoResized = BrowserTestUtils.waitForEvent(pipWin, "resize"); + await ContentTask.spawn(browser, { src }, async ({ src }) => { + let doc = content.document; + let video = doc.getElementById("with-controls"); + video.src = src; + }); + await videoResized; + } + + function getAvailScreenSize(screen) { + let screenLeft = {}, + screenTop = {}, + screenWidth = {}, + screenHeight = {}; + screen.GetAvailRectDisplayPix( + screenLeft, + screenTop, + screenWidth, + screenHeight + ); + + // We have to divide these dimensions by the CSS scale factor for the + // display in order for the video to be positioned correctly on displays + // that are not at a 1.0 scaling. + let scaleFactor = + screen.contentsScaleFactor / screen.defaultCSSScaleFactor; + screenWidth.value *= scaleFactor; + screenHeight.value *= scaleFactor; + screenLeft.value *= scaleFactor; + screenTop.value *= scaleFactor; + + return [ + screenLeft.value, + screenTop.value, + screenWidth.value, + screenHeight.value, + ]; + } + + let screen = Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager) + .screenForRect(1, 1, 1, 1); + + let [defaultX, defaultY, defaultWidth, defaultHeight] = + getAvailScreenSize(screen); + + // Default size of PiP window + let rightEdge = defaultX + defaultWidth; + let bottomEdge = defaultY + defaultHeight; + + // tab height + // Used only for Linux as the PiP window has a tab + let tabHeight = 35; + + // clear already saved information + clearSavedPosition(); + + // Open PiP + let pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + let defaultPiPWidth = pipWin.innerWidth; + let defaultPiPHeight = pipWin.innerHeight; + + // Check that it is opened at default location + isfuzzy( + pipWin.screenX, + rightEdge - defaultPiPWidth, + ACCEPTABLE_DIFFERENCE, + "Default PiP X location" + ); + if (AppConstants.platform == "linux") { + isfuzzy( + pipWin.screenY, + bottomEdge - defaultPiPHeight - tabHeight, + ACCEPTABLE_DIFFERENCE, + "Default PiP Y location" + ); + } else { + isfuzzy( + pipWin.screenY, + bottomEdge - defaultPiPHeight, + ACCEPTABLE_DIFFERENCE, + "Default PiP Y location" + ); + } + isfuzzy( + pipWin.innerHeight, + defaultPiPHeight, + ACCEPTABLE_DIFFERENCE, + "Default PiP height" + ); + isfuzzy( + pipWin.innerWidth, + defaultPiPWidth, + ACCEPTABLE_DIFFERENCE, + "Default PiP width" + ); + + let top = defaultY; + let left = defaultX; + pipWin.moveTo(left, top); + let height = pipWin.innerHeight / 2; + let width = pipWin.innerWidth / 2; + pipWin.resizeTo(width, height); + + // CLose first PiP window and open another + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + // PiP is opened at 0, 0 with size 1/4 default width and 1/4 default height + isfuzzy( + pipWin.screenX, + left, + ACCEPTABLE_DIFFERENCE, + "Opened at last X location" + ); + isfuzzy( + pipWin.screenY, + top, + ACCEPTABLE_DIFFERENCE, + "Opened at last Y location" + ); + isfuzzy( + pipWin.innerHeight, + height, + ACCEPTABLE_DIFFERENCE, + "Opened with 1/2 default height" + ); + isfuzzy( + pipWin.innerWidth, + width, + ACCEPTABLE_DIFFERENCE, + "Opened with 1/2 default width" + ); + + // Mac and Linux did not allow moving to coordinates offscreen so this + // test is skipped on those platforms + if (AppConstants.platform == "win") { + // Move to -1111, -1111 and adjust size to 1/4 width and 1/4 height + left = -11111; + top = -11111; + pipWin.moveTo(left, top); + pipWin.resizeTo(pipWin.innerWidth / 4, pipWin.innerHeight / 4); + + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + // because the coordinates are off screen, the default size and location will be used + isfuzzy( + pipWin.screenX, + rightEdge - defaultPiPWidth, + ACCEPTABLE_DIFFERENCE, + "Opened at default X location" + ); + isfuzzy( + pipWin.screenY, + bottomEdge - defaultPiPHeight, + ACCEPTABLE_DIFFERENCE, + "Opened at default Y location" + ); + isfuzzy( + pipWin.innerWidth, + defaultPiPWidth, + ACCEPTABLE_DIFFERENCE, + "Opened at default PiP width" + ); + isfuzzy( + pipWin.innerHeight, + defaultPiPHeight, + ACCEPTABLE_DIFFERENCE, + "Opened at default PiP height" + ); + } + + // Linux doesn't handle switching the video source well and it will + // cause the tests to failed in unexpected ways. Possibly caused by + // bug 1594223 https://bugzilla.mozilla.org/show_bug.cgi?id=1594223 + if (AppConstants.platform != "linux") { + // Save width and height for when aspect ratio is changed + height = pipWin.innerHeight; + width = pipWin.innerWidth; + + left = 200; + top = 100; + pipWin.moveTo(left, top); + + // Now switch the video so the video ratio is different + await switchVideoSource("test-video-cropped.mp4"); + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + isfuzzy( + pipWin.screenX, + left, + ACCEPTABLE_DIFFERENCE, + "Opened at last X location" + ); + isfuzzy( + pipWin.screenY, + top, + ACCEPTABLE_DIFFERENCE, + "Opened at last Y location" + ); + isfuzzy( + pipWin.innerHeight, + height, + ACCEPTABLE_DIFFERENCE, + "Opened height with previous width" + ); + isfuzzy( + pipWin.innerWidth, + height * (pipWin.innerWidth / pipWin.innerHeight), + ACCEPTABLE_DIFFERENCE, + "Width is changed to adjust for aspect ration" + ); + + left = 300; + top = 300; + pipWin.moveTo(left, top); + pipWin.resizeTo(defaultPiPWidth / 2, defaultPiPHeight / 2); + + // Save height for when aspect ratio is changed + height = pipWin.innerHeight; + + // Now switch the video so the video ratio is different + await switchVideoSource("test-video.mp4"); + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + isfuzzy( + pipWin.screenX, + left, + ACCEPTABLE_DIFFERENCE, + "Opened at last X location" + ); + isfuzzy( + pipWin.screenY, + top, + ACCEPTABLE_DIFFERENCE, + "Opened at last Y location" + ); + isfuzzy( + pipWin.innerHeight, + height, + ACCEPTABLE_DIFFERENCE, + "Opened with previous height" + ); + isfuzzy( + pipWin.innerWidth, + height * (pipWin.innerWidth / pipWin.innerHeight), + ACCEPTABLE_DIFFERENCE, + "Width is changed to adjust for aspect ration" + ); + } + + // Move so that part of PiP is off screen (bottom right) + + left = rightEdge - Math.round((3 * pipWin.innerWidth) / 4); + top = bottomEdge - Math.round((3 * pipWin.innerHeight) / 4); + + let movePromise = BrowserTestUtils.waitForEvent( + pipWin.windowRoot, + "MozUpdateWindowPos" + ); + pipWin.moveTo(left, top); + await movePromise; + + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + // Redefine top and left to where the PiP windop will open + left = rightEdge - pipWin.innerWidth; + top = bottomEdge - pipWin.innerHeight; + + // await new Promise(r => setTimeout(r, 5000)); + // PiP is opened bottom right but not off screen + isfuzzy( + pipWin.screenX, + left, + ACCEPTABLE_DIFFERENCE, + "Opened at last X location but shifted back on screen" + ); + if (AppConstants.platform == "linux") { + isfuzzy( + pipWin.screenY, + top - tabHeight, + ACCEPTABLE_DIFFERENCE, + "Opened at last Y location but shifted back on screen" + ); + } else { + isfuzzy( + pipWin.screenY, + top, + ACCEPTABLE_DIFFERENCE, + "Opened at last Y location but shifted back on screen" + ); + } + + // Move so that part of PiP is off screen (top left) + left = defaultX - Math.round(pipWin.innerWidth / 4); + top = defaultY - Math.round(pipWin.innerHeight / 4); + + movePromise = BrowserTestUtils.waitForEvent( + pipWin.windowRoot, + "MozUpdateWindowPos" + ); + pipWin.moveTo(left, top); + await movePromise; + + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + // PiP is opened top left on screen + isfuzzy( + pipWin.screenX, + defaultX, + ACCEPTABLE_DIFFERENCE, + "Opened at last X location but shifted back on screen" + ); + isfuzzy( + pipWin.screenY, + defaultY, + ACCEPTABLE_DIFFERENCE, + "Opened at last Y location but shifted back on screen" + ); + + if (AppConstants.platform != "linux") { + // test that if video is on right edge and new video with smaller width + // is opened next, it is still on the right edge + left = rightEdge - pipWin.innerWidth; + top = Math.round(bottomEdge / 4); + + pipWin.moveTo(left, top); + + // Used to ensure that video width decreases for next PiP window + width = pipWin.innerWidth; + isfuzzy( + pipWin.innerWidth + pipWin.screenX, + rightEdge, + ACCEPTABLE_DIFFERENCE, + "Video is on right edge before video is changed" + ); + + // Now switch the video so the video width is smaller + await switchVideoSource("test-video-cropped.mp4"); + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + Assert.less(pipWin.innerWidth, width, "New video width is smaller"); + isfuzzy( + pipWin.innerWidth + pipWin.screenX, + rightEdge, + ACCEPTABLE_DIFFERENCE, + "Video is on right edge after video is changed" + ); + } + + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + } + ); +} + +add_task(async function test_pip_save_last_loc() { + await doTest(); +}); + +add_task(async function test_pip_save_last_loc_with_os_zoom() { + await SpecialPowers.pushPrefEnv({ set: [["ui.textScaleFactor", 120]] }); + await doTest(); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_shortcutsAfterFocus.js b/toolkit/components/pictureinpicture/tests/browser_shortcutsAfterFocus.js new file mode 100644 index 0000000000..7d40664df4 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_shortcutsAfterFocus.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests functionality of arrow keys in Picture-in-Picture window + * for seeking and volume adjustment + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.keyboard-controls.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let waitForVideoEvent = eventType => { + return BrowserTestUtils.waitForContentEvent(browser, eventType, true); + }; + + await ensureVideosReady(browser); + + // Open the video in PiP + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // run the next tests 4 times to ensure that they work for each PiP button, including none + for (var i = 0; i < 4; i++) { + // Try seek forward + let seekedForwardPromise = waitForVideoEvent("seeked"); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, pipWin); + ok(await seekedForwardPromise, "The time seeked forward"); + + // Try seek backward + let seekedBackwardPromise = waitForVideoEvent("seeked"); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, pipWin); + ok(await seekedBackwardPromise, "The time seeked backward"); + + // Try volume down + let volumeDownPromise = waitForVideoEvent("volumechange"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, pipWin); + ok(await volumeDownPromise, "The volume went down"); + + // Try volume up + let volumeUpPromise = waitForVideoEvent("volumechange"); + EventUtils.synthesizeKey("KEY_ArrowUp", {}, pipWin); + ok(await volumeUpPromise, "The volume went up"); + + // Tab to get to the next button + EventUtils.synthesizeKey("KEY_Tab", {}, pipWin); + } + + await BrowserTestUtils.closeWindow(pipWin); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_showMessage.js b/toolkit/components/pictureinpicture/tests/browser_showMessage.js new file mode 100644 index 0000000000..24d8347a7f --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_showMessage.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that triggering Picture-in-Picture causes the Picture-in-Picture + * window to be opened, and a message to be displayed in the original video + * player area. Also ensures that once the Picture-in-Picture window is closed, + * the video goes back to the original state. + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + await ensureMessageAndClosePiP(browser, videoID, pipWin, false); + } + ); + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_smallVideoLayout.js b/toolkit/components/pictureinpicture/tests/browser_smallVideoLayout.js new file mode 100644 index 0000000000..84866fba3e --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_smallVideoLayout.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the Picture-in-Picture toggle is hidden when videos + * are laid out with dimensions smaller than MIN_VIDEO_DIMENSION (a + * constant that is also defined in videocontrols.js). + */ +add_task(async () => { + // Most of the Picture-in-Picture tests run with the always-show + // preference set to true to avoid the toggle visibility heuristics. + // Since this test actually exercises those heuristics, we have + // to temporarily disable that pref. + // + // We also reduce the minimum video length for displaying the toggle + // to 5 seconds to avoid having to include or generate a 45 second long + // video (which is the default minimum length). + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.video-toggle.always-show", + false, + ], + ["media.videocontrols.picture-in-picture.video-toggle.min-video-secs", 5], + ], + }); + + // This is the minimum size of the video in either width or height for + // which we will show the toggle. See videocontrols.js. + const MIN_VIDEO_DIMENSION = 140; // pixels + + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_SOUND, + gBrowser, + }, + async browser => { + // Shrink the video down to less than MIN_VIDEO_DIMENSION. + let targetWidth = MIN_VIDEO_DIMENSION - 1; + await SpecialPowers.spawn( + browser, + [videoID, targetWidth], + async (videoID, targetWidth) => { + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let resizePromise = ContentTaskUtils.waitForEvent( + shadowRoot.firstChild, + "resizevideocontrols" + ); + video.style.width = targetWidth + "px"; + await resizePromise; + } + ); + + // The toggle should be hidden. + await testToggleHelper(browser, videoID, false); + + // Now re-expand the video. + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let resizePromise = ContentTaskUtils.waitForEvent( + shadowRoot.firstChild, + "resizevideocontrols" + ); + video.style.width = ""; + await resizePromise; + }); + + // The toggle should be visible. + await testToggleHelper(browser, videoID, true); + } + ); + } +}); + +/** + * Tests that when using the experimental toggle variations, videos + * under 320px width are given the "small-video" attribute. + */ +add_task(async () => { + const TOGGLE_SMALL = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-wrapper": DEFAULT_TOGGLE_OPACITY, + }, + hidden: [], + }, + + hoverToggle: { + opacities: { + ".pip-wrapper": 1.0, + }, + hidden: [".pip-expanded"], + }, + }, + }; + + const TOGGLE_LARGE = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-small": 0.0, + ".pip-wrapper": DEFAULT_TOGGLE_OPACITY, + ".pip-expanded": 0.0, + }, + hidden: [".pip-explainer", ".pip-icon-label > .pip-icon"], + }, + hoverToggle: { + opacities: { + ".pip-small": 0.0, + ".pip-wrapper": 1.0, + }, + hidden: [ + ".pip-explainer", + ".pip-icon-label > .pip-icon", + ".pip-expanded", + ], + }, + }, + }; + + // Most of the Picture-in-Picture tests run with the always-show + // preference set to true to avoid the toggle visibility heuristics. + // Since this test actually exercises those heuristics, we have + // to temporarily disable that pref. + // + // We also reduce the minimum video length for displaying the toggle + // to 5 seconds to avoid having to include or generate a 45 second long + // video (which is the default minimum length). + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.video-toggle.always-show", + false, + ], + ["media.videocontrols.picture-in-picture.video-toggle.min-video-secs", 5], + ["media.videocontrols.picture-in-picture.video-toggle.mode", 1], + ], + }); + + // Videos that are thinner than MIN_VIDEO_WIDTH should have the small-video + // attribute set on the experimental toggle. + const MIN_VIDEO_WIDTH = 320; // pixels + + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_SOUND, + gBrowser, + }, + async browser => { + // Shrink the video down to less than MIN_VIDEO_WIDTH. + let targetWidth = MIN_VIDEO_WIDTH - 1; + let isSmallVideo = await SpecialPowers.spawn( + browser, + [videoID, targetWidth], + async (videoID, targetWidth) => { + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let resizePromise = ContentTaskUtils.waitForEvent( + shadowRoot.firstChild, + "resizevideocontrols" + ); + video.style.width = targetWidth + "px"; + await resizePromise; + let toggle = shadowRoot.getElementById("pictureInPictureToggle"); + return toggle.hasAttribute("small-video"); + } + ); + + Assert.ok(isSmallVideo, "Video should have small-video attribute"); + + await testToggleHelper(browser, videoID, true, undefined, TOGGLE_SMALL); + + // Now re-expand the video. + isSmallVideo = await SpecialPowers.spawn( + browser, + [videoID], + async videoID => { + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let resizePromise = ContentTaskUtils.waitForEvent( + shadowRoot.firstChild, + "resizevideocontrols" + ); + video.style.width = ""; + await resizePromise; + let toggle = shadowRoot.getElementById("pictureInPictureToggle"); + return toggle.hasAttribute("small-video"); + } + ); + + Assert.ok(!isSmallVideo, "Video should not have small-video attribute"); + + await testToggleHelper(browser, videoID, true, undefined, TOGGLE_LARGE); + } + ); + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_stripVideoStyles.js b/toolkit/components/pictureinpicture/tests/browser_stripVideoStyles.js new file mode 100644 index 0000000000..10eb8f43ea --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_stripVideoStyles.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that <video>'s with styles on the element don't have those + * styles cloned over into the <video> that's inserted into the + * player window. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let styles = { + padding: "15px", + border: "5px solid red", + margin: "3px", + position: "absolute", + }; + + await SpecialPowers.spawn(browser, [styles], async videoStyles => { + let video = content.document.getElementById("no-controls"); + for (let styleProperty in videoStyles) { + video.style[styleProperty] = videoStyles[styleProperty]; + } + }); + + let pipWin = await triggerPictureInPicture(browser, "no-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + let playerBrowser = pipWin.document.getElementById("browser"); + await SpecialPowers.spawn(playerBrowser, [styles], async videoStyles => { + let video = content.document.querySelector("video"); + for (let styleProperty in videoStyles) { + Assert.equal( + video.style[styleProperty], + "", + `Style ${styleProperty} should not be set` + ); + } + }); + await BrowserTestUtils.closeWindow(pipWin); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_subtitles_settings_panel.js b/toolkit/components/pictureinpicture/tests/browser_subtitles_settings_panel.js new file mode 100644 index 0000000000..bbb81cde55 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_subtitles_settings_panel.js @@ -0,0 +1,273 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that pressing the Escape key will close the subtitles settings panel and + * not remove focus if activated via the mouse. + */ +add_task(async function test_closePanelESCMouseFocus() { + clearSavedPosition(); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + [ + "media.videocontrols.picture-in-picture.display-text-tracks.size", + "medium", + ], + ], + }); + + let videoID = "with-controls"; + + await ensureVideosReady(browser); + await prepareVideosAndWebVTTTracks(browser, videoID); + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + }); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // Resize PiP window so that subtitles button is visible + let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize"); + pipWin.resizeTo(640, 360); + await resizePromise; + + let subtitlesButton = pipWin.document.getElementById("closed-caption"); + Assert.ok(subtitlesButton, "Subtitles button found"); + + let subtitlesPanel = pipWin.document.getElementById("settings"); + let panelVisiblePromise = BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(subtitlesPanel), + "Wait for panel to be visible" + ); + + EventUtils.synthesizeMouseAtCenter(subtitlesButton, {}, pipWin); + + await panelVisiblePromise; + + let audioButton = pipWin.document.getElementById("audio"); + audioButton.focus(); + + let panelHiddenPromise = BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isHidden(subtitlesPanel), + "Wait for panel to be hidden" + ); + + EventUtils.synthesizeKey("KEY_Escape", {}, pipWin); + + info("Make sure subtitles settings panel closes after pressing ESC"); + await panelHiddenPromise; + + Assert.notEqual( + pipWin.document.activeElement, + subtitlesButton, + "Subtitles button does not have focus after closing panel" + ); + Assert.ok(pipWin, "PiP window is still open"); + + clearSavedPosition(); + } + ); +}); + +/** + * Tests that pressing the Escape key will close the subtitles settings panel and + * refocus on the subtitles button if activated via the keyboard. + */ +add_task(async function test_closePanelESCKeyboardFocus() { + clearSavedPosition(); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + + let videoID = "with-controls"; + + await ensureVideosReady(browser); + await prepareVideosAndWebVTTTracks(browser, videoID); + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + }); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // Resize PiP window so that subtitles button is visible + let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize"); + pipWin.resizeTo(640, 360); + await resizePromise; + + let subtitlesButton = pipWin.document.getElementById("closed-caption"); + Assert.ok(subtitlesButton, "Subtitles button found"); + + let subtitlesPanel = pipWin.document.getElementById("settings"); + let subtitlesToggle = pipWin.document.getElementById("subtitles-toggle"); + let panelVisiblePromise = BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(subtitlesPanel), + "Wait for panel to be visible" + ); + + subtitlesButton.focus(); + EventUtils.synthesizeKey(" ", {}, pipWin); + + await panelVisiblePromise; + + Assert.equal( + pipWin.document.activeElement, + subtitlesToggle, + "Subtitles switch toggle should have focus after opening panel" + ); + + let panelHiddenPromise = BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isHidden(subtitlesPanel), + "Wait for panel to be hidden" + ); + + EventUtils.synthesizeKey("KEY_Escape", {}, pipWin); + + info("Make sure subtitles settings panel closes after pressing ESC"); + await panelHiddenPromise; + + Assert.equal( + pipWin.document.activeElement, + subtitlesButton, + "Subtitles button has focus after closing panel" + ); + Assert.ok(pipWin, "PiP window is still open"); + + clearSavedPosition(); + } + ); +}); + +/** + * Tests keyboard navigation for the subtitles settings panel and that it closes after selecting + * the subtitles button. + */ +add_task(async function test_panelKeyboardButtons() { + clearSavedPosition(); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + + let videoID = "with-controls"; + + await ensureVideosReady(browser); + await prepareVideosAndWebVTTTracks(browser, videoID); + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + // Mute video + content.document.getElementById(videoID).muted = true; + }); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // Resize PiP window so that subtitles button is visible + let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize"); + pipWin.resizeTo(640, 360); + await resizePromise; + + let subtitlesButton = pipWin.document.getElementById("closed-caption"); + Assert.ok(subtitlesButton, "Subtitles button found"); + + let subtitlesPanel = pipWin.document.getElementById("settings"); + let subtitlesToggle = pipWin.document.getElementById("subtitles-toggle"); + let panelVisiblePromise = BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(subtitlesPanel), + "Wait for panel to be visible" + ); + + subtitlesButton.focus(); + EventUtils.synthesizeKey(" ", {}, pipWin); + + await panelVisiblePromise; + + Assert.equal( + pipWin.document.activeElement, + subtitlesToggle, + "Subtitles switch toggle should have focus after opening panel" + ); + + let fontMediumRadio = pipWin.document.getElementById("medium"); + EventUtils.synthesizeKey("KEY_Tab", {}, pipWin); + + Assert.equal( + pipWin.document.activeElement, + fontMediumRadio, + "Medium font size radio button should have focus" + ); + + let fontSmallRadio = pipWin.document.getElementById("small"); + EventUtils.synthesizeKey("KEY_ArrowUp", {}, pipWin); + + Assert.equal( + pipWin.document.activeElement, + fontSmallRadio, + "Small font size radio button should have focus" + ); + Assert.ok(isVideoMuted(browser, videoID), "Video should still be muted"); + Assert.equal( + SpecialPowers.getCharPref( + "media.videocontrols.picture-in-picture.display-text-tracks.size" + ), + "small", + "Font size changed to small" + ); + + subtitlesButton.focus(); + + let panelHiddenPromise = BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isHidden(subtitlesPanel), + "Wait for panel to be hidden" + ); + + EventUtils.synthesizeKey(" ", {}, pipWin); + + info( + "Make sure subtitles settings panel closes after pressing the subtitles button" + ); + await panelHiddenPromise; + + Assert.ok(pipWin, "PiP window is still open"); + + clearSavedPosition(); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_tabIconOverlayPiP.js b/toolkit/components/pictureinpicture/tests/browser_tabIconOverlayPiP.js new file mode 100644 index 0000000000..3ca55f2c73 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_tabIconOverlayPiP.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * The goal of this test is check the that "tab-icon-overlay" image is + * showing when the tab is using PiP. + * + * The browser will create a tab and open a video using PiP + * then the tests check that the tab icon overlay image is showing* + * + * + */ +add_task(async () => { + let videoID = "with-controls"; + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_SOUND, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + let audioPromise = BrowserTestUtils.waitForEvent( + browser, + "DOMAudioPlaybackStarted" + ); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + }); + + // Check that video is playing + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused."); + await audioPromise; + + // Need tab to access the tab-icon-overlay element + let tab = gBrowser.getTabForBrowser(browser); + + // Use tab to get the tab-icon-overlay element + let tabIconOverlay = tab.getElementsByClassName("tab-icon-overlay")[0]; + + // Not in PiP yet so the tab-icon-overlay does not have "pictureinpicture" attribute + ok(!tabIconOverlay.hasAttribute("pictureinpicture"), "Not using PiP"); + + // Sound is playing so tab should have "soundplaying" attribute + ok(tabIconOverlay.hasAttribute("soundplaying"), "Sound is playing"); + + // Start the PiP + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // Check that video is still playing + ok(!(await isVideoPaused(browser, videoID)), "The video is not paused."); + + // Video is still playing so the tab-icon-overlay should have "soundplaying" as an attribute + ok( + tabIconOverlay.hasAttribute("soundplaying"), + "Tab knows sound is playing" + ); + + // Now in PiP. "pictureinpicture" is an attribute + ok( + tabIconOverlay.hasAttribute("pictureinpicture"), + "Tab knows were using PiP" + ); + + // We know the tab has sound playing and it is using PiP so we can check the + // tab-icon-overlay image is showing + let style = window.getComputedStyle(tabIconOverlay); + Assert.equal( + style.listStyleImage, + 'url("chrome://browser/skin/tabbrowser/tab-audio-playing-small.svg")', + "Got the tab-icon-overlay image" + ); + + // Check tab is not muted + ok(!tabIconOverlay.hasAttribute("muted"), "Tab is not muted"); + + // Click on tab icon overlay to mute tab and check it is muted + tabIconOverlay.click(); + ok(tabIconOverlay.hasAttribute("muted"), "Tab is muted"); + + // Click on tab icon overlay to unmute tab and check it is not muted + tabIconOverlay.click(); + ok(!tabIconOverlay.hasAttribute("muted"), "Tab is not muted"); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_telemetry_enhancements.js b/toolkit/components/pictureinpicture/tests/browser_telemetry_enhancements.js new file mode 100644 index 0000000000..3f027de170 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_telemetry_enhancements.js @@ -0,0 +1,230 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PAGE_LONG = TEST_ROOT + "test-video-selection.html"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const EXPECTED_EVENT_CREATE = [ + [ + "pictureinpicture", + "create", + "player", + undefined, + { ccEnabled: "false", webVTTSubtitles: "false" }, + ], +]; + +const EXPECTED_EVENT_CREATE_WITH_TEXT_TRACKS = [ + [ + "pictureinpicture", + "create", + "player", + undefined, + { ccEnabled: "true", webVTTSubtitles: "true" }, + ], +]; + +const EXPECTED_EVENT_CLOSED_METHOD_CLOSE_BUTTON = [ + { + category: "pictureinpicture", + method: "closed_method", + object: "closeButton", + }, +]; + +const videoID = "with-controls"; + +const EXPECTED_EVENT_CLOSED_METHOD_UNPIP = [ + { + category: "pictureinpicture", + method: "closed_method", + object: "unpip", + }, +]; + +const FULLSCREEN_EVENTS = [ + { + category: "pictureinpicture", + method: "fullscreen", + object: "player", + extraKey: { enter: "true" }, + }, + { + category: "pictureinpicture", + method: "fullscreen", + object: "player", + extraKey: { enter: "true" }, + }, +]; + +add_task(async function testCreateAndCloseButtonTelemetry() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + Services.telemetry.clearEvents(); + + await ensureVideosReady(browser); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let filter = { + category: "pictureinpicture", + method: "create", + object: "player", + }; + await waitForTelemeryEvents( + filter, + EXPECTED_EVENT_CREATE.length, + "parent" + ); + + TelemetryTestUtils.assertEvents(EXPECTED_EVENT_CREATE, filter, { + clear: true, + process: "parent", + }); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let closeButton = pipWin.document.getElementById("close"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin); + await pipClosed; + + filter = { + category: "pictureinpicture", + method: "closed_method", + object: "closeButton", + }; + await waitForTelemeryEvents( + filter, + EXPECTED_EVENT_CLOSED_METHOD_CLOSE_BUTTON.length, + "parent" + ); + + TelemetryTestUtils.assertEvents( + EXPECTED_EVENT_CLOSED_METHOD_CLOSE_BUTTON, + filter, + { clear: true, process: "parent" } + ); + + let hist = TelemetryTestUtils.getAndClearHistogram( + "FX_PICTURE_IN_PICTURE_WINDOW_OPEN_DURATION" + ); + + Assert.ok(hist, "Histogram exists"); + } + ); +}); + +add_task(async function textTextTracksAndUnpipTelemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + Services.telemetry.clearEvents(); + + await ensureVideosReady(browser); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let filter = { + category: "pictureinpicture", + method: "create", + object: "player", + }; + await waitForTelemeryEvents( + filter, + EXPECTED_EVENT_CREATE_WITH_TEXT_TRACKS.length, + "parent" + ); + + TelemetryTestUtils.assertEvents( + EXPECTED_EVENT_CREATE_WITH_TEXT_TRACKS, + filter, + { clear: true, process: "parent" } + ); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let unpipButton = pipWin.document.getElementById("unpip"); + EventUtils.synthesizeMouseAtCenter(unpipButton, {}, pipWin); + await pipClosed; + + filter = { + category: "pictureinpicture", + method: "closed_method", + object: "unpip", + }; + await waitForTelemeryEvents( + filter, + EXPECTED_EVENT_CLOSED_METHOD_UNPIP.length, + "parent" + ); + + TelemetryTestUtils.assertEvents( + EXPECTED_EVENT_CLOSED_METHOD_UNPIP, + filter, + { clear: true, process: "parent" } + ); + } + ); +}); + +add_task(async function test_fullscreen_events() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + Services.telemetry.clearEvents(); + + await ensureVideosReady(browser); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let fullscreenBtn = pipWin.document.getElementById("fullscreen"); + + await promiseFullscreenEntered(pipWin, () => { + fullscreenBtn.click(); + }); + + await promiseFullscreenExited(pipWin, () => { + fullscreenBtn.click(); + }); + + let filter = { + category: "pictureinpicture", + method: "fullscreen", + object: "player", + }; + await waitForTelemeryEvents(filter, FULLSCREEN_EVENTS.length, "parent"); + + TelemetryTestUtils.assertEvents(FULLSCREEN_EVENTS, filter, { + clear: true, + process: "parent", + }); + + await ensureMessageAndClosePiP(browser, videoID, pipWin, false); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_1.js b/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_1.js new file mode 100644 index 0000000000..b2b1ded13d --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_1.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that text tracks shown on the source video + * do not appear on a newly created pip window if the pref + * is disabled. + */ +add_task(async function test_text_tracks_new_window_pref_disabled() { + info("Running test: new window - pref disabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + false, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + + ok(textTracks, "TextTracks container should exist in the pip window"); + ok( + !textTracks.textContent, + "Text tracks should not be visible on the pip window" + ); + }); + } + ); +}); + +/** + * This test ensures that text tracks shown on the source video + * appear on a newly created pip window if the pref is enabled. + */ +add_task(async function test_text_tracks_new_window_pref_enabled() { + info("Running test: new window - pref enabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + }); + } + ); +}); + +/** + * This test ensures that text tracks do not appear on a new pip window + * if no track is loaded and the pref is enabled. + */ +add_task(async function test_text_tracks_new_window_no_track() { + info("Running test: new window - no track"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID, -1); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + + ok(textTracks, "TextTracks container should exist in the pip window"); + ok( + !textTracks.textContent, + "Text tracks should not be visible on the pip window" + ); + }); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_2.js b/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_2.js new file mode 100644 index 0000000000..9f1ebefbf5 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_2.js @@ -0,0 +1,448 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that text tracks disappear from the pip window + * when the pref is disabled. + */ +add_task(async function test_text_tracks_existing_window_pref_disabled() { + info("Running test: existing window - pref disabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + }); + + info("Turning off pref"); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + false, + ], + ], + }); + + // Verify that cue is no longer on the pip window + info("Checking that cue is no longer on pip window"); + await SpecialPowers.spawn(pipBrowser, [], async () => { + let textTracks = content.document.getElementById("texttracks"); + await ContentTaskUtils.waitForCondition(() => { + return !textTracks.textContent; + }, `Text track is still visible on the pip window. Got ${textTracks.textContent}`); + info("Successfully removed text tracks from pip window"); + }); + } + ); +}); + +/** + * This test ensures that text tracks shown on the source video + * window appear on an existing pip window when the pref is enabled. + */ +add_task(async function test_text_tracks_existing_window_pref_enabled() { + info("Running test: existing window - pref enabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + false, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + + ok(textTracks, "TextTracks container should exist in the pip window"); + ok( + !textTracks.textContent, + "Text tracks should not be visible on the pip window" + ); + }); + + info("Turning on pref"); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + + info("Checking that cue is on pip window"); + await SpecialPowers.spawn(pipBrowser, [], async () => { + let textTracks = content.document.getElementById("texttracks"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + info("Successfully displayed text tracks on pip window"); + }); + } + ); +}); + +/** + * This test ensures that text tracks update to the correct track + * when a new track is selected. + */ +add_task(async function test_text_tracks_existing_window_new_track() { + info("Running test: existing window - new track"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + ok( + textTracks.textContent.includes("track 1"), + "Track 1 should be loaded" + ); + }); + + // Change track in the content window + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + let tracks = video.textTracks; + + info("Changing to a new track"); + let track1 = tracks[0]; + track1.mode = "disabled"; + let track2 = tracks[1]; + track2.mode = "showing"; + }); + + // Ensure new track is loaded + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking new text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + ok( + textTracks.textContent.includes("track 2"), + "Track 2 should be loaded" + ); + }); + } + ); +}); + +/** + * This test ensures that text tracks are correctly updated. + */ +add_task(async function test_text_tracks_existing_window_cues() { + info("Running test: existing window - cues"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + + // Verify that first cue appears + info("Checking first cue on pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + ok( + textTracks.textContent.includes("cue 1"), + `Expected text should be displayed on the pip window. Got ${textTracks.textContent}.` + ); + }); + + // Play video to move to the next cue + await waitForNextCue(browser, videoID); + + // Test remaining cues + await SpecialPowers.spawn(pipBrowser, [], async () => { + let textTracks = content.document.getElementById("texttracks"); + + // Verify that empty cue makes text disappear + info("Checking empty cue in pip window"); + await ContentTaskUtils.waitForCondition(() => { + info(`Current text content is: ${textTracks.textContent}`); + return !textTracks.textContent; + }, `Text track is still visible on the pip window. Got ${textTracks.textContent}`); + }); + + await waitForNextCue(browser, videoID); + + // Wait and verify second cue + await SpecialPowers.spawn(pipBrowser, [], async () => { + let textTracks = content.document.getElementById("texttracks"); + info("Checking second cue in pip window"); + await ContentTaskUtils.waitForCondition(() => { + // Cue may not appear right away after cuechange event. + // Wait until it appears before verifying text content. + info(`Current text content is: ${textTracks.textContent}`); + return ( + textTracks.textContent && textTracks.textContent.includes("cue 2") + ); + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + }); + } + ); +}); + +/** + * This test ensures that text tracks disappear if no track is selected. + */ +add_task(async function test_text_tracks_existing_window_no_track() { + info("Running test: existing window - no track"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + }); + + // Remove track in the content window + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + let tracks = video.textTracks; + + info("Removing tracks"); + let track1 = tracks[0]; + track1.mode = "disabled"; + let track2 = tracks[1]; + track2.mode = "disabled"; + }); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking that text track disappears from pip window"); + let textTracks = content.document.getElementById("texttracks"); + + await ContentTaskUtils.waitForCondition(() => { + return !textTracks.textContent; + }, `Text track is still visible on the pip window. Got ${textTracks.textContent}`); + }); + } + ); +}); + +/** + * This test ensures that text tracks appear correctly if there are multiple active cues. + */ +add_task(async function test_text_tracks_existing_window_multi_cue() { + info("Running test: existing window - multi cue"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID, 2); + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + + // Verify multiple active cues + ok(textTracks, "TextTracks container should exist in the pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + is(textTracks.children.length, 2, "Text tracks should load 2 cues"); + }); + } + ); +}); + +/** + * This test ensures that the showHiddenTextTracks override correctly shows + * text tracks with a mode of "hidden". + */ +const prepareHiddenTrackTest = () => + new Promise((resolve, reject) => { + BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + + async browser => { + const videoID = "with-controls"; + await prepareVideosAndWebVTTTracks(browser, videoID, 0, "hidden"); + await SpecialPowers.spawn(browser, [{ videoID }], async args => { + let video = content.document.getElementById(args.videoID); + const tracks = video.textTracks; + Assert.strictEqual( + tracks[0].mode, + "hidden", + "Track 1 mode is 'hidden'" + ); + }); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + if (!pipBrowser) { + reject(); + } + resolve(pipBrowser); + } + ); + }); + +add_task(async function test_hidden_text_tracks_override() { + info("Running test - showHiddenTextTracks"); + + info("hidden mode with override"); + Services.ppmm.sharedData.set(SHARED_DATA_KEY, { + "*://example.com/*": { showHiddenTextTracks: true }, + }); + Services.ppmm.sharedData.flush(); + + await prepareHiddenTrackTest().then(async pipBrowser => { + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + + // Verify text track is showing in PiP window. + ok(textTracks, "TextTracks container should exist in the pip window"); + ok(textTracks.textContent.includes("track 1"), "Track 1 should be shown"); + }); + }); + + info("hidden mode without override"); + Services.ppmm.sharedData.set(SHARED_DATA_KEY, {}); + Services.ppmm.sharedData.flush(); + + await prepareHiddenTrackTest().then(async pipBrowser => { + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + + // Verify text track is [not] showing in PiP window. + ok( + !textTracks || !textTracks.textContent.length, + "Text track should NOT appear in PiP window." + ); + }); + }); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_3.js b/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_3.js new file mode 100644 index 0000000000..3e62556d45 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_text_tracks_webvtt_3.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verifies the value of a cue's .line property. + * @param {Element} browser The <xul:browser> hosting the <video> + * @param {String} videoID The ID of the video being checked + * @param {Integer} trackIndex The index of the track to be loaded + * @param {Integer} cueIndex The index of the cue to be tested on + * @param {Integer|String} expectedValue The expected line value of the cue + */ +async function verifyLineForCue( + browser, + videoID, + trackIndex, + cueIndex, + expectedValue +) { + await SpecialPowers.spawn( + browser, + [{ videoID, trackIndex, cueIndex, expectedValue }], + async args => { + info("Checking .line property values"); + const video = content.document.getElementById(args.videoID); + const activeCues = video.textTracks[args.trackIndex].activeCues; + const vttCueLine = activeCues[args.cueIndex].line; + is(vttCueLine, args.expectedValue, "Cue line should have expected value"); + } + ); +} + +/** + * This test ensures that text tracks appear in expected order if + * VTTCue.line property is auto. + */ +add_task(async function test_text_tracks_new_window_line_auto() { + info("Running test: new window - line auto"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + let trackIndex = 2; + await prepareVideosAndWebVTTTracks(browser, videoID, trackIndex); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await verifyLineForCue(browser, videoID, trackIndex, 0, "auto"); + await verifyLineForCue(browser, videoID, trackIndex, 1, "auto"); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + + let cueDivs = textTracks.children; + + is(cueDivs.length, 2, "There should be 2 active cues"); + // cue1 in this case refers to the first cue to be defined in the vtt file. + // cue2 is therefore the next cue to be defined right after in the vtt file. + ok( + cueDivs[0].textContent.includes("cue 2"), + `cue 2 should be above. Got: ${cueDivs[0].textContent}` + ); + ok( + cueDivs[1].textContent.includes("cue 1"), + `cue 1 should be below. Got: ${cueDivs[1].textContent}` + ); + }); + } + ); +}); + +/** + * This test ensures that text tracks appear in expected order if + * VTTCue.line property is an integer. + */ +add_task(async function test_text_tracks_new_window_line_integer() { + info("Running test: new window - line integer"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + let trackIndex = 3; + await prepareVideosAndWebVTTTracks(browser, videoID, trackIndex); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await verifyLineForCue(browser, videoID, trackIndex, 0, 2); + await verifyLineForCue(browser, videoID, trackIndex, 1, 3); + await verifyLineForCue(browser, videoID, trackIndex, 2, 1); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + + let cueDivs = textTracks.children; + + is(cueDivs.length, 3, "There should be 3 active cues"); + + // cue1 in this case refers to the first cue to be defined in the vtt file. + // cue2 is therefore the next cue to be defined right after in the vtt file. + ok( + cueDivs[0].textContent.includes("cue 3"), + `cue 3 should be above. Got: ${cueDivs[0].textContent}` + ); + ok( + cueDivs[1].textContent.includes("cue 1"), + `cue 1 should be next. Got: ${cueDivs[1].textContent}` + ); + ok( + cueDivs[2].textContent.includes("cue 2"), + `cue 2 should be below. Got: ${cueDivs[2].textContent}` + ); + }); + } + ); +}); + +/** + * This test ensures that text tracks appear in expected order if + * VTTCue.line property is a percentage value. + */ +add_task(async function test_text_tracks_new_window_line_percent() { + info("Running test: new window - line percent"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + let trackIndex = 4; + await prepareVideosAndWebVTTTracks(browser, videoID, trackIndex); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + let pipBrowser = pipWin.document.getElementById("browser"); + + await verifyLineForCue(browser, videoID, trackIndex, 0, 90); + await verifyLineForCue(browser, videoID, trackIndex, 1, 10); + await verifyLineForCue(browser, videoID, trackIndex, 2, 50); + + await SpecialPowers.spawn(pipBrowser, [], async () => { + info("Checking text track content in pip window"); + let textTracks = content.document.getElementById("texttracks"); + ok(textTracks, "TextTracks container should exist in the pip window"); + + await ContentTaskUtils.waitForCondition(() => { + return textTracks.textContent; + }, `Text track is still not visible on the pip window. Got ${textTracks.textContent}`); + + let cueDivs = textTracks.children; + is(cueDivs.length, 3, "There should be 3 active cues"); + + // cue1 in this case refers to the first cue to be defined in the vtt file. + // cue2 is therefore the next cue to be defined right after in the vtt file. + ok( + cueDivs[0].textContent.includes("cue 2"), + `cue 2 should be above. Got: ${cueDivs[0].textContent}` + ); + ok( + cueDivs[1].textContent.includes("cue 3"), + `cue 3 should be next. Got: ${cueDivs[1].textContent}` + ); + ok( + cueDivs[2].textContent.includes("cue 1"), + `cue 1 should be below. Got: ${cueDivs[2].textContent}` + ); + }); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_thirdPartyIframe.js b/toolkit/components/pictureinpicture/tests/browser_thirdPartyIframe.js new file mode 100644 index 0000000000..e02fe21f4e --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_thirdPartyIframe.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const EXAMPLE_COM = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const EXAMPLE_ORG = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" +); +const EXAMPLE_COM_TEST_PAGE = EXAMPLE_COM + "test-page.html"; +const EXAMPLE_ORG_WITH_IFRAME = EXAMPLE_ORG + "test-page-with-iframe.html"; + +/** + * Tests that videos hosted inside of a third-party <iframe> can be opened + * in a Picture-in-Picture window. + */ +add_task(async () => { + for (let videoID of ["with-controls", "no-controls"]) { + info(`Testing ${videoID} case.`); + + await BrowserTestUtils.withNewTab( + { + url: EXAMPLE_ORG_WITH_IFRAME, + gBrowser, + }, + async browser => { + // EXAMPLE_ORG_WITH_IFRAME is hosted at a different domain from + // EXAMPLE_COM_TEST_PAGE, so loading EXAMPLE_COM_TEST_PAGE within + // the iframe will act as our third-party iframe. + await SpecialPowers.spawn( + browser, + [EXAMPLE_COM_TEST_PAGE], + async EXAMPLE_COM_TEST_PAGE => { + let iframe = content.document.getElementById("iframe"); + let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load"); + iframe.src = EXAMPLE_COM_TEST_PAGE; + await loadPromise; + } + ); + + let iframeBc = browser.browsingContext.children[0]; + + if (gFissionBrowser) { + Assert.notEqual( + browser.browsingContext.currentWindowGlobal.osPid, + iframeBc.currentWindowGlobal.osPid, + "The iframe should be running in a different process." + ); + } + + let pipWin = await triggerPictureInPicture(iframeBc, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + await ensureMessageAndClosePiP(iframeBc, videoID, pipWin, true); + + await SimpleTest.promiseFocus(window); + + // Now try using the command / keyboard shortcut + pipWin = await triggerPictureInPicture(iframeBc, videoID, () => { + document.getElementById("View:PictureInPicture").doCommand(); + }); + ok(pipWin, "Got Picture-in-Picture window using command."); + + await ensureMessageAndClosePiP(iframeBc, videoID, pipWin, true); + } + ); + } +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleAfterTabTearOutIn.js b/toolkit/components/pictureinpicture/tests/browser_toggleAfterTabTearOutIn.js new file mode 100644 index 0000000000..5588b3a775 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleAfterTabTearOutIn.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Helper function that tries to use the mouse to open the Picture-in-Picture + * player window for a video with and without the built-in controls. + * + * @param {Element} tab The tab to be tested. + * @return Promise + * @resolves When the toggles for both the video-with-controls and + * video-without-controls have been tested. + */ +async function testToggleForTab(tab) { + for (let videoID of ["with-controls", "no-controls"]) { + let browser = tab.linkedBrowser; + info(`Testing ${videoID} case.`); + + await testToggleHelper(browser, videoID, true); + } +} + +/** + * Tests that the Picture-in-Picture toggle still works after tearing out the + * tab into a new window, or tearing in a tab from one window to another. + */ +add_task(async () => { + // The startingTab will be torn out and placed in the new window. + let startingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE + ); + + // Tear out the starting tab into its own window... + let newWinLoaded = BrowserTestUtils.waitForNewWindow(); + let win2 = gBrowser.replaceTabWithWindow(startingTab); + await newWinLoaded; + + // Let's maximize the newly opened window so we don't have to worry about + // the videos being visible. + if (win2.windowState != win2.STATE_MAXIMIZED) { + let resizePromise = BrowserTestUtils.waitForEvent(win2, "resize"); + win2.maximize(); + await resizePromise; + } + + await SimpleTest.promiseFocus(win2); + await testToggleForTab(win2.gBrowser.selectedTab); + + // Now bring the tab back to the original window. + let dragInTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gBrowser.swapBrowsersAndCloseOther(dragInTab, win2.gBrowser.selectedTab); + await SimpleTest.promiseFocus(window); + await testToggleForTab(dragInTab); + + BrowserTestUtils.removeTab(dragInTab); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleButtonOnNanDuration.js b/toolkit/components/pictureinpicture/tests/browser_toggleButtonOnNanDuration.js new file mode 100644 index 0000000000..6eaa4b5bcd --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleButtonOnNanDuration.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that we do not show the Picture-in-Picture toggle on video + * elements that have a NaN duration. + */ +add_task(async function test_toggleButtonOnNanDuration() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_NAN_VIDEO_DURATION, + gBrowser, + }, + async browser => { + const VIDEO_ID = "test-video"; + + await SpecialPowers.spawn(browser, [VIDEO_ID], async videoID => { + let video = content.document.getElementById(videoID); + if (video.readyState < content.HTMLMediaElement.HAVE_ENOUGH_DATA) { + info(`Waiting for 'canplaythrough' for ${videoID}`); + await ContentTaskUtils.waitForEvent(video, "canplaythrough"); + } + }); + + await testToggleHelper(browser, "nan-duration", false); + + await testToggleHelper(browser, "test-video", true); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleButtonOverlay.js b/toolkit/components/pictureinpicture/tests/browser_toggleButtonOverlay.js new file mode 100644 index 0000000000..6f81075770 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleButtonOverlay.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the Picture-in-Picture toggle can be clicked when overlaid + * with a transparent button, but not clicked when overlaid with an + * opaque button. + */ +add_task(async () => { + const PAGE = TEST_ROOT + "test-button-overlay.html"; + await testToggle(PAGE, { + "video-partial-transparent-button": { canToggle: true }, + "video-opaque-button": { canToggle: false }, + }); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleMode_2.js b/toolkit/components/pictureinpicture/tests/browser_toggleMode_2.js new file mode 100644 index 0000000000..a845137a69 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleMode_2.js @@ -0,0 +1,204 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * See the documentation for the DEFAULT_TOGGLE_STYLES object in head.js + * for a description of what these toggle style objects are representing. + */ +const TOGGLE_STYLES_LEFT_EXPLAINER = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-small": 0.0, + ".pip-wrapper": DEFAULT_TOGGLE_OPACITY, + ".pip-expanded": 1.0, + }, + hidden: [".pip-icon-label > .pip-icon"], + }, + + hoverToggle: { + opacities: { + ".pip-small": 0.0, + ".pip-wrapper": 1.0, + ".pip-expanded": 1.0, + }, + hidden: [".pip-icon-label > .pip-icon"], + }, + }, +}; + +const TOGGLE_STYLES_RIGHT_EXPLAINER = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-small": 0.0, + ".pip-wrapper": DEFAULT_TOGGLE_OPACITY, + ".pip-expanded": 1.0, + }, + hidden: [".pip-wrapper > .pip-icon"], + }, + + hoverToggle: { + opacities: { + ".pip-small": 0.0, + ".pip-wrapper": 1.0, + ".pip-expanded": 1.0, + }, + hidden: [".pip-wrapper > .pip-icon"], + }, + }, +}; + +const TOGGLE_STYLES_LEFT_SMALL = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-wrapper": DEFAULT_TOGGLE_OPACITY, + }, + hidden: [".pip-expanded"], + }, + + hoverToggle: { + opacities: { + ".pip-wrapper": 1.0, + }, + hidden: [".pip-expanded"], + }, + }, +}; + +const TOGGLE_STYLES_RIGHT_SMALL = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-wrapper": DEFAULT_TOGGLE_OPACITY, + }, + hidden: [".pip-expanded"], + }, + + hoverToggle: { + opacities: { + ".pip-wrapper": 1.0, + }, + hidden: [".pip-expanded"], + }, + }, +}; + +/** + * Tests the Mode 2 variation of the Picture-in-Picture toggle in both the + * left and right positions, when the user is in the state where they've never + * clicked on the Picture-in-Picture toggle before (since we show a more detailed + * toggle in that state). + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.videocontrols.picture-in-picture.video-toggle.position", "left"], + [HAS_USED_PREF, false], + ], + }); + + await testToggle(TEST_PAGE, { + "with-controls": { + canToggle: true, + toggleStyles: TOGGLE_STYLES_LEFT_EXPLAINER, + }, + }); + + Assert.ok( + Services.prefs.getBoolPref(HAS_USED_PREF, false), + "Entered has-used mode." + ); + Services.prefs.clearUserPref(HAS_USED_PREF); + + await testToggle(TEST_PAGE, { + "no-controls": { + canToggle: true, + toggleStyles: TOGGLE_STYLES_LEFT_EXPLAINER, + shouldSeeClickEventAfterToggle: true, + }, + }); + + Assert.ok( + Services.prefs.getBoolPref(HAS_USED_PREF, false), + "Entered has-used mode." + ); + Services.prefs.clearUserPref(HAS_USED_PREF); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.videocontrols.picture-in-picture.video-toggle.position", "right"], + ], + }); + + await testToggle(TEST_PAGE, { + "with-controls": { + canToggle: true, + toggleStyles: TOGGLE_STYLES_RIGHT_EXPLAINER, + }, + }); + + Assert.ok( + Services.prefs.getBoolPref(HAS_USED_PREF, false), + "Entered has-used mode." + ); + Services.prefs.clearUserPref(HAS_USED_PREF); + + await testToggle(TEST_PAGE, { + "no-controls": { + canToggle: true, + toggleStyles: TOGGLE_STYLES_RIGHT_EXPLAINER, + shouldSeeClickEventAfterToggle: true, + }, + }); + + Assert.ok( + Services.prefs.getBoolPref(HAS_USED_PREF, false), + "Entered has-used mode." + ); + Services.prefs.clearUserPref(HAS_USED_PREF); +}); + +/** + * Tests the Mode 2 variation of the Picture-in-Picture toggle in both the + * left and right positions, when the user is in the state where they've + * used the Picture-in-Picture feature before. + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.videocontrols.picture-in-picture.video-toggle.mode", 2], + ["media.videocontrols.picture-in-picture.video-toggle.position", "left"], + [HAS_USED_PREF, true], + ], + }); + + await testToggle(TEST_PAGE, { + "with-controls": { + canToggle: true, + toggleStyles: TOGGLE_STYLES_LEFT_SMALL, + }, + "no-controls": { canToggle: true, toggleStyles: TOGGLE_STYLES_LEFT_SMALL }, + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.videocontrols.picture-in-picture.video-toggle.position", "right"], + ], + }); + + await testToggle(TEST_PAGE, { + "with-controls": { + canToggle: true, + toggleStyles: TOGGLE_STYLES_LEFT_SMALL, + }, + "no-controls": { canToggle: true, toggleStyles: TOGGLE_STYLES_LEFT_SMALL }, + }); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleOnInsertedVideo.js b/toolkit/components/pictureinpicture/tests/browser_toggleOnInsertedVideo.js new file mode 100644 index 0000000000..36172feeb4 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleOnInsertedVideo.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the Picture-in-Picture toggle correctly attaches itself when the + * video element has been inserted into the DOM after the video is ready to + * play. + */ +add_task(async () => { + const PAGE = TEST_ROOT + "test-page.html"; + + await testToggle( + PAGE, + { + inserted: { canToggle: true }, + }, + async browser => { + await SpecialPowers.spawn(browser, [], async () => { + let doc = content.document; + + // To avoid issues with the video not being scrolled into view, get + // rid of the other videos on the page. + let preExistingVideos = doc.querySelectorAll("video"); + for (let video of preExistingVideos) { + video.remove(); + } + + let newVideo = doc.createElement("video"); + const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" + ); + let ready = ContentTaskUtils.waitForEvent(newVideo, "canplay"); + newVideo.src = "test-video.mp4"; + newVideo.id = "inserted"; + await ready; + doc.body.appendChild(newVideo); + }); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleOpaqueOverlay.js b/toolkit/components/pictureinpicture/tests/browser_toggleOpaqueOverlay.js new file mode 100644 index 0000000000..18c906bf20 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleOpaqueOverlay.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the Picture-in-Picture toggle is not clickable when + * overlaid with opaque elements. + */ +add_task(async () => { + const PAGE = TEST_ROOT + "test-opaque-overlay.html"; + await testToggle(PAGE, { + "video-full-opacity": { canToggle: false }, + "video-full-opacity-over-toggle": { canToggle: false }, + }); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_togglePointerEventsNone.js b/toolkit/components/pictureinpicture/tests/browser_togglePointerEventsNone.js new file mode 100644 index 0000000000..23cc393960 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_togglePointerEventsNone.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the Picture-in-Picture toggle is clickable even if the + * video element has pointer-events: none. + */ +add_task(async () => { + const PAGE = TEST_ROOT + "test-pointer-events-none.html"; + await testToggle(PAGE, { + "with-controls": { canToggle: true }, + "no-controls": { canToggle: true }, + }); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_togglePolicies.js b/toolkit/components/pictureinpicture/tests/browser_togglePolicies.js new file mode 100644 index 0000000000..a95bbb0d48 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_togglePolicies.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that by setting a Picture-in-Picture toggle position policy + * in the sharedData structure, that the toggle position can be + * change for a particular URI. + */ +add_task(async () => { + let positionPolicies = [ + TOGGLE_POLICIES.TOP, + TOGGLE_POLICIES.ONE_QUARTER, + TOGGLE_POLICIES.MIDDLE, + TOGGLE_POLICIES.THREE_QUARTERS, + TOGGLE_POLICIES.BOTTOM, + ]; + + for (let policy of positionPolicies) { + Services.ppmm.sharedData.set(SHARED_DATA_KEY, { + "*://example.com/*": { policy }, + }); + Services.ppmm.sharedData.flush(); + + let expectations = { + "with-controls": { canToggle: true, policy }, + "no-controls": { canToggle: true, policy }, + }; + + // For <video> elements with controls, the video controls overlap the + // toggle when its on the bottom and can't be clicked, so we'll ignore + // that case. + if (policy == TOGGLE_POLICIES.BOTTOM) { + expectations["with-controls"] = { canToggle: true }; + } + + await testToggle(TEST_PAGE, expectations); + + // And ensure that other pages aren't affected by this override. + await testToggle(TEST_PAGE_2, { + "with-controls": { canToggle: true }, + "no-controls": { canToggle: true }, + }); + } + + Services.ppmm.sharedData.set(SHARED_DATA_KEY, {}); + Services.ppmm.sharedData.flush(); +}); + +/** + * Tests that by setting a Picture-in-Picture toggle hidden policy + * in the sharedData structure, that the toggle can be suppressed. + */ +add_task(async () => { + Services.ppmm.sharedData.set(SHARED_DATA_KEY, { + "*://example.com/*": { policy: TOGGLE_POLICIES.HIDDEN }, + }); + Services.ppmm.sharedData.flush(); + + await testToggle(TEST_PAGE, { + "with-controls": { canToggle: false, policy: TOGGLE_POLICIES.HIDDEN }, + "no-controls": { canToggle: false, policy: TOGGLE_POLICIES.HIDDEN }, + }); + + // And ensure that other pages aren't affected by this override. + await testToggle(TEST_PAGE_2, { + "with-controls": { canToggle: true }, + "no-controls": { canToggle: true }, + }); + + Services.ppmm.sharedData.set(SHARED_DATA_KEY, {}); + Services.ppmm.sharedData.flush(); +}); + +/** + * Tests that policies are re-evaluated if the page URI is transitioned + * via the history API. + */ +add_task(async () => { + Services.ppmm.sharedData.set(SHARED_DATA_KEY, { + "*://example.com/*/test-page.html": { policy: TOGGLE_POLICIES.HIDDEN }, + }); + Services.ppmm.sharedData.flush(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + await ensureVideosReady(browser); + await SimpleTest.promiseFocus(browser); + + await testToggleHelper( + browser, + "no-controls", + false, + TOGGLE_POLICIES.HIDDEN + ); + + await SpecialPowers.spawn(browser, [], async function () { + content.history.pushState({}, "2", "otherpage.html"); + }); + + // Since we no longer match the policy URI, we should be able + // to use the Picture-in-Picture toggle. + await testToggleHelper(browser, "no-controls", true); + + // Now use the history API to put us back at the original location, + // which should have the HIDDEN policy re-applied. + await SpecialPowers.spawn(browser, [], async function () { + content.history.pushState({}, "Return", "test-page.html"); + }); + + await testToggleHelper( + browser, + "no-controls", + false, + TOGGLE_POLICIES.HIDDEN + ); + } + ); + + Services.ppmm.sharedData.set(SHARED_DATA_KEY, {}); + Services.ppmm.sharedData.flush(); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_togglePositionChange.js b/toolkit/components/pictureinpicture/tests/browser_togglePositionChange.js new file mode 100644 index 0000000000..a868bc3d71 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_togglePositionChange.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests that Picture-in-Picture "move toggle" context menu item + * successfully changes preference. + */ +add_task(async () => { + let videoID = "with-controls"; + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + await SpecialPowers.spawn(browser, [videoID], async videoID => { + await content.document.getElementById(videoID).play(); + }); + + const TOGGLE_POSITION_PREF = + "media.videocontrols.picture-in-picture.video-toggle.position"; + const TOGGLE_POSITION_RIGHT = "right"; + const TOGGLE_POSITION_LEFT = "left"; + + await SpecialPowers.pushPrefEnv({ + set: [[TOGGLE_POSITION_PREF, TOGGLE_POSITION_RIGHT]], + }); + + let contextMoveToggle = document.getElementById( + "context_MovePictureInPictureToggle" + ); + contextMoveToggle.click(); + let position = Services.prefs.getStringPref( + TOGGLE_POSITION_PREF, + TOGGLE_POSITION_RIGHT + ); + + Assert.ok( + position === TOGGLE_POSITION_LEFT, + "Picture-in-Picture toggle position value should be 'left'." + ); + + contextMoveToggle.click(); + position = Services.prefs.getStringPref( + TOGGLE_POSITION_PREF, + TOGGLE_POSITION_RIGHT + ); + + Assert.ok( + position === TOGGLE_POSITION_RIGHT, + "Picture-in-Picture toggle position value should be 'right'." + ); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleSimple.js b/toolkit/components/pictureinpicture/tests/browser_toggleSimple.js new file mode 100644 index 0000000000..a9351816fa --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleSimple.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that we show the Picture-in-Picture toggle on video + * elements when hovering them with the mouse cursor, and that + * clicking on them causes the Picture-in-Picture window to + * open if the toggle isn't being occluded. This test tests videos + * both with and without controls. + */ +add_task(async () => { + await testToggle(TEST_PAGE, { + "with-controls": { canToggle: true }, + "no-controls": { canToggle: true }, + }); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-1.js b/toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-1.js new file mode 100644 index 0000000000..658bb0f362 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-1.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the Picture-in-Picture toggle can appear and be clicked + * when the video is overlaid with transparent elements. Also tests the + * site-specific toggle visibility threshold to ensure that we can + * configure opacities that can't be clicked through. + */ +add_task(async () => { + const PAGE = TEST_ROOT + "test-transparent-overlay-1.html"; + await testToggle(PAGE, { + "video-transparent-background": { canToggle: true }, + "video-alpha-background": { canToggle: true }, + }); + + // Now set a toggle visibility threshold to 0.4 and ensure that the + // partially obscured toggle can't be clicked. + Services.ppmm.sharedData.set(SHARED_DATA_KEY, { + "*://example.com/*": { visibilityThreshold: 0.4 }, + }); + Services.ppmm.sharedData.flush(); + + await testToggle(PAGE, { + "video-transparent-background": { canToggle: true }, + "video-alpha-background": { canToggle: false }, + }); + + Services.ppmm.sharedData.set(SHARED_DATA_KEY, {}); + Services.ppmm.sharedData.flush(); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-2.js b/toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-2.js new file mode 100644 index 0000000000..b425b50d1c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-2.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the Picture-in-Picture toggle can appear and be clicked + * when the video is overlaid with elements that have zero and partial + * opacity. Also tests the site-specific toggle visibility threshold to + * ensure that we can configure opacities that can't be clicked through. + */ +add_task(async () => { + const PAGE = TEST_ROOT + "test-transparent-overlay-2.html"; + await testToggle(PAGE, { + "video-zero-opacity": { canToggle: true }, + "video-partial-opacity": { canToggle: true }, + }); + + // Now set a toggle visibility threshold to 0.4 and ensure that the + // partially obscured toggle can't be clicked. + Services.ppmm.sharedData.set(SHARED_DATA_KEY, { + "*://example.com/*": { visibilityThreshold: 0.4 }, + }); + Services.ppmm.sharedData.flush(); + + await testToggle(PAGE, { + "video-zero-opacity": { canToggle: true }, + "video-partial-opacity": { canToggle: false }, + }); + + Services.ppmm.sharedData.set(SHARED_DATA_KEY, {}); + Services.ppmm.sharedData.flush(); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggle_enabled.js b/toolkit/components/pictureinpicture/tests/browser_toggle_enabled.js new file mode 100644 index 0000000000..197bb9357c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggle_enabled.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const videoID = "with-controls"; +const TOGGLE_ENABLED_PREF = + "media.videocontrols.picture-in-picture.video-toggle.enabled"; +const PIP_ENABLED_PREF = "media.videocontrols.picture-in-picture.enabled"; +const ACCEPTABLE_DIFF = 1; + +function checkDifference(actual, expected) { + let diff = Math.abs(actual - expected); + return diff <= ACCEPTABLE_DIFF; +} + +function isVideoRect(videoRect, rect) { + info( + "Video rect and toggle rect will be the same if the toggle doesn't show" + ); + info(`Video rect: ${JSON.stringify(videoRect)}`); + info(`Toggle rect: ${JSON.stringify(rect)}`); + return ( + checkDifference(videoRect.top, rect.top) && + checkDifference(videoRect.left, rect.left) && + checkDifference(videoRect.width, rect.width) && + checkDifference(videoRect.height, rect.height) + ); +} + +/** + * Tests if the toggle is available depending on prefs + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let videoRect = await SpecialPowers.spawn( + browser, + [videoID], + async videoID => { + let video = content.document.getElementById(videoID); + let rect = video.getBoundingClientRect(); + + return { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + }; + } + ); + // both toggle and pip true + await SpecialPowers.pushPrefEnv({ + set: [ + [TOGGLE_ENABLED_PREF, true], + [PIP_ENABLED_PREF, true], + ], + }); + + let rect = await getToggleClientRect(browser, videoID); + + Assert.ok(!isVideoRect(videoRect, rect), "Toggle is showing"); + + // only toggle false + await SpecialPowers.pushPrefEnv({ + set: [[TOGGLE_ENABLED_PREF, false]], + }); + + rect = await getToggleClientRect(browser, videoID); + Assert.ok(isVideoRect(videoRect, rect), "The toggle is not showing"); + + // only pip false + await SpecialPowers.pushPrefEnv({ + set: [ + [TOGGLE_ENABLED_PREF, true], + [PIP_ENABLED_PREF, false], + ], + }); + + rect = await getToggleClientRect(browser, videoID); + Assert.ok(isVideoRect(videoRect, rect), "The toggle is not showing"); + + // both toggle and pip false + await SpecialPowers.pushPrefEnv({ + set: [ + [TOGGLE_ENABLED_PREF, false], + [PIP_ENABLED_PREF, false], + ], + }); + + rect = await getToggleClientRect(browser, videoID); + Assert.ok(isVideoRect(videoRect, rect), "The toggle is not showing"); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggle_videocontrols.js b/toolkit/components/pictureinpicture/tests/browser_toggle_videocontrols.js new file mode 100644 index 0000000000..248f816fba --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggle_videocontrols.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the Picture-in-Picture toggle is hidden when opening the closed captions menu + * and is visible when closing the closed captions menu. + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ["media.videocontrols.picture-in-picture.video-toggle.enabled", true], + ], + }); + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID, -1); + await prepareForToggleClick(browser, videoID); + + let args = { + videoID, + DEFAULT_TOGGLE_STYLES, + }; + + await SpecialPowers.spawn(browser, [args], async args => { + let { videoID, DEFAULT_TOGGLE_STYLES } = args; + let video = this.content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let closedCaptionButton = shadowRoot.querySelector( + "#closedCaptionButton" + ); + let toggle = shadowRoot.querySelector( + `#${DEFAULT_TOGGLE_STYLES.rootID}` + ); + let textTrackListContainer = shadowRoot.querySelector( + "#textTrackListContainer" + ); + + Assert.ok(!toggle.hidden, "Toggle should be visible"); + Assert.ok( + textTrackListContainer.hidden, + "textTrackListContainer should be hidden" + ); + + info("Opening text track list from cc button"); + closedCaptionButton.click(); + + Assert.ok(toggle.hidden, "Toggle should be hidden"); + Assert.ok( + !textTrackListContainer.hidden, + "textTrackListContainer should be visible" + ); + + info("Clicking the cc button again to close it"); + closedCaptionButton.click(); + + Assert.ok(!toggle.hidden, "Toggle should be visible again"); + Assert.ok( + textTrackListContainer.hidden, + "textTrackListContainer should be hidden again" + ); + }); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_toggle_without_audio.js b/toolkit/components/pictureinpicture/tests/browser_toggle_without_audio.js new file mode 100644 index 0000000000..0c9ca5eeba --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggle_without_audio.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const videoID = "without-audio"; +const MIN_DURATION_PREF = + "media.videocontrols.picture-in-picture.video-toggle.min-video-secs"; +const ALWAYS_SHOW_PREF = + "media.videocontrols.picture-in-picture.video-toggle.always-show"; +const ACCEPTABLE_DIFF = 1; + +function checkDifference(actual, expected) { + let diff = Math.abs(actual - expected); + return diff <= ACCEPTABLE_DIFF; +} + +function isVideoRect(videoRect, rect) { + info( + "Video rect and toggle rect will be the same if the toggle doesn't show" + ); + info(`Video rect: ${JSON.stringify(videoRect)}`); + info(`Toggle rect: ${JSON.stringify(rect)}`); + return ( + checkDifference(videoRect.top, rect.top) && + checkDifference(videoRect.left, rect.left) && + checkDifference(videoRect.width, rect.width) && + checkDifference(videoRect.height, rect.height) + ); +} + +/** + * Tests if the toggle is available for a video without an audio track + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE_WITHOUT_AUDIO, + }, + async browser => { + let videoRect = await SpecialPowers.spawn( + browser, + [videoID], + async videoID => { + let video = content.document.getElementById(videoID); + Assert.ok(!video.mozHasAudio, "Video does not have an audio track"); + let rect = video.getBoundingClientRect(); + + return { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + }; + } + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ALWAYS_SHOW_PREF, false], // don't always show, we're testing the display logic + [MIN_DURATION_PREF, 3], // sample video is only 4s + ], + }); + + let rect = await getToggleClientRect(browser, videoID); + + Assert.ok(!isVideoRect(videoRect, rect), "Toggle is showing"); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_touch_toggle_enablepip.js b/toolkit/components/pictureinpicture/tests/browser_touch_toggle_enablepip.js new file mode 100644 index 0000000000..e50c430d0c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_touch_toggle_enablepip.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that Picture-in-Picture intializes without changes to video playback + * when opened via the toggle using a touch event. Also ensures that elements + * in the content window can still be interacted with afterwards. + */ +add_task(async () => { + let videoID = "with-controls"; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.videocontrols.picture-in-picture.video-toggle.position", "right"], + ], + }); + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + let toggleStyles = DEFAULT_TOGGLE_STYLES; + let stage = "hoverVideo"; + let toggleStylesForStage = toggleStyles.stages[stage]; + + // Remove other page elements before reading PiP toggle's client rect. + // Otherwise, we will provide the wrong coordinates when simulating the touch event. + await SpecialPowers.spawn(browser, [], async args => { + info( + "Removing other elements first to make the PiP toggle more visible" + ); + this.content.document.getElementById("no-controls").remove(); + let otherEls = this.content.document.querySelectorAll("h1"); + for (let el of otherEls) { + el.remove(); + } + }); + + let toggleClientRect = await getToggleClientRect(browser, videoID); + await prepareForToggleClick(browser, videoID); + + await SpecialPowers.spawn( + browser, + [{ videoID, toggleClientRect, toggleStylesForStage }], + async args => { + // waitForToggleOpacity is based on toggleOpacityReachesThreshold. + // Waits for toggle to reach target opacity. + async function waitForToggleOpacity( + shadowRoot, + toggleStylesForStage + ) { + for (let hiddenElement of toggleStylesForStage.hidden) { + let el = shadowRoot.querySelector(hiddenElement); + ok( + ContentTaskUtils.isHidden(el), + `Expected ${hiddenElement} to be hidden.` + ); + } + + for (let opacityElement in toggleStylesForStage.opacities) { + let opacityThreshold = + toggleStylesForStage.opacities[opacityElement]; + let el = shadowRoot.querySelector(opacityElement); + + await ContentTaskUtils.waitForCondition( + () => { + let opacity = parseFloat( + this.content.getComputedStyle(el).opacity + ); + return opacity >= opacityThreshold; + }, + `Toggle element ${opacityElement} should have eventually reached ` + + `target opacity ${opacityThreshold}`, + 100, + 100 + ); + + ok(true, "Toggle reached target opacity."); + } + } + let { videoID, toggleClientRect, toggleStylesForStage } = args; + let video = this.content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + + info("Creating a new button in the content window"); + let button = this.content.document.createElement("button"); + let buttonSelected = false; + button.ontouchstart = () => { + info("Button selected via touch event"); + buttonSelected = true; + return true; + }; + button.id = "testbutton"; + button.style.backgroundColor = "red"; + // Move button to the top for better visibility. + button.style.position = "absolute"; + button.style.top = "0"; + button.textContent = "test button"; + this.content.document.body.appendChild(button); + + await video.play(); + + info("Hover over the video to show the Picture-in-Picture toggle"); + await EventUtils.synthesizeMouseAtCenter( + video, + { type: "mousemove" }, + this.content.window + ); + await EventUtils.synthesizeMouseAtCenter( + video, + { type: "mouseover" }, + this.content.window + ); + + let toggleCenterX = + toggleClientRect.left + toggleClientRect.width / 2; + let toggleCenterY = + toggleClientRect.top + toggleClientRect.height / 2; + + // We want to wait for the toggle to reach opacity so that we can select it. + info("Waiting for toggle to become fully visible"); + await waitForToggleOpacity(shadowRoot, toggleStylesForStage); + + info("Simulating touch event on PiP toggle"); + let utils = EventUtils._getDOMWindowUtils(this.content.window); + let id = utils.DEFAULT_TOUCH_POINTER_ID; + let rx = 1; + let ry = 1; + let angle = 0; + let force = 1; + let tiltX = 0; + let tiltY = 0; + let twist = 0; + + let defaultPrevented = utils.sendTouchEvent( + "touchstart", + [id], + [toggleCenterX], + [toggleCenterY], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + false /* modifiers */ + ); + utils.sendTouchEvent( + "touchend", + [id], + [toggleCenterX], + [toggleCenterY], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + false /* modifiers */ + ); + + ok( + defaultPrevented, + "Touchstart event's default actions should be prevented" + ); + ok(!video.paused, "Video should still be playing"); + + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + + let testButton = this.content.document.getElementById("testbutton"); + let buttonRect = testButton.getBoundingClientRect(); + let buttonCenterX = buttonRect.left + buttonRect.width / 2; + let buttonCenterY = buttonRect.top + buttonRect.height / 2; + + info("Simulating touch event on new button"); + defaultPrevented = utils.sendTouchEvent( + "touchstart", + [id], + [buttonCenterX], + [buttonCenterY], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + false /* modifiers */ + ); + utils.sendTouchEvent( + "touchend", + [id], + [buttonCenterX], + [buttonCenterY], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + false /* modifiers */ + ); + + ok( + buttonSelected, + "Button in content window was selected via touchstart" + ); + ok( + !defaultPrevented, + "Touchstart event's default actions should no longer be prevented" + ); + } + ); + + try { + info("Picture-in-Picture window should open"); + await BrowserTestUtils.waitForCondition( + () => Services.wm.getEnumerator(WINDOW_TYPE).hasMoreElements(), + "Found a Picture-in-Picture" + ); + for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) { + if (!win.closed) { + ok(true, "Found a Picture-in-Picture window as expected"); + win.close(); + } + } + } catch { + ok(false, "No Picture-in-Picture window found, which is unexpected"); + } + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_urlbar_toggle.js b/toolkit/components/pictureinpicture/tests/browser_urlbar_toggle.js new file mode 100644 index 0000000000..cf808c5177 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_urlbar_toggle.js @@ -0,0 +1,456 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const { ASRouter } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouter.sys.mjs" +); + +const PIP_URLBAR_EVENTS = [ + { + category: "pictureinpicture", + method: "opened_method", + object: "urlBar", + }, +]; + +const PIP_DISABLED_EVENTS = [ + { + category: "pictureinpicture", + method: "opened_method", + object: "urlBar", + extra: { disableDialog: "true" }, + }, + { + category: "pictureinpicture", + method: "disrespect_disable", + object: "urlBar", + }, +]; + +const FIRST_TOGGLE_AFTER_CALLOUT_EXPECTED_EVENTS = [ + { + category: "pictureinpicture", + method: "opened_method", + object: "urlBar", + extra: { firstTimeToggle: "true", callout: "true" }, + }, +]; + +add_task(async function test_urlbar_toggle_multiple_contexts() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_MULTIPLE_CONTEXTS, + gBrowser, + }, + async browser => { + Services.telemetry.clearEvents(); + await ensureVideosReady(browser); + await ensureVideosReady(browser.browsingContext.children[0]); + + await TestUtils.waitForCondition( + () => + PictureInPicture.getEligiblePipVideoCount(browser).totalPipCount === + 2, + "Waiting for videos to register" + ); + + let { totalPipCount } = + PictureInPicture.getEligiblePipVideoCount(browser); + is(totalPipCount, 2, "Total PiP count is 2"); + + let pipUrlbarToggle = document.getElementById( + "picture-in-picture-button" + ); + ok( + BrowserTestUtils.isHidden(pipUrlbarToggle), + "PiP urlbar toggle is hidden because there is more than 1 video" + ); + + // Remove one video from page so urlbar toggle will show + await SpecialPowers.spawn(browser, [], async () => { + let video = content.document.getElementById("with-controls"); + video.remove(); + }); + + await BrowserTestUtils.waitForMutationCondition( + pipUrlbarToggle, + { attributeFilter: ["hidden"] }, + () => BrowserTestUtils.isVisible(pipUrlbarToggle) + ); + + ok( + BrowserTestUtils.isVisible(pipUrlbarToggle), + "PiP urlbar toggle is visible" + ); + + ({ totalPipCount } = PictureInPicture.getEligiblePipVideoCount(browser)); + is(totalPipCount, 1, "Total PiP count is 1"); + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + pipUrlbarToggle.click(); + let win = await domWindowOpened; + ok(win, "A Picture-in-Picture window opened."); + + await assertVideoIsBeingCloned( + browser.browsingContext.children[0], + "video" + ); + + let filter = { + category: "pictureinpicture", + method: "opened_method", + object: "urlBar", + }; + await waitForTelemeryEvents(filter, PIP_URLBAR_EVENTS.length, "content"); + + TelemetryTestUtils.assertEvents(PIP_URLBAR_EVENTS, filter, { + clear: true, + process: "content", + }); + + let domWindowClosed = BrowserTestUtils.domWindowClosed(win); + pipUrlbarToggle.click(); + await domWindowClosed; + + await SpecialPowers.spawn(browser, [], async () => { + let iframe = content.document.getElementById("iframe"); + iframe.remove(); + }); + + await BrowserTestUtils.waitForMutationCondition( + pipUrlbarToggle, + { attributeFilter: ["hidden"] }, + () => BrowserTestUtils.isHidden(pipUrlbarToggle) + ); + + ok( + BrowserTestUtils.isHidden(pipUrlbarToggle), + "PiP urlbar toggle is hidden because there are no videos on the page" + ); + + ({ totalPipCount } = PictureInPicture.getEligiblePipVideoCount(browser)); + is(totalPipCount, 0, "Total PiP count is 0"); + } + ); +}); + +add_task(async function test_urlbar_toggle_switch_tabs() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_TRANSPARENT_NESTED_IFRAMES, + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + await TestUtils.waitForCondition( + () => + PictureInPicture.getEligiblePipVideoCount(browser).totalPipCount === + 1, + "Waiting for video to register" + ); + + let { totalPipCount } = + PictureInPicture.getEligiblePipVideoCount(browser); + is(totalPipCount, 1, "Total PiP count is 1"); + + let pipUrlbarToggle = document.getElementById( + "picture-in-picture-button" + ); + ok( + BrowserTestUtils.isVisible(pipUrlbarToggle), + "PiP urlbar toggle is visible because there is 1 video" + ); + + let pipActivePromise = BrowserTestUtils.waitForMutationCondition( + pipUrlbarToggle, + { attributeFilter: ["pipactive"] }, + () => pipUrlbarToggle.hasAttribute("pipactive") + ); + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + pipUrlbarToggle.click(); + let win = await domWindowOpened; + ok(win, "A Picture-in-Picture window opened."); + + await assertVideoIsBeingCloned(browser, "video"); + + await pipActivePromise; + + ok( + pipUrlbarToggle.hasAttribute("pipactive"), + "We are PiP'd in this tab so the icon is active" + ); + + let newTab = BrowserTestUtils.addTab( + gBrowser, + TEST_PAGE_TRANSPARENT_NESTED_IFRAMES + ); + await BrowserTestUtils.browserLoaded(newTab.linkedBrowser); + + await BrowserTestUtils.switchTab(gBrowser, newTab); + + await BrowserTestUtils.waitForMutationCondition( + pipUrlbarToggle, + { attributeFilter: ["pipactive"] }, + () => !pipUrlbarToggle.hasAttribute("pipactive") + ); + + ok( + !pipUrlbarToggle.hasAttribute("pipactive"), + "After switching tabs the pip icon is not active" + ); + + BrowserTestUtils.removeTab(newTab); + + await ensureMessageAndClosePiP( + browser, + "video-transparent-background", + win, + false + ); + } + ); +}); + +add_task(async function test_pipDisabled() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_PIP_DISABLED, + gBrowser, + }, + async browser => { + Services.telemetry.clearEvents(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.respect-disablePictureInPicture", + true, + ], + ], + }); + + const VIDEO_ID = "with-controls"; + await ensureVideosReady(browser); + + await TestUtils.waitForCondition( + () => + PictureInPicture.getEligiblePipVideoCount(browser).totalPipCount === + 1, + "Waiting for video to register" + ); + + let { totalPipCount, totalPipDisabled } = + PictureInPicture.getEligiblePipVideoCount(browser); + is(totalPipCount, 1, "Total PiP count is 1"); + is(totalPipDisabled, 1, "PiP is disabled on 1 video"); + + // Confirm that the toggle is hidden because we are respecting disablePictureInPicture + await testToggleHelper(browser, VIDEO_ID, false); + + let pipUrlbarToggle = document.getElementById( + "picture-in-picture-button" + ); + ok( + BrowserTestUtils.isVisible(pipUrlbarToggle), + "PiP urlbar toggle is visible because PiP is disabled" + ); + + pipUrlbarToggle.click(); + + let panel = browser.ownerDocument.querySelector("#PictureInPicturePanel"); + await BrowserTestUtils.waitForCondition(async () => { + if (!panel) { + panel = browser.ownerDocument.querySelector("#PictureInPicturePanel"); + } + return BrowserTestUtils.isVisible(panel); + }); + + let respectPipDisableSwitch = panel.querySelector( + "#respect-pipDisabled-switch" + ); + ok( + !respectPipDisableSwitch.pressed, + "Respect PiP disabled is not pressed" + ); + + EventUtils.synthesizeMouseAtCenter(respectPipDisableSwitch.buttonEl, {}); + await BrowserTestUtils.waitForEvent(respectPipDisableSwitch, "toggle"); + ok(respectPipDisableSwitch.pressed, "Respect PiP disabled is pressed"); + + pipUrlbarToggle.click(); + + await BrowserTestUtils.waitForCondition(async () => { + return BrowserTestUtils.isHidden(panel); + }); + + let filter = { + category: "pictureinpicture", + object: "urlBar", + }; + await waitForTelemeryEvents(filter, PIP_DISABLED_EVENTS.length, "parent"); + TelemetryTestUtils.assertEvents(PIP_DISABLED_EVENTS, filter, { + clear: true, + process: "parent", + }); + + // Confirm that the toggle is now visible because we no longer respect disablePictureInPicture + await testToggleHelper(browser, VIDEO_ID, true); + + let pipActivePromise = BrowserTestUtils.waitForMutationCondition( + pipUrlbarToggle, + { attributeFilter: ["pipactive"] }, + () => pipUrlbarToggle.hasAttribute("pipactive") + ); + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + pipUrlbarToggle.click(); + let win = await domWindowOpened; + ok(win, "A Picture-in-Picture window opened."); + + await assertVideoIsBeingCloned(browser, "video"); + + await pipActivePromise; + + ok( + pipUrlbarToggle.hasAttribute("pipactive"), + "We are PiP'd in this tab so the icon is active" + ); + + let domWindowClosed = BrowserTestUtils.domWindowClosed(win); + pipUrlbarToggle.click(); + await domWindowClosed; + + await BrowserTestUtils.waitForMutationCondition( + pipUrlbarToggle, + { attributeFilter: ["pipactive"] }, + () => !pipUrlbarToggle.hasAttribute("pipactive") + ); + + ok( + !pipUrlbarToggle.hasAttribute("pipactive"), + "We closed the PiP window so the urlbar button is no longer active" + ); + + await SpecialPowers.popPrefEnv(); + } + ); +}); + +add_task(async function test_urlbar_toggle_telemetry() { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_MULTIPLE_CONTEXTS, + gBrowser, + }, + async browser => { + Services.telemetry.clearEvents(); + await ensureVideosReady(browser); + await ensureVideosReady(browser.browsingContext.children[0]); + + await TestUtils.waitForCondition( + () => + PictureInPicture.getEligiblePipVideoCount(browser).totalPipCount === + 2, + "Waiting for videos to register" + ); + + let { totalPipCount } = + PictureInPicture.getEligiblePipVideoCount(browser); + is(totalPipCount, 2, "Total PiP count is 2"); + + let pipUrlbarToggle = document.getElementById( + "picture-in-picture-button" + ); + ok( + BrowserTestUtils.isHidden(pipUrlbarToggle), + "PiP urlbar toggle is hidden because there is more than 1 video" + ); + + // Remove one video from page so urlbar toggle will show + await SpecialPowers.spawn(browser, [], async () => { + let video = content.document.getElementById("with-controls"); + video.remove(); + }); + await BrowserTestUtils.waitForMutationCondition( + pipUrlbarToggle, + { attributeFilter: ["hidden"] }, + () => BrowserTestUtils.isVisible(pipUrlbarToggle) + ); + ok( + BrowserTestUtils.isVisible(pipUrlbarToggle), + "PiP urlbar toggle is visible" + ); + + // open for first time after seeing feature callout message + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.video-toggle.has-used", + false, + ], + ], + }); + let fakeMessage = { + template: "feature_callout", + id: "FAKE_PIP_FEATURE_CALLOUT", + content: { + screens: [{ anchors: [{ selector: "#picture-in-picture-button" }] }], + }, + }; + await ASRouter.waitForInitialized; + let originalASRouterState; + await ASRouter.setState(state => { + originalASRouterState = state; + return { + messages: [...state.messages, fakeMessage], + messageImpressions: { + ...state.messageImpressions, + [fakeMessage.id]: [Date.now()], + }, + }; + }); + + ({ totalPipCount } = PictureInPicture.getEligiblePipVideoCount(browser)); + is(totalPipCount, 1, "Total PiP count is 1"); + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + pipUrlbarToggle.click(); + let win = await domWindowOpened; + ok(win, "A Picture-in-Picture window opened."); + await assertVideoIsBeingCloned( + browser.browsingContext.children[0], + "video" + ); + + let filter = { + category: "pictureinpicture", + method: "opened_method", + object: "urlBar", + }; + await waitForTelemeryEvents( + filter, + FIRST_TOGGLE_AFTER_CALLOUT_EXPECTED_EVENTS.length, + "content" + ); + + TelemetryTestUtils.assertEvents( + FIRST_TOGGLE_AFTER_CALLOUT_EXPECTED_EVENTS, + filter, + { clear: true, process: "content" } + ); + + let domWindowClosed = BrowserTestUtils.domWindowClosed(win); + pipUrlbarToggle.click(); + await domWindowClosed; + + await ASRouter.setState(originalASRouterState); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_videoEmptied.js b/toolkit/components/pictureinpicture/tests/browser_videoEmptied.js new file mode 100644 index 0000000000..bc96a9ea58 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_videoEmptied.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the subtitles button hides after switching to a video that does not have subtitles + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "media.videocontrols.picture-in-picture.display-text-tracks.enabled", + true, + ], + ], + }); + + let videoID = "with-controls"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + await prepareVideosAndWebVTTTracks(browser, videoID); + + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + // Need to make sure that the PiP window is at least the minimum height + let multiplier = 1; + while (true) { + if (multiplier * pipWin.innerHeight > 325) { + break; + } + multiplier += 0.5; + } + + pipWin.moveTo(50, 50); + pipWin.resizeTo( + pipWin.innerWidth * multiplier, + pipWin.innerHeight * multiplier + ); + + let subtitlesButton = pipWin.document.querySelector("#closed-caption"); + await TestUtils.waitForCondition(() => { + return !subtitlesButton.disabled; + }, "Waiting for subtitles button to be enabled"); + ok(!subtitlesButton.disabled, "The subtitles button is enabled"); + + let emptied = SpecialPowers.spawn(browser, [{ videoID }], async args => { + let video = content.document.getElementById(args.videoID); + info("Waiting for emptied event to be called"); + await ContentTaskUtils.waitForEvent(video, "emptied"); + }); + + await SpecialPowers.spawn(browser, [{ videoID }], async args => { + let video = content.document.getElementById(args.videoID); + video.setAttribute("src", video.src); + let len = video.textTracks.length; + for (let i = 0; i < len; i++) { + video.removeChild(video.children[0]); + } + video.load(); + }); + + await emptied; + + await TestUtils.waitForCondition(() => { + return subtitlesButton.disabled; + }, "Waiting for subtitles button to be disabled after it was enabled"); + ok(subtitlesButton.disabled, "The subtitles button is disabled"); + + await BrowserTestUtils.closeWindow(pipWin); + } + ); +}); + +/** + * Tests the the subtitles button shows after switching from a video with no subtitles to a video with subtitles + */ +add_task(async () => { + const videoID = "with-controls"; + const videoID2 = "with-controls-no-tracks"; + + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE_WITH_WEBVTT, + gBrowser, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, videoID2); + ok(pipWin, "Got Picture-in-Picture window."); + + // Need to make sure that the PiP window is at least the minimum height + let multiplier = 1; + while (true) { + if (multiplier * pipWin.innerHeight > 325) { + break; + } + multiplier += 0.5; + } + + pipWin.moveTo(50, 50); + pipWin.resizeTo( + pipWin.innerWidth * multiplier, + pipWin.innerHeight * multiplier + ); + + let subtitlesButton = pipWin.document.querySelector("#closed-caption"); + await TestUtils.waitForCondition(() => { + return subtitlesButton.disabled; + }, "Making sure the subtitles button is disabled initially"); + ok(subtitlesButton.disabled, "The subtitles button is disabled"); + + await SpecialPowers.spawn( + browser, + [{ videoID, videoID2 }], + async args => { + let video2 = content.document.getElementById(args.videoID2); + + let track = video2.addTextTrack("captions", "English", "en"); + track.mode = "showing"; + track.addCue( + new content.window.VTTCue(0, 12, "[Test] This is the first cue") + ); + track.addCue( + new content.window.VTTCue(18.7, 21.5, "This is the second cue") + ); + + video2.setAttribute("src", video2.src); + video2.load(); + + is( + video2.textTracks.length, + 1, + "Number of tracks loaded should be 1" + ); + video2.play(); + video2.pause(); + } + ); + + subtitlesButton = pipWin.document.querySelector("#closed-caption"); + await TestUtils.waitForCondition(() => { + return !subtitlesButton.disabled; + }, "Waiting for the subtitles button to be enabled after switching to a video with subtitles."); + ok(!subtitlesButton.disabled, "The subtitles button is enabled"); + + await BrowserTestUtils.closeWindow(pipWin); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_videoSelection.js b/toolkit/components/pictureinpicture/tests/browser_videoSelection.js new file mode 100644 index 0000000000..f887f158c1 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_videoSelection.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the correct video is opened in the + * Picture-in-Picture player when opened via keyboard shortcut. + * The shortcut will open the first unpaused video + * or the longest video on the page. + */ +add_task(async function test_video_selection() { + await BrowserTestUtils.withNewTab( + { + url: TEST_ROOT + "test-video-selection.html", + gBrowser, + }, + async browser => { + await ensureVideosReady(browser); + + let pipVideoID = await SpecialPowers.spawn(browser, [], () => { + let videoList = content.document.querySelectorAll("video"); + let longestDuration = -1; + let pipVideoID = null; + + for (let video of videoList) { + if (!video.paused) { + pipVideoID = video.id; + break; + } + if (video.duration > longestDuration) { + pipVideoID = video.id; + longestDuration = video.duration; + } + } + return pipVideoID; + }); + + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + let videoReady = SpecialPowers.spawn( + browser, + [pipVideoID], + async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + } + ); + + let eventObj = { accelKey: true, shiftKey: true }; + if (AppConstants.platform == "macosx") { + eventObj.altKey = true; + } + EventUtils.synthesizeKey("]", eventObj, window); + + let pipWin = await domWindowOpened; + await videoReady; + + ok(pipWin, "Got Picture-in-Picture window."); + + await ensureMessageAndClosePiP(browser, pipVideoID, pipWin, false); + + pipVideoID = await SpecialPowers.spawn(browser, [], () => { + let videoList = content.document.querySelectorAll("video"); + videoList[1].play(); + videoList[2].play(); + let longestDuration = -1; + let pipVideoID = null; + + for (let video of videoList) { + if (!video.paused) { + pipVideoID = video.id; + break; + } + if (video.duration > longestDuration) { + pipVideoID = video.id; + longestDuration = video.duration; + } + } + + return pipVideoID; + }); + + // Next time we want to use a keyboard shortcut with the main window in focus again. + await SimpleTest.promiseFocus(browser); + + domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + videoReady = SpecialPowers.spawn(browser, [pipVideoID], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + }); + + EventUtils.synthesizeKey("]", eventObj, window); + + pipWin = await domWindowOpened; + await videoReady; + + ok(pipWin, "Got Picture-in-Picture window."); + + await ensureMessageAndClosePiP(browser, pipVideoID, pipWin, false); + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/click-event-helper.js b/toolkit/components/pictureinpicture/tests/click-event-helper.js new file mode 100644 index 0000000000..6b3ba42994 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/click-event-helper.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This helper script is used to record mouse button events for + * Picture-in-Picture toggle click tests. Anytime the toggle is + * clicked, we expect none of the events to be fired. Otherwise, + * all events should be fired when clicking. + */ + +let eventTypes = ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]; + +for (let event of eventTypes) { + addEventListener(event, recordEvent, { capture: true }); +} + +let recordedEvents = []; +function recordEvent(event) { + recordedEvents.push(event.type); +} + +function getRecordedEvents() { + let result = recordedEvents.concat(); + recordedEvents = []; + return result; +} diff --git a/toolkit/components/pictureinpicture/tests/head.js b/toolkit/components/pictureinpicture/tests/head.js new file mode 100644 index 0000000000..7e792b2e9e --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/head.js @@ -0,0 +1,1153 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TOGGLE_POLICIES } = ChromeUtils.importESModule( + "resource://gre/modules/PictureInPictureControls.sys.mjs" +); + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); +const TEST_ROOT_2 = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.org" +); +const TEST_PAGE = TEST_ROOT + "test-page.html"; +const TEST_PAGE_2 = TEST_ROOT_2 + "test-page.html"; +const TEST_PAGE_WITH_IFRAME = TEST_ROOT_2 + "test-page-with-iframe.html"; +const TEST_PAGE_WITH_SOUND = TEST_ROOT + "test-page-with-sound.html"; +const TEST_PAGE_WITHOUT_AUDIO = TEST_ROOT + "test-page-without-audio.html"; +const TEST_PAGE_WITH_NAN_VIDEO_DURATION = + TEST_ROOT + "test-page-with-nan-video-duration.html"; +const TEST_PAGE_WITH_WEBVTT = TEST_ROOT + "test-page-with-webvtt.html"; +const TEST_PAGE_MULTIPLE_CONTEXTS = + TEST_ROOT + "test-page-multiple-contexts.html"; +const TEST_PAGE_TRANSPARENT_NESTED_IFRAMES = + TEST_ROOT + "test-transparent-nested-iframes.html"; +const TEST_PAGE_PIP_DISABLED = TEST_ROOT + "test-page-pipDisabled.html"; +const WINDOW_TYPE = "Toolkit:PictureInPicture"; +const TOGGLE_POSITION_PREF = + "media.videocontrols.picture-in-picture.video-toggle.position"; +/* As of Bug 1811312, 80% toggle opacity is for the PiP toggle experiment control. */ +const DEFAULT_TOGGLE_OPACITY = 0.8; +const HAS_USED_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; +const SHARED_DATA_KEY = "PictureInPicture:SiteOverrides"; +// Used for clearing the size and location of the PiP window +const PLAYER_URI = "chrome://global/content/pictureinpicture/player.xhtml"; +const ACCEPTABLE_DIFFERENCE = 2; + +/** + * We currently ship with a few different variations of the + * Picture-in-Picture toggle. The tests for Picture-in-Picture include tests + * that check the style rules of various parts of the toggle. Since each toggle + * variation has different style rules, we introduce a structure here to + * describe the appearance of the toggle at different stages for the tests. + * + * The top-level structure looks like this: + * + * { + * rootID (String): The ID of the root element of the toggle. + * stages (Object): An Object representing the styles of the toggle at + * different stages of its use. Each property represents a different + * stage that can be tested. Right now, those stages are: + * + * hoverVideo: + * When the mouse is hovering the video but not the toggle. + * + * hoverToggle: + * When the mouse is hovering both the video and the toggle. + * + * Both stages must be assigned an Object with the following properties: + * + * opacities: + * This should be set to an Object where the key is a CSS selector for + * an element, and the value is a double for what the eventual opacity + * of that element should be set to. + * + * hidden: + * This should be set to an Array of CSS selector strings for elements + * that should be hidden during a particular stage. + * } + * + * DEFAULT_TOGGLE_STYLES is the set of styles for the default variation of the + * toggle. + */ +const DEFAULT_TOGGLE_STYLES = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-wrapper": DEFAULT_TOGGLE_OPACITY, + }, + hidden: [".pip-expanded"], + }, + + hoverToggle: { + opacities: { + ".pip-wrapper": 1.0, + }, + hidden: [".pip-expanded"], + }, + }, +}; + +/** + * Given a browser and the ID for a <video> element, triggers + * Picture-in-Picture for that <video>, and resolves with the + * Picture-in-Picture window once it is ready to be used. + * + * If triggerFn is not specified, then open using the + * MozTogglePictureInPicture event. + * + * @param {Element,BrowsingContext} browser The <xul:browser> or + * BrowsingContext hosting the <video> + * + * @param {String} videoID The ID of the video to trigger + * Picture-in-Picture on. + * + * @param {boolean} triggerFn Use the given function to open the pip window, + * which runs in the parent process. + * + * @return Promise + * @resolves With the Picture-in-Picture window when ready. + */ +async function triggerPictureInPicture(browser, videoID, triggerFn) { + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + + let videoReady = null; + if (triggerFn) { + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + video.focus(); + }); + + triggerFn(); + + videoReady = SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + }); + } else { + videoReady = SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + let event = new content.CustomEvent("MozTogglePictureInPicture", { + bubbles: true, + }); + video.dispatchEvent(event); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + }); + } + let win = await domWindowOpened; + await Promise.all([ + SimpleTest.promiseFocus(win), + win.promiseDocumentFlushed(() => {}), + videoReady, + ]); + return win; +} + +/** + * Given a browser and the ID for a <video> element, checks that the + * video is showing the "This video is playing in Picture-in-Picture mode." + * status message overlay. + * + * @param {Element,BrowsingContext} browser The <xul:browser> or + * BrowsingContext hosting the <video> + * + * @param {String} videoID The ID of the video to trigger + * Picture-in-Picture on. + * + * @param {bool} expected True if we expect the message to be showing. + * + * @return Promise + * @resolves When the checks have completed. + */ +async function assertShowingMessage(browser, videoID, expected) { + let showing = await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let pipOverlay = shadowRoot.querySelector(".pictureInPictureOverlay"); + Assert.ok(pipOverlay, "Should be able to find Picture-in-Picture overlay."); + + let rect = pipOverlay.getBoundingClientRect(); + return rect.height > 0 && rect.width > 0; + }); + Assert.equal( + showing, + expected, + "Video should be showing the expected state." + ); +} + +/** + * Tests if a video is currently being cloned for a given content browser. Provides a + * good indicator for answering if this video is currently open in PiP. + * + * @param {Browser} browser + * The content browser or browsing contect that the video lives in + * @param {string} videoId + * The id associated with the video + * + * @returns {bool} + * Whether the video is currently being cloned (And is most likely open in PiP) + */ +function assertVideoIsBeingCloned(browser, selector) { + return SpecialPowers.spawn(browser, [selector], async slctr => { + let video = content.document.querySelector(slctr); + await ContentTaskUtils.waitForCondition(() => { + return video.isCloningElementVisually; + }, "Video is being cloned visually."); + }); +} + +/** + * Ensures that each of the videos loaded inside of a document in a + * <browser> have reached the HAVE_ENOUGH_DATA readyState. + * + * @param {Element} browser The <xul:browser> hosting the <video>(s) or the browsing context + * + * @return Promise + * @resolves When each <video> is in the HAVE_ENOUGH_DATA readyState. + */ +async function ensureVideosReady(browser) { + // PictureInPictureToggleChild waits for videos to fire their "canplay" + // event before considering them for the toggle, so we start by making + // sure each <video> has done this. + info(`Waiting for videos to be ready`); + await SpecialPowers.spawn(browser, [], async () => { + let videos = this.content.document.querySelectorAll("video"); + for (let video of videos) { + video.currentTime = 0; + if (video.readyState < content.HTMLMediaElement.HAVE_ENOUGH_DATA) { + info(`Waiting for 'canplaythrough' for '${video.id}'`); + await ContentTaskUtils.waitForEvent(video, "canplaythrough"); + } + } + }); +} + +/** + * Tests that the toggle opacity reaches or exceeds a certain threshold within + * a reasonable time. + * + * @param {Element} browser The <xul:browser> that has the <video> in it. + * @param {String} videoID The ID of the video element that we expect the toggle + * to appear on. + * @param {String} stage The stage for which the opacity is going to change. This + * should be one of "hoverVideo" or "hoverToggle". + * @param {Object} toggleStyles Optional argument. See the documentation for the + * DEFAULT_TOGGLE_STYLES object for a sense of what styleRules is expected to be. + * + * @return Promise + * @resolves When the check has completed. + */ +async function toggleOpacityReachesThreshold( + browser, + videoID, + stage, + toggleStyles = DEFAULT_TOGGLE_STYLES +) { + let togglePosition = Services.prefs.getStringPref( + TOGGLE_POSITION_PREF, + "right" + ); + let hasUsed = Services.prefs.getBoolPref(HAS_USED_PREF, false); + let toggleStylesForStage = toggleStyles.stages[stage]; + info( + `Testing toggle for stage ${stage} ` + + `in position ${togglePosition}, has used: ${hasUsed}` + ); + + let args = { videoID, toggleStylesForStage, togglePosition, hasUsed }; + await SpecialPowers.spawn(browser, [args], async args => { + let { videoID, toggleStylesForStage } = args; + + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + + for (let hiddenElement of toggleStylesForStage.hidden) { + let el = shadowRoot.querySelector(hiddenElement); + ok( + ContentTaskUtils.isHidden(el), + `Expected ${hiddenElement} to be hidden.` + ); + } + + for (let opacityElement in toggleStylesForStage.opacities) { + let opacityThreshold = toggleStylesForStage.opacities[opacityElement]; + let el = shadowRoot.querySelector(opacityElement); + + await ContentTaskUtils.waitForCondition( + () => { + let opacity = parseFloat(this.content.getComputedStyle(el).opacity); + return opacity >= opacityThreshold; + }, + `Toggle element ${opacityElement} should have eventually reached ` + + `target opacity ${opacityThreshold}`, + 100, + 100 + ); + } + + ok(true, "Toggle reached target opacity."); + }); +} + +/** + * Tests that the toggle has the correct policy attribute set. This should be called + * either when the toggle is visible, or events have been queued such that the toggle + * will soon be visible. + * + * @param {Element} browser The <xul:browser> that has the <video> in it. + * @param {String} videoID The ID of the video element that we expect the toggle + * to appear on. + * @param {Number} policy Optional argument. If policy is defined, then it should + * be one of the values in the TOGGLE_POLICIES from PictureInPictureControls.sys.mjs. + * If undefined, this function will ensure no policy attribute is set. + * + * @return Promise + * @resolves When the check has completed. + */ +async function assertTogglePolicy( + browser, + videoID, + policy, + toggleStyles = DEFAULT_TOGGLE_STYLES +) { + let toggleID = toggleStyles.rootID; + let args = { videoID, toggleID, policy }; + await SpecialPowers.spawn(browser, [args], async args => { + let { videoID, toggleID, policy } = args; + + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let controlsOverlay = shadowRoot.querySelector(".controlsOverlay"); + let toggle = shadowRoot.getElementById(toggleID); + + await ContentTaskUtils.waitForCondition(() => { + return controlsOverlay.classList.contains("hovering"); + }, "Waiting for the hovering state to be set on the video."); + + if (policy) { + const { TOGGLE_POLICY_STRINGS } = ChromeUtils.importESModule( + "resource://gre/modules/PictureInPictureControls.sys.mjs" + ); + let policyAttr = toggle.getAttribute("policy"); + Assert.equal( + policyAttr, + TOGGLE_POLICY_STRINGS[policy], + "The correct toggle policy is set." + ); + } else { + Assert.ok( + !toggle.hasAttribute("policy"), + "No toggle policy should be set." + ); + } + }); +} + +/** + * Tests that either all or none of the expected mousebutton events + * fire in web content when clicking on the page. + * + * Note: This function will only work on pages that load the + * click-event-helper.js script. + * + * @param {Element} browser The <xul:browser> that will receive the mouse + * events. + * @param {bool} isExpectingEvents True if we expect all of the normal + * mouse button events to fire. False if we expect none of them to fire. + * @param {bool} isExpectingClick True if the mouse events should include the + * "click" event, which is only included when the primary mouse button is pressed. + * @return Promise + * @resolves When the check has completed. + */ +async function assertSawMouseEvents( + browser, + isExpectingEvents, + isExpectingClick = true +) { + const MOUSE_BUTTON_EVENTS = [ + "pointerdown", + "mousedown", + "pointerup", + "mouseup", + ]; + + if (isExpectingClick) { + MOUSE_BUTTON_EVENTS.push("click"); + } + + let mouseEvents = await SpecialPowers.spawn(browser, [], async () => { + return this.content.wrappedJSObject.getRecordedEvents(); + }); + + let expectedEvents = isExpectingEvents ? MOUSE_BUTTON_EVENTS : []; + Assert.deepEqual( + mouseEvents, + expectedEvents, + "Expected to get the right mouse events." + ); +} + +/** + * Tests that a click event is fire in web content when clicking on the page. + * + * Note: This function will only work on pages that load the + * click-event-helper.js script. + * + * @param {Element} browser The <xul:browser> that will receive the mouse + * events. + * @return Promise + * @resolves When the check has completed. + */ +async function assertSawClickEventOnly(browser) { + let mouseEvents = await SpecialPowers.spawn(browser, [], async () => { + return this.content.wrappedJSObject.getRecordedEvents(); + }); + Assert.deepEqual( + mouseEvents, + ["click"], + "Expected to get the right mouse events." + ); +} + +/** + * Ensures that a <video> inside of a <browser> is scrolled into view, + * and then returns the coordinates of its Picture-in-Picture toggle as well + * as whether or not the <video> element is showing the built-in controls. + * + * @param {Element} browser The <xul:browser> that has the <video> loaded in it. + * @param {String} videoID The ID of the video that has the toggle. + * + * @return Promise + * @resolves With the following Object structure: + * { + * controls: <Boolean>, + * } + * + * Where controls represents whether or not the video has the default control set + * displayed. + */ +async function prepareForToggleClick(browser, videoID) { + // Synthesize a mouse move just outside of the video to ensure that + // the video is in a non-hovering state. We'll go 5 pixels to the + // left and above the top-left corner. + await BrowserTestUtils.synthesizeMouse( + `#${videoID}`, + -5, + -5, + { + type: "mousemove", + }, + browser, + false + ); + + // For each video, make sure it's scrolled into view, and get the rect for + // the toggle while we're at it. + let args = { videoID }; + return SpecialPowers.spawn(browser, [args], async args => { + let { videoID } = args; + + let video = content.document.getElementById(videoID); + video.scrollIntoView({ behaviour: "instant" }); + + if (!video.controls) { + // For no-controls <video> elements, an IntersectionObserver is used + // to know when we the PictureInPictureChild should begin tracking + // mousemove events. We don't exactly know when that IntersectionObserver + // will fire, so we poll a special testing function that will tell us when + // the video that we care about is being tracked. + let { PictureInPictureToggleChild } = ChromeUtils.importESModule( + "resource://gre/actors/PictureInPictureChild.sys.mjs" + ); + await ContentTaskUtils.waitForCondition( + () => { + return PictureInPictureToggleChild.isTracking(video); + }, + "Waiting for PictureInPictureToggleChild to be tracking the video.", + 100, + 100 + ); + } + + let shadowRoot = video.openOrClosedShadowRoot; + let controlsOverlay = shadowRoot.querySelector(".controlsOverlay"); + await ContentTaskUtils.waitForCondition( + () => { + return !controlsOverlay.classList.contains("hovering"); + }, + "Waiting for the video to not be hovered.", + 100, + 100 + ); + + return { + controls: video.controls, + }; + }); +} + +/** + * Returns client rect info for the toggle if it's supposed to be visible + * on hover. Otherwise, returns client rect info for the video with the + * associated ID. + * + * @param {Element} browser The <xul:browser> that has the <video> loaded in it. + * @param {String} videoID The ID of the video that has the toggle. + * + * @return Promise + * @resolves With the following Object structure: + * { + * top: <Number>, + * left: <Number>, + * width: <Number>, + * height: <Number>, + * } + */ +async function getToggleClientRect( + browser, + videoID, + toggleStyles = DEFAULT_TOGGLE_STYLES +) { + let args = { videoID, toggleID: toggleStyles.rootID }; + return ContentTask.spawn(browser, args, async args => { + const { Rect } = ChromeUtils.importESModule( + "resource://gre/modules/Geometry.sys.mjs" + ); + + let { videoID, toggleID } = args; + let video = content.document.getElementById(videoID); + let shadowRoot = video.openOrClosedShadowRoot; + let toggle = shadowRoot.getElementById(toggleID); + let rect = Rect.fromRect(toggle.getBoundingClientRect()); + + let clickableChildren = toggle.querySelectorAll(".clickable"); + for (let child of clickableChildren) { + let childRect = Rect.fromRect(child.getBoundingClientRect()); + rect.expandToContain(childRect); + } + + if (!rect.width && !rect.height) { + rect = video.getBoundingClientRect(); + } + + return { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + }; + }); +} + +/** + * This function will hover over the middle of the video and then + * hover over the toggle. + * @param browser The current browser + * @param videoID The video element id + */ +async function hoverToggle(browser, videoID) { + await prepareForToggleClick(browser, videoID); + + // Hover the mouse over the video to reveal the toggle. + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mousemove", + }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + info("Checking toggle policy"); + await assertTogglePolicy(browser, videoID, null); + + let toggleClientRect = await getToggleClientRect(browser, videoID); + + info("Hovering the toggle rect now."); + let toggleCenterX = toggleClientRect.left + toggleClientRect.width / 2; + let toggleCenterY = toggleClientRect.top + toggleClientRect.height / 2; + + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleCenterX, + toggleCenterY, + { + type: "mousemove", + }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleCenterX, + toggleCenterY, + { + type: "mouseover", + }, + browser + ); +} + +/** + * Test helper for the Picture-in-Picture toggle. Loads a page, and then + * tests the provided video elements for the toggle both appearing and + * opening the Picture-in-Picture window in the expected cases. + * + * @param {String} testURL The URL of the page with the <video> elements. + * @param {Object} expectations An object with the following schema: + * <video-element-id>: { + * canToggle: {Boolean} + * policy: {Number} (optional) + * styleRules: {Object} (optional) + * } + * If canToggle is true, then it's expected that moving the mouse over the + * video and then clicking in the toggle region should open a + * Picture-in-Picture window. If canToggle is false, we expect that a click + * in this region will not result in the window opening. + * + * If policy is defined, then it should be one of the values in the + * TOGGLE_POLICIES from PictureInPictureControls.sys.mjs. + * + * See the documentation for the DEFAULT_TOGGLE_STYLES object for a sense + * of what styleRules is expected to be. If left undefined, styleRules will + * default to DEFAULT_TOGGLE_STYLES. + * + * @param {async Function} prepFn An optional asynchronous function to run + * before running the toggle test. The function is passed the opened + * <xul:browser> as its only argument once the testURL has finished loading. + * + * @return Promise + * @resolves When the test is complete and the tab with the loaded page is + * removed. + */ +async function testToggle(testURL, expectations, prepFn = async () => {}) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: testURL, + }, + async browser => { + await prepFn(browser); + await ensureVideosReady(browser); + + for (let [ + videoID, + { canToggle, policy, toggleStyles, shouldSeeClickEventAfterToggle }, + ] of Object.entries(expectations)) { + await SimpleTest.promiseFocus(browser); + info(`Testing video with id: ${videoID}`); + + await testToggleHelper( + browser, + videoID, + canToggle, + policy, + toggleStyles, + shouldSeeClickEventAfterToggle + ); + } + } + ); +} + +/** + * Test helper for the Picture-in-Picture toggle. Given a loaded page with some + * videos on it, tests that the toggle behaves as expected when interacted + * with by the mouse. + * + * @param {Element} browser The <xul:browser> that has the <video> loaded in it. + * @param {String} videoID The ID of the video that has the toggle. + * @param {Boolean} canToggle True if we expect the toggle to be visible and + * clickable by the mouse for the associated video. + * @param {Number} policy Optional argument. If policy is defined, then it should + * be one of the values in the TOGGLE_POLICIES from PictureInPictureControls.sys.mjs. + * @param {Object} toggleStyles Optional argument. See the documentation for the + * DEFAULT_TOGGLE_STYLES object for a sense of what styleRules is expected to be. + * + * @return Promise + * @resolves When the check for the toggle is complete. + */ +async function testToggleHelper( + browser, + videoID, + canToggle, + policy, + toggleStyles, + shouldSeeClickEventAfterToggle +) { + let { controls } = await prepareForToggleClick(browser, videoID); + + // Hover the mouse over the video to reveal the toggle. + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mousemove", + }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + info("Checking toggle policy"); + await assertTogglePolicy(browser, videoID, policy, toggleStyles); + + if (canToggle) { + info("Waiting for toggle to become visible"); + await toggleOpacityReachesThreshold( + browser, + videoID, + "hoverVideo", + toggleStyles + ); + } + + let toggleClientRect = await getToggleClientRect( + browser, + videoID, + toggleStyles + ); + + info("Hovering the toggle rect now."); + let toggleCenterX = toggleClientRect.left + toggleClientRect.width / 2; + let toggleCenterY = toggleClientRect.top + toggleClientRect.height / 2; + + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleCenterX, + toggleCenterY, + { + type: "mousemove", + }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleCenterX, + toggleCenterY, + { + type: "mouseover", + }, + browser + ); + + if (canToggle) { + info("Waiting for toggle to reach full opacity"); + await toggleOpacityReachesThreshold( + browser, + videoID, + "hoverToggle", + toggleStyles + ); + } + + // First, ensure that a non-primary mouse click is ignored. + info("Right-clicking on toggle."); + + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleCenterX, + toggleCenterY, + { button: 2 }, + browser + ); + + // For videos without the built-in controls, we expect that all mouse events + // should have fired - otherwise, the events are all suppressed. For videos + // with controls, none of the events should be fired, as the controls overlay + // absorbs them all. + // + // Note that the right-click does not result in a "click" event firing. + await assertSawMouseEvents(browser, !controls, false); + + // The message to open the Picture-in-Picture window would normally be sent + // immediately before this Promise resolved, so the window should have opened + // by now if it was going to happen. + for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) { + if (!win.closed) { + ok(false, "Found a Picture-in-Picture window unexpectedly."); + return; + } + } + + ok(true, "No Picture-in-Picture window found."); + + // Okay, now test with the primary mouse button. + + if (canToggle) { + info( + "Clicking on toggle, and expecting a Picture-in-Picture window to open" + ); + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleCenterX, + toggleCenterY, + {}, + browser + ); + let win = await domWindowOpened; + ok(win, "A Picture-in-Picture window opened."); + + await assertVideoIsBeingCloned(browser, "#" + videoID); + + await BrowserTestUtils.closeWindow(win); + + // We do get a "Click" sometimes, it depends on many + // factors such as whether the video has control and + // the style of the toggle. + if (shouldSeeClickEventAfterToggle) { + await assertSawClickEventOnly(browser); + } else { + // Make sure that clicking on the toggle resulted in no mouse button events + // being fired in content. + await assertSawMouseEvents(browser, false); + } + } else { + info( + "Clicking on toggle, and expecting no Picture-in-Picture window opens" + ); + await BrowserTestUtils.synthesizeMouseAtPoint( + toggleCenterX, + toggleCenterY, + {}, + browser + ); + + // If we aren't showing the toggle, we expect all mouse events to be seen. + await assertSawMouseEvents(browser, !controls); + + // The message to open the Picture-in-Picture window would normally be sent + // immediately before this Promise resolved, so the window should have opened + // by now if it was going to happen. + for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) { + if (!win.closed) { + ok(false, "Found a Picture-in-Picture window unexpectedly."); + return; + } + } + + ok(true, "No Picture-in-Picture window found."); + } + + // Click on the very top-left pixel of the document and ensure that we + // see all of the mouse events for it. + await BrowserTestUtils.synthesizeMouseAtPoint(1, 1, {}, browser); + await assertSawMouseEvents(browser, true); +} + +/** + * Helper function that ensures that a provided async function + * causes a window to fully enter fullscreen mode. + * + * @param window (DOM Window) + * The window that is expected to enter fullscreen mode. + * @param asyncFn (Async Function) + * The async function to run to trigger the fullscreen switch. + * @return Promise + * @resolves When the fullscreen entering transition completes. + */ +async function promiseFullscreenEntered(window, asyncFn) { + let entered = BrowserTestUtils.waitForEvent( + window, + "MozDOMFullscreen:Entered" + ); + + await asyncFn(); + + await entered; + + await BrowserTestUtils.waitForCondition(() => { + return !TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS"); + }); + + if (AppConstants.platform == "macosx") { + // On macOS, the fullscreen transition takes some extra time + // to complete, and we don't receive events for it. We need to + // wait for it to complete or else input events in the next test + // might get eaten up. This is the best we can currently do. + // + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + dump(`BJW promiseFullscreenEntered: waiting for 2 second timeout.\n`); + await new Promise(resolve => setTimeout(resolve, 2000)); + } +} + +/** + * Helper function that ensures that a provided async function + * causes a window to fully exit fullscreen mode. + * + * @param window (DOM Window) + * The window that is expected to exit fullscreen mode. + * @param asyncFn (Async Function) + * The async function to run to trigger the fullscreen switch. + * @return Promise + * @resolves When the fullscreen exiting transition completes. + */ +async function promiseFullscreenExited(window, asyncFn) { + let exited = BrowserTestUtils.waitForEvent(window, "MozDOMFullscreen:Exited"); + + await asyncFn(); + + await exited; + + await BrowserTestUtils.waitForCondition(() => { + return !TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS"); + }); + + if (AppConstants.platform == "macosx") { + // On macOS, the fullscreen transition takes some extra time + // to complete, and we don't receive events for it. We need to + // wait for it to complete or else input events in the next test + // might get eaten up. This is the best we can currently do. + // + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + } +} + +/** + * Helper function that ensures that the "This video is + * playing in Picture-in-Picture mode" message works, + * then closes the player window + * + * @param {Element} browser The <xul:browser> that has the <video> loaded in it. + * @param {String} videoID The ID of the video that has the toggle. + * @param {Element} pipWin The Picture-in-Picture window that was opened + * @param {Boolean} iframe True if the test is on an Iframe, which modifies + * the test behavior + */ +async function ensureMessageAndClosePiP(browser, videoID, pipWin, isIframe) { + try { + await assertShowingMessage(browser, videoID, true); + } finally { + let uaWidgetUpdate = null; + if (isIframe) { + uaWidgetUpdate = SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForEvent( + content.windowRoot, + "UAWidgetSetupOrChange", + true /* capture */ + ); + }); + } else { + uaWidgetUpdate = BrowserTestUtils.waitForContentEvent( + browser, + "UAWidgetSetupOrChange", + true /* capture */ + ); + } + await BrowserTestUtils.closeWindow(pipWin); + await uaWidgetUpdate; + } +} + +/** + * Helper function that returns True if the specified video is paused + * and False if the specified video is not paused. + * + * @param {Element} browser The <xul:browser> that has the <video> loaded in it. + * @param {String} videoID The ID of the video to check. + */ +async function isVideoPaused(browser, videoID) { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).paused; + }); +} + +/** + * Helper function that returns True if the specified video is muted + * and False if the specified video is not muted. + * + * @param {Element} browser The <xul:browser> that has the <video> loaded in it. + * @param {String} videoID The ID of the video to check. + */ +async function isVideoMuted(browser, videoID) { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).muted; + }); +} + +/** + * Initializes videos and text tracks for the current test case. + * First track is the default track to be loaded onto the video. + * Once initialization is done, play then pause the requested video. + * so that text tracks are loaded. + * @param {Element} browser The <xul:browser> hosting the <video> + * @param {String} videoID The ID of the video being checked + * @param {Integer} defaultTrackIndex The index of the track to be loaded, or none if -1 + * @param {String} trackMode the mode that the video's textTracks should be set to + */ +async function prepareVideosAndWebVTTTracks( + browser, + videoID, + defaultTrackIndex = 0, + trackMode = "showing" +) { + info("Preparing video and initial text tracks"); + await ensureVideosReady(browser); + await SpecialPowers.spawn( + browser, + [{ videoID, defaultTrackIndex, trackMode }], + async args => { + let video = content.document.getElementById(args.videoID); + let tracks = video.textTracks; + + is(tracks.length, 5, "Number of tracks loaded should be 5"); + + // Enable track for originating video + if (args.defaultTrackIndex >= 0) { + info(`Loading track ${args.defaultTrackIndex + 1}`); + let track = tracks[args.defaultTrackIndex]; + tracks.mode = args.trackMode; + track.mode = args.trackMode; + } + + // Briefly play the video to load text tracks onto the pip window. + info("Playing video to load text tracks"); + video.play(); + info("Pausing video"); + video.pause(); + ok(video.paused, "Video should be paused before proceeding with test"); + } + ); +} + +/** + * Plays originating video until the next cue is loaded. + * Once the next cue is loaded, pause the video. + * @param {Element} browser The <xul:browser> hosting the <video> + * @param {String} videoID The ID of the video being checked + * @param {Integer} textTrackIndex The index of the track to be loaded, or none if -1 + */ +async function waitForNextCue(browser, videoID, textTrackIndex = 0) { + if (textTrackIndex < 0) { + ok(false, "Cannot wait for next cue with invalid track index"); + } + + await SpecialPowers.spawn( + browser, + [{ videoID, textTrackIndex }], + async args => { + let video = content.document.getElementById(args.videoID); + info("Playing video to activate next cue"); + video.play(); + ok(!video.paused, "Video is playing"); + + info("Waiting until cuechange is called"); + await ContentTaskUtils.waitForEvent( + video.textTracks[args.textTrackIndex], + "cuechange" + ); + + info("Pausing video to read text track"); + video.pause(); + ok(video.paused, "Video is paused"); + } + ); +} + +/** + * The PiP window saves the positon when closed and sometimes we don't want + * this information to persist to other tests. This function will clear the + * position so the PiP window will open in the default position. + */ +function clearSavedPosition() { + let xulStore = Services.xulStore; + xulStore.setValue(PLAYER_URI, "picture-in-picture", "left", NaN); + xulStore.setValue(PLAYER_URI, "picture-in-picture", "top", NaN); + xulStore.setValue(PLAYER_URI, "picture-in-picture", "width", NaN); + xulStore.setValue(PLAYER_URI, "picture-in-picture", "height", NaN); +} + +function overrideSavedPosition(left, top, width, height) { + let xulStore = Services.xulStore; + xulStore.setValue(PLAYER_URI, "picture-in-picture", "left", left); + xulStore.setValue(PLAYER_URI, "picture-in-picture", "top", top); + xulStore.setValue(PLAYER_URI, "picture-in-picture", "width", width); + xulStore.setValue(PLAYER_URI, "picture-in-picture", "height", height); +} + +/** + * Function used to filter events when waiting for the correct number + * telemetry events. + * @param {String} expected The expected string or undefined + * @param {String} actual The actual string + * @returns true if the expected is undefined or if expected matches actual + */ +function matches(expected, actual) { + if (expected === undefined) { + return true; + } + return expected === actual; +} + +/** + * Function that waits for the expected number of events aftering filtering. + * @param {Object} filter An object containing optional filters + * { + * category: (optional) The category of the event. Ex. "pictureinpicture" + * method: (optional) The method of the event. Ex. "create" + * object: (optional) The object of the event. Ex. "player" + * } + * @param {Number} length The number of events to wait for + * @param {String} process Should be "content" or "parent" depending on the event + */ +async function waitForTelemeryEvents(filter, length, process) { + let { + category: filterCategory, + method: filterMethod, + object: filterObject, + } = filter; + + let events = []; + await TestUtils.waitForCondition( + () => { + events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + )[process]; + if (!events) { + return false; + } + + let filtered = events + .map(([, /* timestamp */ category, method, object, value, extra]) => { + // We don't care about the `timestamp` value. + // Tests that examine that value should use `snapshotEvents` directly. + return [category, method, object, value, extra]; + }) + .filter(([category, method, object]) => { + return ( + matches(filterCategory, category) && + matches(filterMethod, method) && + matches(filterObject, object) + ); + }); + info(JSON.stringify(filtered, null, 2)); + return filtered && filtered.length >= length; + }, + `Waiting for ${length} pictureinpicture telemetry event(s) with filter ${JSON.stringify( + filter, + null, + 2 + )}`, + 200, + 100 + ); +} diff --git a/toolkit/components/pictureinpicture/tests/no-audio-track.webm b/toolkit/components/pictureinpicture/tests/no-audio-track.webm Binary files differnew file mode 100644 index 0000000000..72b0297233 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/no-audio-track.webm diff --git a/toolkit/components/pictureinpicture/tests/short.mp4 b/toolkit/components/pictureinpicture/tests/short.mp4 Binary files differnew file mode 100644 index 0000000000..abe37b9f9d --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/short.mp4 diff --git a/toolkit/components/pictureinpicture/tests/test-button-overlay.html b/toolkit/components/pictureinpicture/tests/test-button-overlay.html new file mode 100644 index 0000000000..9917fba973 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-button-overlay.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture test - transparent overlays - 1</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + } + + .container { + position: relative; + display: inline-block; + } + + .overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + color: white; + } + + .toggle-overlay { + position: absolute; + min-width: 50px; + right: 0px; + height: 100%; + top: calc(50% - 25px); + } + + button { + height: 100px; + } + + .transparent-background { + background-color: transparent; + } + + .partial-opacity { + opacity: 0.5; + } + + .full-opacity { + opacity: 1.0; + background-color: green; + } + + .no-pointer-events { + pointer-events: none; + } + + .pointer-events { + pointer-events: auto; + } +</style> +<body> + <div class="container"> + <div class="overlay transparent-background no-pointer-events"> + This is a fully transparent overlay using a transparent background. + <div class="toggle-overlay partial-opacity pointer-events"> + <button>I'm a button overlapping the toggle</button> + </div> + </div> + <video id="video-partial-transparent-button" src="test-video.mp4" loop="true"></video> + </div> + + <div class="container"> + <div class="overlay transparent-background no-pointer-events"> + This is a fully transparent overlay using a transparent background. + <div class="toggle-overlay full-opacity pointer-events"> + <button>I'm a button overlapping the toggle</button> + </div> + </div> + <video id="video-opaque-button" src="test-video.mp4" loop="true"></video> + </div> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-media-stream.html b/toolkit/components/pictureinpicture/tests/test-media-stream.html new file mode 100644 index 0000000000..ad5f91dd25 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-media-stream.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + border: 1px solid black; + } +</style> +<body> + <script> + function fireEvents() { + for (let videoID of ["with-controls", "no-controls"]) { + let video = document.getElementById(videoID); + let event = new CustomEvent("MozTogglePictureInPicture", { bubbles: true }); + video.dispatchEvent(event); + } + } + </script> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-opaque-overlay.html b/toolkit/components/pictureinpicture/tests/test-opaque-overlay.html new file mode 100644 index 0000000000..425c0e74c2 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-opaque-overlay.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture test - transparent overlays - 1</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + } + + .container { + position: relative; + display: inline-block; + } + + .overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + color: white; + } + + .toggle-overlay { + position: absolute; + min-width: 50px; + right: 0px; + height: 100%; + top: calc(50% - 25px); + } + + .full-opacity { + opacity: 1.0; + background-color: green; + } +</style> +<body> + <div class="container"> + <div class="overlay full-opacity">This is a fully opaque overlay using opacity: 1.0</div> + <video id="video-full-opacity" src="test-video.mp4" loop="true"></video> + </div> + + <div class="container"> + <div class="toggle-overlay full-opacity">This is a fully opaque overlay over a region covering the toggle at opacity: 1.0</div> + <video id="video-full-opacity-over-toggle" src="test-video.mp4" loop="true"></video> + </div> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-page-multiple-contexts.html b/toolkit/components/pictureinpicture/tests/test-page-multiple-contexts.html new file mode 100644 index 0000000000..420370aff2 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-page-multiple-contexts.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> +</head> +<style> + video { + display: block; + border: 1px solid black; + } +</style> +<body> + <h1>Video in page</h1> + <video id="with-controls" src="test-video.mp4" controls loop="true" width="400" height="225"></video> + <h1>Video in frame</h1> + <iframe id="iframe" width="400" height="225" src="test-video.mp4"></iframe> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-page-pipDisabled.html b/toolkit/components/pictureinpicture/tests/test-page-pipDisabled.html new file mode 100644 index 0000000000..142d2d74d2 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-page-pipDisabled.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + border: 1px solid black; + } +</style> +<body> + <h1>Video with PiPDisabled</h1> + <video id="with-controls" src="test-video.mp4" controls loop="true" width="400" height="225" disablePictureInPicture="true"></video> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-page-with-iframe.html b/toolkit/components/pictureinpicture/tests/test-page-with-iframe.html new file mode 100644 index 0000000000..02205a028b --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-page-with-iframe.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + html, body { + height: 100vh; + width: 100vw; + margin: 0; + padding: 0; + overflow: hidden; + } + #iframe { + height: 100vh; + width: 100vw; + padding: 0; + margin: 0; + border: 0; + } +</style> +<body> + <iframe id="iframe"></iframe> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-page-with-nan-video-duration.html b/toolkit/components/pictureinpicture/tests/test-page-with-nan-video-duration.html new file mode 100644 index 0000000000..b16c3682a0 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-page-with-nan-video-duration.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Bug 1679174</title> + <script type="text/javascript" src="click-event-helper.js"></script> + </head> + <body> + <video id="nan-duration"></video> + <video controls id="test-video"> + <source src="test-video.mp4" type="video/mp4"> + </video> + </body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-page-with-sound.html b/toolkit/components/pictureinpicture/tests/test-page-with-sound.html new file mode 100644 index 0000000000..6b6a860bc2 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-page-with-sound.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests (longer video with sound)</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + border: 1px solid black; + } +</style> +<body> + <h1>Video with controls</h1> + <video id="with-controls" src="gizmo.mp4" controls loop="true"></video> + <h1>Video without controls</h1> + <video id="no-controls" src="gizmo.mp4" loop="true"></video> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-page-with-webvtt.html b/toolkit/components/pictureinpicture/tests/test-page-with-webvtt.html new file mode 100644 index 0000000000..e05ad8dd2c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-page-with-webvtt.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + border: 1px solid black; + } +</style> +<body> + <h1>Video with controls</h1> + <video id="with-controls" src="test-video-long.mp4" controls width="400" height="225"> + <track + id="track1" + kind="captions" + label="[test] en" + srclang="en" + src="test-webvtt-1.vtt" + /> + <track + id="track2" + kind="subtitles" + label="[test] fr" + srclang="fr" + src="test-webvtt-2.vtt" + /> + <track + id="track3" + kind="subtitles" + label="[test] eo" + srclang="eo" + src="test-webvtt-3.vtt" + /> + <track + id="track4" + kind="subtitles" + label="[test] zh" + srclang="zh" + src="test-webvtt-4.vtt" + /> + <track + id="track5" + kind="subtitles" + label="[test] es" + srclang="es" + src="test-webvtt-5.vtt" + /> + </video> + + <video id="with-controls-no-tracks" src="test-video-long.mp4" controls width="400" height="225"></video> + + <script> + function fireEvents() { + for (let videoID of ["with-controls"]) { + let video = document.getElementById(videoID); + let event = new CustomEvent("MozTogglePictureInPicture", { bubbles: true }); + video.dispatchEvent(event); + } + } + </script> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-page-without-audio.html b/toolkit/components/pictureinpicture/tests/test-page-without-audio.html new file mode 100644 index 0000000000..862042cd59 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-page-without-audio.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + border: 1px solid black; + } +</style> +<body> + <h1>Video without audio track</h1> + <video id="without-audio" src="no-audio-track.webm" controls width="400" height="225"></video> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-page.html b/toolkit/components/pictureinpicture/tests/test-page.html new file mode 100644 index 0000000000..a62ff1ac4a --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-page.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + border: 1px solid black; + } +</style> +<body> + <h1>Video with controls</h1> + <video id="with-controls" src="test-video.mp4" controls loop="true" width="400" height="225"></video> + <h1>Video without controls</h1> + <video id="no-controls" src="test-video.mp4" loop="true" width="400" height="225"></video> + + <script> + function fireEvents() { + for (let videoID of ["with-controls", "no-controls"]) { + let video = document.getElementById(videoID); + let event = new CustomEvent("MozTogglePictureInPicture", { bubbles: true }); + video.dispatchEvent(event); + } + } + </script> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-pointer-events-none.html b/toolkit/components/pictureinpicture/tests/test-pointer-events-none.html new file mode 100644 index 0000000000..63254b329c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-pointer-events-none.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture test - pointer-events: none</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + pointer-events: none; + } + +</style> +<body> + <h1>Video with controls</h1> + <video id="with-controls" src="test-video.mp4" controls loop="true"></video> + <h1>Video without controls</h1> + <video id="no-controls" src="test-video.mp4" loop="true"></video> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-reversed.html b/toolkit/components/pictureinpicture/tests/test-reversed.html new file mode 100644 index 0000000000..4f6d516698 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-reversed.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + #reversed { + transform: scaleX(-1); + } +</style> +<body> + <h1>Reversed video</h1> + <video id="reversed" src="test-video.mp4" controls loop="true" width="400" height="225"></video> + <h1>Not Reversed Video</h1> + <video id="not-reversed" src="test-video.mp4" loop="true" width="400" height="225"></video> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-transparent-nested-iframes.html b/toolkit/components/pictureinpicture/tests/test-transparent-nested-iframes.html new file mode 100644 index 0000000000..d7efcc1e92 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-transparent-nested-iframes.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture test - transparent iframe</title> +</head> + +<style> + video { + display: block; + } + + .root { + position: relative; + display: inline-block; + } + + .controls { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + color: white; + } + + .container, + iframe { + width: 100%; + height: 100%; + } + + iframe { + border: 0; + } +</style> + +<body> + <div class="root"> + <div class="controls"> + <div class="container"> + <iframe src="about:blank"></iframe> + </div> + </div> + + <div class="video-container"> + <video id="video-transparent-background" src="test-video.mp4" loop="true"></video> + </div> + </div> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-transparent-overlay-1.html b/toolkit/components/pictureinpicture/tests/test-transparent-overlay-1.html new file mode 100644 index 0000000000..8f0f76311b --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-transparent-overlay-1.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture test - transparent overlays - 1</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + } + + .container { + position: relative; + display: inline-block; + } + + .overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + color: white; + } + + .transparent-background { + background-color: transparent; + } + + .alpha-background { + background-color: rgba(255, 0, 0, 0.5); + } +</style> +<body> + <div class="container"> + <div class="overlay transparent-background">This is a fully transparent overlay</div> + <video id="video-transparent-background" src="test-video.mp4" loop="true"></video> + </div> + + <div class="container"> + <div class="overlay alpha-background">This is a partially transparent overlay using alpha</div> + <video id="video-alpha-background" src="test-video.mp4" loop="true"></video> + </div> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-transparent-overlay-2.html b/toolkit/components/pictureinpicture/tests/test-transparent-overlay-2.html new file mode 100644 index 0000000000..86dab15690 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-transparent-overlay-2.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture test - transparent overlays - 1</title> + <script type="text/javascript" src="click-event-helper.js"></script> +</head> +<style> + video { + display: block; + } + + .container { + position: relative; + display: inline-block; + } + + .overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + color: white; + } + + .zero-opacity { + opacity: 0; + } + + .partial-opacity { + opacity: 0.5; + } +</style> +<body> + <div class="container"> + <div class="overlay zero-opacity">This is a transparent overlay using opacity: 0</div> + <video id="video-zero-opacity" src="test-video.mp4" loop="true"></video> + </div> + + <div class="container"> + <div class="overlay partial-opacity">This is a partially transparent overlay using opacity: 0.5</div> + <video id="video-partial-opacity" src="test-video.mp4" loop="true"></video> + </div> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-video-cropped.mp4 b/toolkit/components/pictureinpicture/tests/test-video-cropped.mp4 Binary files differnew file mode 100644 index 0000000000..6ea66eb1fc --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-video-cropped.mp4 diff --git a/toolkit/components/pictureinpicture/tests/test-video-long.mp4 b/toolkit/components/pictureinpicture/tests/test-video-long.mp4 Binary files differnew file mode 100644 index 0000000000..714c17ca12 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-video-long.mp4 diff --git a/toolkit/components/pictureinpicture/tests/test-video-selection.html b/toolkit/components/pictureinpicture/tests/test-video-selection.html new file mode 100644 index 0000000000..4ce65d6309 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-video-selection.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Picture-in-Picture tests</title> + <script type="text/javascript"></script> +</head> +<style> + video { + display: block; + border: 1px solid black; + } +</style> +<body> + <h1>Shortest Video</h1> + <video id="shortest" src="test-video.mp4" controls loop="true"></video> + <h1>Shorter Video</h1> + <video id="short" src="short.mp4" controls loop="true"></video> + <h1>Longer Video</h1> + <video id="long" src="test-video-long.mp4" controls loop="true"></video> +</body> +</html> diff --git a/toolkit/components/pictureinpicture/tests/test-video-vertical.mp4 b/toolkit/components/pictureinpicture/tests/test-video-vertical.mp4 Binary files differnew file mode 100644 index 0000000000..404eec6fd0 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-video-vertical.mp4 diff --git a/toolkit/components/pictureinpicture/tests/test-video.mp4 b/toolkit/components/pictureinpicture/tests/test-video.mp4 Binary files differnew file mode 100644 index 0000000000..90bbe6bc26 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-video.mp4 diff --git a/toolkit/components/pictureinpicture/tests/test-webvtt-1.vtt b/toolkit/components/pictureinpicture/tests/test-webvtt-1.vtt new file mode 100644 index 0000000000..fd16ef6d32 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-webvtt-1.vtt @@ -0,0 +1,10 @@ +WEBVTT + +1 +00:00:00.000 --> 00:00:01.000 +track 1 - cue 1 + +2 +00:00:02.000 --> 00:00:05.000 +- <b>track 1 - cue 2 bold</b> +- <i>track 1 - cue 2 italicized<i> diff --git a/toolkit/components/pictureinpicture/tests/test-webvtt-2.vtt b/toolkit/components/pictureinpicture/tests/test-webvtt-2.vtt new file mode 100644 index 0000000000..21fda2b75c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-webvtt-2.vtt @@ -0,0 +1,10 @@ +WEBVTT + +1 +00:00:00.000 --> 00:00:01.000 +track 2 - cue 1 + +2 +00:00:02.000 --> 00:00:05.000 +- <b>track 2 - cue 2 bold</b> +- <i>track 2 - cue 2 italicized<i> diff --git a/toolkit/components/pictureinpicture/tests/test-webvtt-3.vtt b/toolkit/components/pictureinpicture/tests/test-webvtt-3.vtt new file mode 100644 index 0000000000..0207d9e65f --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-webvtt-3.vtt @@ -0,0 +1,11 @@ +WEBVTT + +Test file with multiple active cues and VTTCue.line as "auto" + +1 +00:00:00.000 --> 00:00:01.000 +track 3 - cue 1 + +2 +00:00:00.000 --> 00:00:01.000 +track 3 - cue 2 diff --git a/toolkit/components/pictureinpicture/tests/test-webvtt-4.vtt b/toolkit/components/pictureinpicture/tests/test-webvtt-4.vtt new file mode 100644 index 0000000000..9e4a540f7f --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-webvtt-4.vtt @@ -0,0 +1,15 @@ +WEBVTT + +Test file with multiple active cues and VTTCue.line as an integer + +1 +00:00:00.000 --> 00:00:01.000 line:2 +track 4 - cue 1 - integer line + +2 +00:00:00.000 --> 00:00:01.000 line:3 +track 4 - cue 2 - integer line + +3 +00:00:00.000 --> 00:00:01.000 line:1 +track 4 - cue 3 - integer line diff --git a/toolkit/components/pictureinpicture/tests/test-webvtt-5.vtt b/toolkit/components/pictureinpicture/tests/test-webvtt-5.vtt new file mode 100644 index 0000000000..3a25d83529 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/test-webvtt-5.vtt @@ -0,0 +1,12 @@ +WEBVTT + +Test file with multiple active cues and VTTCue.line as a percentage value + +00:00:00.000 --> 00:00:01.000 line:90% +track 5 - cue 1 - percent line + +00:00:00.000 --> 00:00:01.000 line:10% +track 5 - cue 2 - percent line + +00:00:00.000 --> 00:00:01.000 line:50% +track 5 - cue 3 - percent line |