diff options
Diffstat (limited to '')
59 files changed, 5084 insertions, 0 deletions
diff --git a/toolkit/components/pictureinpicture/tests/.eslintrc.js b/toolkit/components/pictureinpicture/tests/.eslintrc.js new file mode 100644 index 0000000000..1779fd7f1c --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test"], +}; diff --git a/toolkit/components/pictureinpicture/tests/browser.ini b/toolkit/components/pictureinpicture/tests/browser.ini new file mode 100644 index 0000000000..a11f4c7c5b --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser.ini @@ -0,0 +1,87 @@ +[DEFAULT] +support-files = + click-event-helper.js + head.js + test-button-overlay.html + test-opaque-overlay.html + test-page.html + test-page-with-iframe.html + test-page-with-sound.html + test-pointer-events-none.html + test-transparent-overlay-1.html + test-transparent-overlay-2.html + test-video-selection.html + test-reversed.html + test-media-stream.html + test-video.mp4 + test-video-cropped.mp4 + test-video-vertical.mp4 + test-video-long.mp4 + short.mp4 + ../../../../dom/media/test/gizmo.mp4 + ../../../../dom/media/test/owl.mp3 + +prefs = + media.videocontrols.picture-in-picture.enabled=true + media.videocontrols.picture-in-picture.allow-multiple=false + media.videocontrols.picture-in-picture.video-toggle.enabled=true + media.videocontrols.picture-in-picture.video-toggle.testing=true + media.videocontrols.picture-in-picture.video-toggle.always-show=true + media.videocontrols.picture-in-picture.video-toggle.has-used=true + +[browser_cannotTriggerFromContent.js] +[browser_closePipPause.js] +[browser_contextMenu.js] +skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205 +[browser_cornerSnapping.js] +run-if = os == "mac" +[browser_closePlayer.js] +[browser_closeTab.js] +[browser_dblclickFullscreen.js] +[browser_flipIconWithRTL.js] +[browser_mediaStreamVideos.js] +[browser_durationChange.js] +[browser_fullscreen.js] +skip-if = (os == "mac" && debug) || os == "linux" #Bug 1566173, Bug 1664667 +[browser_keyboardShortcut.js] +[browser_mouseButtonVariation.js] +skip-if = debug +[browser_noToggleOnAudio.js] +[browser_playerControls.js] +[browser_multiPip.js] +[browser_removeVideoElement.js] +[browser_rerequestPiP.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_tabIconOverlayPiP.js] +[browser_thirdPartyIframe.js] +[browser_toggleAfterTabTearOutIn.js] +skip-if = (os == 'linux' && bits == 64) || (os == 'mac' && !asan && !debug) # Bug 1605546 +[browser_toggleButtonOverlay.js] +skip-if = true # Bug 1546455 +[browser_toggleMode_2.js] +skip-if = (os == 'linux') # 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 +[browser_toggleSimple.js] +skip-if = os == 'linux' # Bug 1546455 +[browser_toggleTransparentOverlay-1.js] +[browser_toggleTransparentOverlay-2.js] +skip-if = os == 'linux' && (debug || asan) # Bug 1546930 +[browser_videoSelection.js] 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_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_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_contextMenu.js b/toolkit/components/pictureinpicture/tests/browser_contextMenu.js new file mode 100644 index 0000000000..b762049844 --- /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 => { + 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." + ); + + await EventUtils.synthesizeMouseAtCenter(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."); + + await openContextMenu(browser, videoId); + + info("Context menu is open again."); + + await EventUtils.synthesizeMouseAtCenter(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_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..c915a6f0e7 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_dblclickFullscreen.js @@ -0,0 +1,90 @@ +/* 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 () => { + await EventUtils.sendMouseEvent( + { + type: "dblclick", + }, + controls, + pipWin + ); + }); + + Assert.equal( + pipWin.document.fullscreenElement, + pipWin.document.body, + "Double-click caused us to enter fullscreen." + ); + + // First, we'll test exiting fullscreen by double-clicking again + // on the document body. + + await promiseFullscreenExited(pipWin, async () => { + await 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 () => { + await 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_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_fullscreen.js b/toolkit/components/pictureinpicture/tests/browser_fullscreen.js new file mode 100644 index 0000000000..5f80c56307 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_fullscreen.js @@ -0,0 +1,142 @@ +/* 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) { + await promiseFullscreenEntered(window, async () => { + await SpecialPowers.spawn(browser, [videoID], async videoID => { + let video = this.content.document.getElementById(videoID); + video.requestFullscreen(); + }); + }); + + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + let args = { videoID, toggleID: DEFAULT_TOGGLE_STYLES.rootID }; + + await promiseFullscreenExited(window, async () => { + 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.is_hidden(toggle), + "Toggle should be hidden in fullscreen mode." + ); + this.content.document.exitFullscreen(); + }); + }); + } + } + ); +}); + +/** + * 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 => { + await promiseFullscreenEntered(window, async () => { + await SpecialPowers.spawn(browser, [], async () => { + this.content.document.body.requestFullscreen(); + }); + }); + + for (let videoID of VIDEOS) { + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${videoID}`, + { + type: "mouseover", + }, + browser + ); + + 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.is_hidden(toggle), + "Toggle should be hidden in fullscreen mode." + ); + }); + } + + await promiseFullscreenExited(window, async () => { + await SpecialPowers.spawn(browser, [], async () => { + this.content.document.exitFullscreen(); + }); + }); + } + ); +}); + +/** + * 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."); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + + // need to focus first, since fullscreen request will be blocked otherwise + await SimpleTest.promiseFocus(window); + + await promiseFullscreenEntered(window, async () => { + await SpecialPowers.spawn(browser, [], async () => { + this.content.document.body.requestFullscreen(); + }); + }); + + await pipClosed; + ok(pipWin.closed, "Picture-In-Picture successfully closed."); + + await promiseFullscreenExited(window, async () => { + await SpecialPowers.spawn(browser, [], async () => { + this.content.document.exitFullscreen(); + }); + }); + } + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_keyboardShortcut.js b/toolkit/components/pictureinpicture/tests/browser_keyboardShortcut.js new file mode 100644 index 0000000000..b2f83205fa --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_keyboardShortcut.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * 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 => { + 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); + + // 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."); + } + ); +}); 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..f424093215 --- /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 assertSawMouseEvents(browser, false); + + 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..f4f48fc7a6 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_multiPip.js @@ -0,0 +1,232 @@ +/* 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"]; +} + +/** + * Set pref for multiple PiP support first + */ +add_task(async () => { + return SpecialPowers.pushPrefEnv({ + set: [["media.videocontrols.picture-in-picture.allow-multiple", true]], + }); +}); + +/** + * 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_noToggleOnAudio.js b/toolkit/components/pictureinpicture/tests/browser_noToggleOnAudio.js new file mode 100644 index 0000000000..0d16bdc9e2 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_noToggleOnAudio.js @@ -0,0 +1,45 @@ +/* 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_playerControls.js b/toolkit/components/pictureinpicture/tests/browser_playerControls.js new file mode 100644 index 0000000000..a2684dd451 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_playerControls.js @@ -0,0 +1,91 @@ +/* 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 isVideoMuted = () => { + return SpecialPowers.spawn(browser, [videoID], async videoID => { + return content.document.getElementById(videoID).muted; + }); + }; + 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()), "The audio is playing."); + EventUtils.synthesizeMouseAtCenter(audioButton, {}, pipWin); + await mutedPromise; + ok(await isVideoMuted(), "The audio is muted."); + + // Try the unmute button + let unmutedPromise = waitForVideoEvent("volumechange"); + EventUtils.synthesizeMouseAtCenter(audioButton, {}, pipWin); + await unmutedPromise; + ok(!(await isVideoMuted()), "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_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_rerequestPiP.js b/toolkit/components/pictureinpicture/tests/browser_rerequestPiP.js new file mode 100644 index 0000000000..27d2861bca --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_rerequestPiP.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if a pre-existing Picture-in-Picture window exists, and a + * different video is requested to open in Picture-in-Picture, that the + * original Picture-in-Picture window closes and a new one is opened. + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let pipWin = await triggerPictureInPicture(browser, "with-controls"); + ok(pipWin, "Got Picture-in-Picture window."); + + let pipClosed = BrowserTestUtils.domWindowClosed(pipWin); + let pipWin2 = await triggerPictureInPicture(browser, "no-controls"); + await pipClosed; + ok(true, "Original Picture-in-Picture window closed."); + + pipClosed = BrowserTestUtils.domWindowClosed(pipWin2); + pipWin2.close(); + await pipClosed; + } + ); +}); diff --git a/toolkit/components/pictureinpicture/tests/browser_resizeVideo.js b/toolkit/components/pictureinpicture/tests/browser_resizeVideo.js new file mode 100644 index 0000000000..917bceb9ac --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_resizeVideo.js @@ -0,0 +1,263 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * 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 == 0) { + 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 + ); +} + +/** + * 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(0, 0); + + 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..fbdf7fb456 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_saveLastPiPLoc.js @@ -0,0 +1,362 @@ +/* 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. + */ + +add_task(async () => { + 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; + } + // This function is used because the rounding of the width can be off + // by about 1 pixel sometimes so this checks that val1 and val2 are + // within 1 pixel + function checkIfEqual(val1, val2, str) { + let equal = Math.abs(val1 - val2); + if (equal <= 1) { + is(equal <= 1, true, str); + } else { + is(val1, val2, str); + } + } + + // Used for clearing the size and location of the PiP window + const PLAYER_URI = + "chrome://global/content/pictureinpicture/player.xhtml"; + + // The PiP window now stores information between tests and needs to be + // cleared before the test begins + function clearSaved() { + 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 getAvailScreenSize(screen) { + let screenLeft = {}, + screenTop = {}, + screenWidth = {}, + screenHeight = {}; + screen.GetAvailRectDisplayPix( + screenLeft, + screenTop, + screenWidth, + screenHeight + ); + let fullLeft = {}, + fullTop = {}, + fullWidth = {}, + fullHeight = {}; + screen.GetRectDisplayPix(fullLeft, fullTop, fullWidth, fullHeight); + + // 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 = + (screenLeft.value - fullLeft.value) * scaleFactor + fullLeft.value; + screenTop.value = + (screenTop.value - fullTop.value) * scaleFactor + fullTop.value; + + 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 + clearSaved(); + + // 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 + checkIfEqual( + pipWin.screenX, + rightEdge - defaultPiPWidth, + "Default PiP X location" + ); + if (AppConstants.platform == "linux") { + checkIfEqual( + pipWin.screenY, + bottomEdge - defaultPiPHeight - tabHeight, + "Default PiP Y location" + ); + } else { + checkIfEqual( + pipWin.screenY, + bottomEdge - defaultPiPHeight, + "Default PiP Y location" + ); + } + checkIfEqual(pipWin.innerHeight, defaultPiPHeight, "Default PiP height"); + checkIfEqual(pipWin.innerWidth, defaultPiPWidth, "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 + checkIfEqual(pipWin.screenX, left, "Opened at last X location"); + checkIfEqual(pipWin.screenY, top, "Opened at last Y location"); + checkIfEqual( + pipWin.innerHeight, + height, + "Opened with 1/2 default height" + ); + checkIfEqual(pipWin.innerWidth, width, "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 + checkIfEqual( + pipWin.screenX, + rightEdge - defaultPiPWidth, + "Opened at default X location" + ); + checkIfEqual( + pipWin.screenY, + bottomEdge - defaultPiPHeight, + "Opened at default Y location" + ); + checkIfEqual( + pipWin.innerWidth, + defaultPiPWidth, + "Opened at default PiP width" + ); + checkIfEqual( + pipWin.innerHeight, + defaultPiPHeight, + "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."); + + checkIfEqual(pipWin.screenX, left, "Opened at last X location"); + checkIfEqual(pipWin.screenY, top, "Opened at last Y location"); + checkIfEqual( + pipWin.innerHeight, + height, + "Opened height with previous width" + ); + checkIfEqual( + pipWin.innerWidth, + height * (pipWin.innerWidth / pipWin.innerHeight), + "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."); + + checkIfEqual(pipWin.screenX, left, "Opened at last X location"); + checkIfEqual(pipWin.screenY, top, "Opened at last Y location"); + checkIfEqual(pipWin.innerHeight, height, "Opened with previous height"); + checkIfEqual( + pipWin.innerWidth, + height * (pipWin.innerWidth / pipWin.innerHeight), + "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 + checkIfEqual( + pipWin.screenX, + left, + "Opened at last X location but shifted back on screen" + ); + if (AppConstants.platform == "linux") { + checkIfEqual( + pipWin.screenY, + top - tabHeight, + "Opened at last Y location but shifted back on screen" + ); + } else { + checkIfEqual( + pipWin.screenY, + top, + "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 + checkIfEqual( + pipWin.screenX, + defaultX, + "Opened at last X location but shifted back on screen" + ); + checkIfEqual( + pipWin.screenY, + defaultY, + "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; + checkIfEqual( + pipWin.innerWidth + pipWin.screenX, + rightEdge, + "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."); + + checkIfEqual( + pipWin.innerWidth < width, + true, + "New video width is smaller" + ); + checkIfEqual( + pipWin.innerWidth + pipWin.screenX, + rightEdge, + "Video is on right edge after video is changed" + ); + } + + await ensureMessageAndClosePiP(browser, "with-controls", pipWin, true); + } + ); +}); 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..0c2a392d9f --- /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": 0.8, + }, + hidden: [], + }, + + hoverToggle: { + opacities: { + ".pip-wrapper": 1.0, + }, + hidden: [".pip-expanded"], + }, + }, + }; + + const TOGGLE_LARGE = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-small": 0.0, + ".pip-wrapper": 0.8, + ".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_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_thirdPartyIframe.js b/toolkit/components/pictureinpicture/tests/browser_thirdPartyIframe.js new file mode 100644 index 0000000000..cf63768d85 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_thirdPartyIframe.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * 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: TEST_PAGE_WITH_IFRAME, + gBrowser, + }, + async browser => { + // TEST_PAGE_WITH_IFRAME is hosted at a different domain from TEST_PAGE, + // so loading TEST_PAGE within the iframe will act as our third-party + // iframe. + await SpecialPowers.spawn(browser, [TEST_PAGE], async TEST_PAGE => { + let iframe = content.document.getElementById("iframe"); + let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load"); + iframe.src = 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); + } + ); + } +}); 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_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..e7a62f6278 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_toggleMode_2.js @@ -0,0 +1,202 @@ +/* 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": 0.8, + ".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": 0.8, + ".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": 0.8, + }, + hidden: [".pip-expanded"], + }, + + hoverToggle: { + opacities: { + ".pip-wrapper": 1.0, + }, + hidden: [".pip-expanded"], + }, + }, +}; + +const TOGGLE_STYLES_RIGHT_SMALL = { + rootID: "pictureInPictureToggle", + stages: { + hoverVideo: { + opacities: { + ".pip-wrapper": 0.8, + }, + 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, + }, + }); + + 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, + }, + }); + + 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..7163966e64 --- /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.import( + "resource://testing-common/ContentTaskUtils.jsm" + ); + 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..165fa1c72b --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_togglePolicies.js @@ -0,0 +1,126 @@ +/* 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.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_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_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..8df9062263 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/head.js @@ -0,0 +1,835 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TOGGLE_POLICIES } = ChromeUtils.import( + "resource://gre/modules/PictureInPictureControls.jsm" +); + +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 WINDOW_TYPE = "Toolkit:PictureInPicture"; +const TOGGLE_POSITION_PREF = + "media.videocontrols.picture-in-picture.video-toggle.position"; +const HAS_USED_PREF = + "media.videocontrols.picture-in-picture.video-toggle.has-used"; +const SHARED_DATA_KEY = "PictureInPicture:SiteOverrides"; + +/** + * 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": 0.8, + }, + 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. + * + * @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. + * + * @return Promise + * @resolves With the Picture-in-Picture window when ready. + */ +async function triggerPictureInPicture(browser, videoID) { + let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null); + let 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 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, videoId) { + return SpecialPowers.spawn(browser, [videoId], async videoID => { + let video = content.document.getElementById(videoID); + 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) + * + * @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) { + 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.is_hidden(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.jsm. + * 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.import( + "resource://gre/modules/PictureInPictureControls.jsm" + ); + 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." + ); +} + +/** + * 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.import( + "resource://gre/actors/PictureInPictureChild.jsm" + ); + 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.import("resource://gre/modules/Geometry.jsm"); + + 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, + }; + }); +} + +/** + * 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.jsm. + * + * 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 }] of Object.entries( + expectations + )) { + await SimpleTest.promiseFocus(browser); + info(`Testing video with id: ${videoID}`); + + await testToggleHelper( + browser, + videoID, + canToggle, + policy, + toggleStyles + ); + } + } + ); +} + +/** + * 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.jsm. + * @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 +) { + 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); + + // 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"); + }); +} + +/** + * 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; + }); +} 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-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-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.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 |