Bug 1540835 - Add some automated tests for the Picture-in-Picture toggle. r=Felipe
authorMike Conley <mconley@mozilla.com>
Thu, 25 Apr 2019 03:35:29 +0000
changeset 530055 3fe91f0bfbffb18239163dd183eb99b63d350798
parent 530054 1eae82ecc532a73b1815c6fdabbda16a8d28d4b4
child 530056 97f1fb7c02bd96ded2c90917085bf75071a00199
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersFelipe
bugs1540835
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1540835 - Add some automated tests for the Picture-in-Picture toggle. r=Felipe Depends on D27001 Differential Revision: https://phabricator.services.mozilla.com/D27002
toolkit/actors/PictureInPictureChild.jsm
toolkit/components/pictureinpicture/tests/browser.ini
toolkit/components/pictureinpicture/tests/browser_toggleButtonOverlay.js
toolkit/components/pictureinpicture/tests/browser_toggleOpaqueOverlay.js
toolkit/components/pictureinpicture/tests/browser_toggleSimple.js
toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-1.js
toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-2.js
toolkit/components/pictureinpicture/tests/head.js
toolkit/components/pictureinpicture/tests/test-button-overlay.html
toolkit/components/pictureinpicture/tests/test-opaque-overlay.html
toolkit/components/pictureinpicture/tests/test-transparent-nested-iframes.html
toolkit/components/pictureinpicture/tests/test-transparent-overlay-1.html
toolkit/components/pictureinpicture/tests/test-transparent-overlay-2.html
toolkit/themes/shared/media/videocontrols.css
--- a/toolkit/actors/PictureInPictureChild.jsm
+++ b/toolkit/actors/PictureInPictureChild.jsm
@@ -13,40 +13,47 @@ ChromeUtils.defineModuleGetter(this, "De
   "resource://gre/modules/DeferredTask.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
 
 const TOGGLE_ENABLED_PREF =
   "media.videocontrols.picture-in-picture.video-toggle.enabled";
+const TOGGLE_TESTING_PREF =
+  "media.videocontrols.picture-in-picture.video-toggle.testing";
 const MOUSEMOVE_PROCESSING_DELAY_MS = 50;
 
 // A weak reference to the most recent <video> in this content
 // process that is being viewed in Picture-in-Picture.
 var gWeakVideo = null;
 // A weak reference to the content window of the most recent
 // Picture-in-Picture window for this content process.
 var gWeakPlayerContent = null;
+// To make it easier to write tests, we have a process-global
+// WeakSet of all <video> elements that are being tracked for
+// mouseover
+var gWeakIntersectingVideosForTesting = new WeakSet();
 
 /**
  * The PictureInPictureToggleChild is responsible for displaying the overlaid
  * Picture-in-Picture toggle over top of <video> elements that the mouse is
  * hovering.
  */
 class PictureInPictureToggleChild extends ActorChild {
   constructor(dispatcher) {
     super(dispatcher);
     // We need to maintain some state about various things related to the
     // Picture-in-Picture toggles - however, for now, the same
     // PictureInPictureToggleChild might be re-used for different documents.
     // We keep the state stashed inside of this WeakMap, keyed on the document
     // itself.
     this.weakDocStates = new WeakMap();
     this.toggleEnabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF);
+    this.toggleTesting = Services.prefs.getBoolPref(TOGGLE_TESTING_PREF, false);
   }
 
   /**
    * Returns the state for the current document referred to via
    * this.content.document. If no such state exists, creates it, stores it
    * and returns it.
    */
   get docState() {
@@ -145,31 +152,50 @@ class PictureInPictureToggleChild extend
     let state = this.docState;
     let oldVisibleVideosCount = state.visibleVideosCount;
     for (let entry of entries) {
       let video = entry.target;
       if (this.worthTracking(entry)) {
         if (!state.weakVisibleVideos.has(video)) {
           state.weakVisibleVideos.add(video);
           state.visibleVideosCount++;
+          if (this.toggleTesting) {
+            gWeakIntersectingVideosForTesting.add(video);
+          }
         }
       } else if (state.weakVisibleVideos.has(video)) {
         state.weakVisibleVideos.delete(video);
         state.visibleVideosCount--;
+        if (this.toggleTesting) {
+          gWeakIntersectingVideosForTesting.delete(video);
+        }
       }
     }
 
+    // For testing, especially in debug or asan builds, we might not
+    // run this idle callback within an acceptable time. While we're
+    // testing, we'll bypass the idle callback performance optimization
+    // and run our callbacks as soon as possible during the next idle
+    // period.
     if (!oldVisibleVideosCount && state.visibleVideosCount) {
-      this.content.requestIdleCallback(() => {
+      if (this.toggleTesting) {
         this.beginTrackingMouseOverVideos();
-      });
+      } else {
+        this.content.requestIdleCallback(() => {
+          this.beginTrackingMouseOverVideos();
+        });
+      }
     } else if (oldVisibleVideosCount && !state.visibleVideosCount) {
-      this.content.requestIdleCallback(() => {
+      if (this.toggleTesting) {
         this.stopTrackingMouseOverVideos();
-      });
+      } else {
+        this.content.requestIdleCallback(() => {
+          this.stopTrackingMouseOverVideos();
+        });
+      }
     }
   }
 
   /**
    * One of the challenges of displaying this toggle is that many sites put
    * things over top of <video> elements, like custom controls, or images, or
    * all manner of things that might intercept mouseevents that would normally
    * fire directly on the <video>. In order to properly detect when the mouse
@@ -408,16 +434,24 @@ class PictureInPictureToggleChild extend
     let toggleRect =
       toggle.ownerGlobal.windowUtils.getBoundsWithoutFlushing(toggle);
     let { clientX, clientY } = event;
     return clientX >= toggleRect.left &&
            clientX <= toggleRect.right &&
            clientY >= toggleRect.top &&
            clientY <= toggleRect.bottom;
   }
+
+  /**
+   * This is a test-only function that returns true if a video is being tracked
+   * for mouseover events after having intersected the viewport.
+   */
+  static isTracking(video) {
+    return gWeakIntersectingVideosForTesting.has(video);
+  }
 }
 
 class PictureInPictureChild extends ActorChild {
   static videoIsPlaying(video) {
     return !!(video.currentTime > 0 && !video.paused && !video.ended && video.readyState > 2);
   }
 
   handleEvent(event) {
--- a/toolkit/components/pictureinpicture/tests/browser.ini
+++ b/toolkit/components/pictureinpicture/tests/browser.ini
@@ -1,14 +1,29 @@
 [DEFAULT]
 support-files =
   head.js
+  test-button-overlay.html
   test-page.html
+  test-opaque-overlay.html
+  test-transparent-overlay-1.html
+  test-transparent-overlay-2.html
   test-video.mp4
 
 prefs =
   media.videocontrols.picture-in-picture.enabled=true
+  media.videocontrols.picture-in-picture.video-toggle.enabled=true
+  media.videocontrols.picture-in-picture.video-toggle.testing=true
 
 [browser_cannotTriggerFromContent.js]
 [browser_contextMenu.js]
 [browser_closeTab.js]
 [browser_rerequestPiP.js]
 [browser_showMessage.js]
+[browser_toggleButtonOverlay.js]
+skip-if = true # Bug 1546455
+[browser_toggleOpaqueOverlay.js]
+skip-if = true # Bug 1546455
+[browser_toggleSimple.js]
+skip-if = os == 'linux' # Bug 1546455
+[browser_toggleTransparentOverlay-1.js]
+[browser_toggleTransparentOverlay-2.js]
+
new file mode 100644
--- /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 },
+  });
+});
new file mode 100644
--- /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 },
+  });
+});
new file mode 100644
--- /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 },
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-1.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 can appear and be clicked
+ * when the video is overlaid with transparent elements.
+ */
+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 },
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/pictureinpicture/tests/browser_toggleTransparentOverlay-2.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 appear and be clicked
+ * when the video is overlaid with elements that have zero and
+ * partial opacity.
+ */
+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 },
+  });
+});
--- a/toolkit/components/pictureinpicture/tests/head.js
+++ b/toolkit/components/pictureinpicture/tests/head.js
@@ -1,28 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TEST_ROOT = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
 const TEST_PAGE = TEST_ROOT + "test-page.html";
 const WINDOW_TYPE = "Toolkit:PictureInPicture";
+const TOGGLE_ID = "pictureInPictureToggleButton";
+const HOVER_VIDEO_OPACITY = 0.8;
+const HOVER_TOGGLE_OPACITY = 1.0;
 
 /**
  * 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} browser The <xul:browser> hosting the <video>
  *
  * @param {String} videoID The ID of the video to trigger
  * Picture-in-Picture on.
  *
- * @returns Promise
+ * @return Promise
  * @resolves With the Picture-in-Picture window when ready.
  */
 async function triggerPictureInPicture(browser, videoID) {
   let domWindowOpened = BrowserTestUtils.domWindowOpened(null);
   let videoReady = ContentTask.spawn(browser, videoID, async videoID => {
     let video = content.document.getElementById(videoID);
     let event = new content.CustomEvent("MozTogglePictureInPicture", { bubbles: true });
     video.dispatchEvent(event);
@@ -43,24 +46,160 @@ async function triggerPictureInPicture(b
  *
  * @param {Element} browser The <xul:browser> 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.
  *
- * @returns Promise
+ * @return Promise
  * @resolves When the checks have completed.
  */
 async function assertShowingMessage(browser, videoID, expected) {
   let showing = await ContentTask.spawn(browser, videoID, async videoID => {
     let video = content.document.getElementById(videoID);
     let shadowRoot = video.openOrClosedShadowRoot;
     let pipOverlay = shadowRoot.querySelector(".pictureInPictureOverlay");
     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.");
 }
+
+async function toggleOpacityReachesThreshold(browser, videoID, opacityThreshold) {
+  let args = { videoID, TOGGLE_ID, opacityThreshold };
+  await ContentTask.spawn(browser, args, async args => {
+    let { videoID, TOGGLE_ID, opacityThreshold } = args;
+    let video = content.document.getElementById(videoID);
+    let shadowRoot = video.openOrClosedShadowRoot;
+    let toggle = shadowRoot.getElementById(TOGGLE_ID);
+
+    await ContentTaskUtils.waitForCondition(() => {
+      let opacity = parseFloat(this.content.getComputedStyle(toggle).opacity);
+      return opacity >= opacityThreshold;
+    }, `Toggle should have opacity >= ${opacityThreshold}`, 100, 100);
+
+    ok(true, "Toggle reached target opacity.");
+  });
+}
+
+/**
+ * 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
+ *   }
+ * 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.
+ *
+ * @return Promise
+ * @resolves When the test is complete and the tab with the loaded page is
+ * removed.
+ */
+async function testToggle(testURL, expectations) {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: testURL,
+  }, async browser => {
+    let videoIDs = Object.keys(expectations);
+
+    // 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 ContentTask.spawn(browser, videoIDs, async videoIDs => {
+      for (let videoID of videoIDs) {
+        let video = content.document.getElementById(videoID);
+        if (video.readyState < content.HTMLMediaElement.HAVE_ENOUGH_DATA) {
+          await ContentTaskUtils.waitForEvent(video, "canplay");
+        }
+      }
+    });
+
+    for (let videoID of videoIDs) {
+      await SimpleTest.promiseFocus(browser);
+      info(`Testing video with id: ${videoID}`);
+
+      // 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, TOGGLE_ID };
+      let toggleClientRect = await ContentTask.spawn(browser, args, async args => {
+        let { videoID, TOGGLE_ID } = args;
+        let video = content.document.getElementById(videoID);
+        video.scrollIntoView({ behaviour: "instant" });
+        let shadowRoot = video.openOrClosedShadowRoot;
+        let toggle = shadowRoot.getElementById(TOGGLE_ID);
+
+        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 rect = toggle.getBoundingClientRect();
+        return { top: rect.top, right: rect.right, left: rect.left, bottom: rect.bottom };
+      });
+
+      // 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("Waiting for toggle to become visible");
+      await toggleOpacityReachesThreshold(browser, videoID, HOVER_VIDEO_OPACITY);
+
+      info("Hovering the toggle rect now.");
+      let toggleLeft = toggleClientRect.left + 2;
+      let toggleTop = toggleClientRect.top + 2;
+      await BrowserTestUtils.synthesizeMouseAtPoint(toggleLeft, toggleTop, {
+        type: "mousemove",
+      }, browser);
+      await BrowserTestUtils.synthesizeMouseAtPoint(toggleLeft, toggleTop, {
+        type: "mouseover",
+      }, browser);
+
+      await toggleOpacityReachesThreshold(browser, videoID, HOVER_TOGGLE_OPACITY);
+
+      if (expectations[videoID].canToggle) {
+        info("Clicking on toggle, and expecting a Picture-in-Picture window to open");
+        let domWindowOpened = BrowserTestUtils.domWindowOpened(null);
+        await BrowserTestUtils.synthesizeMouseAtPoint(toggleLeft, toggleTop, {}, browser);
+        let win = await domWindowOpened;
+        ok(win, "A Picture-in-Picture window opened.");
+        await BrowserTestUtils.closeWindow(win);
+      } else {
+        info("Clicking on toggle, and expecting no Picture-in-Picture window opens");
+        await BrowserTestUtils.synthesizeMouseAtPoint(toggleLeft, toggleTop, {}, browser);
+
+        // 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.");
+      }
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/pictureinpicture/tests/test-button-overlay.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Picture-in-Picture test - transparent overlays - 1</title>
+</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>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/pictureinpicture/tests/test-opaque-overlay.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Picture-in-Picture test - transparent overlays - 1</title>
+</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>
new file mode 100644
--- /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>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/pictureinpicture/tests/test-transparent-overlay-1.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Picture-in-Picture test - transparent overlays - 1</title>
+</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>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/pictureinpicture/tests/test-transparent-overlay-2.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Picture-in-Picture test - transparent overlays - 1</title>
+</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>
--- a/toolkit/themes/shared/media/videocontrols.css
+++ b/toolkit/themes/shared/media/videocontrols.css
@@ -477,16 +477,26 @@
   opacity: 0.8;
 }
 
 .controlsOverlay:hover > .pictureInPictureToggleButton:hover {
   opacity: 1;
   transform: translateY(-50%) translateX(0);
 }
 
+@supports -moz-bool-pref("media.videocontrols.picture-in-picture.video-toggle.testing") {
+  /**
+   * To make automated tests faster, we drop the transition duration in
+   * testing mode.
+   */
+  .pictureInPictureToggleButton {
+    transition-duration: 10ms;
+  }
+}
+
 /* Overlay Play button */
 .clickToPlay {
   min-width: var(--clickToPlay-size);
   min-height: var(--clickToPlay-size);
   border-radius: 50%;
   background-image: url(chrome://global/skin/media/playButton.svg);
   background-repeat: no-repeat;
   background-position: 54% 50%;