Source code

Revision control

Copy as Markdown

Other Tools

/* Any copyright is dedicated to the Public Domain.
"use strict";
const { TOGGLE_POLICIES } = ChromeUtils.importESModule(
);
const TEST_ROOT = getRootDirectory(gTestPath).replace(
);
const TEST_ROOT_2 = getRootDirectory(gTestPath).replace(
);
const TEST_PAGE = TEST_ROOT + "test-page.html";
const TEST_PAGE_2 = TEST_ROOT_2 + "test-page.html";
const TEST_PAGE_WITH_IFRAME = TEST_ROOT_2 + "test-page-with-iframe.html";
const TEST_PAGE_WITH_SOUND = TEST_ROOT + "test-page-with-sound.html";
const TEST_PAGE_WITHOUT_AUDIO = TEST_ROOT + "test-page-without-audio.html";
const TEST_PAGE_WITH_NAN_VIDEO_DURATION =
TEST_ROOT + "test-page-with-nan-video-duration.html";
const TEST_PAGE_WITH_WEBVTT = TEST_ROOT + "test-page-with-webvtt.html";
const TEST_PAGE_MULTIPLE_CONTEXTS =
TEST_ROOT + "test-page-multiple-contexts.html";
const TEST_PAGE_TRANSPARENT_NESTED_IFRAMES =
TEST_ROOT + "test-transparent-nested-iframes.html";
const TEST_PAGE_PIP_DISABLED = TEST_ROOT + "test-page-pipDisabled.html";
const WINDOW_TYPE = "Toolkit:PictureInPicture";
const TOGGLE_POSITION_PREF =
"media.videocontrols.picture-in-picture.video-toggle.position";
/* As of Bug 1811312, 80% toggle opacity is for the PiP toggle experiment control. */
const DEFAULT_TOGGLE_OPACITY = 0.8;
const HAS_USED_PREF =
"media.videocontrols.picture-in-picture.video-toggle.has-used";
const SHARED_DATA_KEY = "PictureInPicture:SiteOverrides";
// Used for clearing the size and location of the PiP window
const ACCEPTABLE_DIFFERENCE = 2;
/**
* We currently ship with a few different variations of the
* Picture-in-Picture toggle. The tests for Picture-in-Picture include tests
* that check the style rules of various parts of the toggle. Since each toggle
* variation has different style rules, we introduce a structure here to
* describe the appearance of the toggle at different stages for the tests.
*
* The top-level structure looks like this:
*
* {
* rootID (String): The ID of the root element of the toggle.
* stages (Object): An Object representing the styles of the toggle at
* different stages of its use. Each property represents a different
* stage that can be tested. Right now, those stages are:
*
* hoverVideo:
* When the mouse is hovering the video but not the toggle.
*
* hoverToggle:
* When the mouse is hovering both the video and the toggle.
*
* Both stages must be assigned an Object with the following properties:
*
* opacities:
* This should be set to an Object where the key is a CSS selector for
* an element, and the value is a double for what the eventual opacity
* of that element should be set to.
*
* hidden:
* This should be set to an Array of CSS selector strings for elements
* that should be hidden during a particular stage.
* }
*
* DEFAULT_TOGGLE_STYLES is the set of styles for the default variation of the
* toggle.
*/
const DEFAULT_TOGGLE_STYLES = {
rootID: "pictureInPictureToggle",
stages: {
hoverVideo: {
opacities: {
".pip-wrapper": DEFAULT_TOGGLE_OPACITY,
},
hidden: [".pip-expanded"],
},
hoverToggle: {
opacities: {
".pip-wrapper": 1.0,
},
hidden: [".pip-expanded"],
},
},
};
/**
* Given a browser and the ID for a <video> element, triggers
* Picture-in-Picture for that <video>, and resolves with the
* Picture-in-Picture window once it is ready to be used.
*
* If triggerFn is not specified, then open using the
* MozTogglePictureInPicture event.
*
* @param {Element,BrowsingContext} browser The <xul:browser> or
* BrowsingContext hosting the <video>
*
* @param {String} videoID The ID of the video to trigger
* Picture-in-Picture on.
*
* @param {boolean} triggerFn Use the given function to open the pip window,
* which runs in the parent process.
*
* @return Promise
* @resolves With the Picture-in-Picture window when ready.
*/
async function triggerPictureInPicture(browser, videoID, triggerFn) {
let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null);
let videoReady = null;
if (triggerFn) {
await SpecialPowers.spawn(browser, [videoID], async videoID => {
let video = content.document.getElementById(videoID);
video.focus();
});
triggerFn();
videoReady = SpecialPowers.spawn(browser, [videoID], async videoID => {
let video = content.document.getElementById(videoID);
await ContentTaskUtils.waitForCondition(() => {
return video.isCloningElementVisually;
}, "Video is being cloned visually.");
});
} else {
videoReady = SpecialPowers.spawn(browser, [videoID], async videoID => {
let video = content.document.getElementById(videoID);
let event = new content.CustomEvent("MozTogglePictureInPicture", {
bubbles: true,
});
video.dispatchEvent(event);
await ContentTaskUtils.waitForCondition(() => {
return video.isCloningElementVisually;
}, "Video is being cloned visually.");
});
}
let win = await domWindowOpened;
await Promise.all([
SimpleTest.promiseFocus(win),
win.promiseDocumentFlushed(() => {}),
videoReady,
]);
return win;
}
/**
* Given a browser and the ID for a <video> element, checks that the
* video is showing the "This video is playing in Picture-in-Picture mode."
* status message overlay.
*
* @param {Element,BrowsingContext} browser The <xul:browser> or
* BrowsingContext hosting the <video>
*
* @param {String} videoID The ID of the video to trigger
* Picture-in-Picture on.
*
* @param {bool} expected True if we expect the message to be showing.
*
* @return Promise
* @resolves When the checks have completed.
*/
async function assertShowingMessage(browser, videoID, expected) {
let showing = await SpecialPowers.spawn(browser, [videoID], async videoID => {
let video = content.document.getElementById(videoID);
let shadowRoot = video.openOrClosedShadowRoot;
let pipOverlay = shadowRoot.querySelector(".pictureInPictureOverlay");
Assert.ok(pipOverlay, "Should be able to find Picture-in-Picture overlay.");
let rect = pipOverlay.getBoundingClientRect();
return rect.height > 0 && rect.width > 0;
});
Assert.equal(
showing,
expected,
"Video should be showing the expected state."
);
}
/**
* Tests if a video is currently being cloned for a given content browser. Provides a
* good indicator for answering if this video is currently open in PiP.
*
* @param {Browser} browser
* The content browser or browsing contect that the video lives in
* @param {string} videoId
* The id associated with the video
*
* @returns {bool}
* Whether the video is currently being cloned (And is most likely open in PiP)
*/
function assertVideoIsBeingCloned(browser, selector) {
return SpecialPowers.spawn(browser, [selector], async slctr => {
let video = content.document.querySelector(slctr);
await ContentTaskUtils.waitForCondition(() => {
return video.isCloningElementVisually;
}, "Video is being cloned visually.");
});
}
/**
* Ensures that each of the videos loaded inside of a document in a
* <browser> have reached the HAVE_ENOUGH_DATA readyState.
*
* @param {Element} browser The <xul:browser> hosting the <video>(s) or the browsing context
*
* @return Promise
* @resolves When each <video> is in the HAVE_ENOUGH_DATA readyState.
*/
async function ensureVideosReady(browser) {
// PictureInPictureToggleChild waits for videos to fire their "canplay"
// event before considering them for the toggle, so we start by making
// sure each <video> has done this.
info(`Waiting for videos to be ready`);
await SpecialPowers.spawn(browser, [], async () => {
let videos = this.content.document.querySelectorAll("video");
for (let video of videos) {
video.currentTime = 0;
if (video.readyState < content.HTMLMediaElement.HAVE_ENOUGH_DATA) {
info(`Waiting for 'canplaythrough' for '${video.id}'`);
await ContentTaskUtils.waitForEvent(video, "canplaythrough");
}
}
});
}
/**
* Tests that the toggle opacity reaches or exceeds a certain threshold within
* a reasonable time.
*
* @param {Element} browser The <xul:browser> that has the <video> in it.
* @param {String} videoID The ID of the video element that we expect the toggle
* to appear on.
* @param {String} stage The stage for which the opacity is going to change. This
* should be one of "hoverVideo" or "hoverToggle".
* @param {Object} toggleStyles Optional argument. See the documentation for the
* DEFAULT_TOGGLE_STYLES object for a sense of what styleRules is expected to be.
*
* @return Promise
* @resolves When the check has completed.
*/
async function toggleOpacityReachesThreshold(
browser,
videoID,
stage,
toggleStyles = DEFAULT_TOGGLE_STYLES
) {
let togglePosition = Services.prefs.getStringPref(
TOGGLE_POSITION_PREF,
"right"
);
let hasUsed = Services.prefs.getBoolPref(HAS_USED_PREF, false);
let toggleStylesForStage = toggleStyles.stages[stage];
info(
`Testing toggle for stage ${stage} ` +
`in position ${togglePosition}, has used: ${hasUsed}`
);
let args = { videoID, toggleStylesForStage, togglePosition, hasUsed };
await SpecialPowers.spawn(browser, [args], async args => {
let { videoID, toggleStylesForStage } = args;
let video = content.document.getElementById(videoID);
let shadowRoot = video.openOrClosedShadowRoot;
for (let hiddenElement of toggleStylesForStage.hidden) {
let el = shadowRoot.querySelector(hiddenElement);
ok(
ContentTaskUtils.isHidden(el),
`Expected ${hiddenElement} to be hidden.`
);
}
for (let opacityElement in toggleStylesForStage.opacities) {
let opacityThreshold = toggleStylesForStage.opacities[opacityElement];
let el = shadowRoot.querySelector(opacityElement);
await ContentTaskUtils.waitForCondition(
() => {
let opacity = parseFloat(this.content.getComputedStyle(el).opacity);
return opacity >= opacityThreshold;
},
`Toggle element ${opacityElement} should have eventually reached ` +
`target opacity ${opacityThreshold}`,
100,
100
);
}
ok(true, "Toggle reached target opacity.");
});
}
/**
* Tests that the toggle has the correct policy attribute set. This should be called
* either when the toggle is visible, or events have been queued such that the toggle
* will soon be visible.
*
* @param {Element} browser The <xul:browser> that has the <video> in it.
* @param {String} videoID The ID of the video element that we expect the toggle
* to appear on.
* @param {Number} policy Optional argument. If policy is defined, then it should
* be one of the values in the TOGGLE_POLICIES from PictureInPictureControls.sys.mjs.
* If undefined, this function will ensure no policy attribute is set.
*
* @return Promise
* @resolves When the check has completed.
*/
async function assertTogglePolicy(
browser,
videoID,
policy,
toggleStyles = DEFAULT_TOGGLE_STYLES
) {
let toggleID = toggleStyles.rootID;
let args = { videoID, toggleID, policy };
await SpecialPowers.spawn(browser, [args], async args => {
let { videoID, toggleID, policy } = args;
let video = content.document.getElementById(videoID);
let shadowRoot = video.openOrClosedShadowRoot;
let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
let toggle = shadowRoot.getElementById(toggleID);
await ContentTaskUtils.waitForCondition(() => {
return controlsOverlay.classList.contains("hovering");
}, "Waiting for the hovering state to be set on the video.");
if (policy) {
const { TOGGLE_POLICY_STRINGS } = ChromeUtils.importESModule(
);
let policyAttr = toggle.getAttribute("policy");
Assert.equal(
policyAttr,
TOGGLE_POLICY_STRINGS[policy],
"The correct toggle policy is set."
);
} else {
Assert.ok(
!toggle.hasAttribute("policy"),
"No toggle policy should be set."
);
}
});
}
/**
* Tests that either all or none of the expected mousebutton events
* fire in web content when clicking on the page.
*
* Note: This function will only work on pages that load the
* click-event-helper.js script.
*
* @param {Element} browser The <xul:browser> that will receive the mouse
* events.
* @param {bool} isExpectingEvents True if we expect all of the normal
* mouse button events to fire. False if we expect none of them to fire.
* @param {bool} isExpectingClick True if the mouse events should include the
* "click" event, which is only included when the primary mouse button is pressed.
* @return Promise
* @resolves When the check has completed.
*/
async function assertSawMouseEvents(
browser,
isExpectingEvents,
isExpectingClick = true
) {
const MOUSE_BUTTON_EVENTS = [
"pointerdown",
"mousedown",
"pointerup",
"mouseup",
];
if (isExpectingClick) {
MOUSE_BUTTON_EVENTS.push("click");
}
let mouseEvents = await SpecialPowers.spawn(browser, [], async () => {
return this.content.wrappedJSObject.getRecordedEvents();
});
let expectedEvents = isExpectingEvents ? MOUSE_BUTTON_EVENTS : [];
Assert.deepEqual(
mouseEvents,
expectedEvents,
"Expected to get the right mouse events."
);
}
/**
* Tests that a click event is fire in web content when clicking on the page.
*
* Note: This function will only work on pages that load the
* click-event-helper.js script.
*
* @param {Element} browser The <xul:browser> that will receive the mouse
* events.
* @return Promise
* @resolves When the check has completed.
*/
async function assertSawClickEventOnly(browser) {
let mouseEvents = await SpecialPowers.spawn(browser, [], async () => {
return this.content.wrappedJSObject.getRecordedEvents();
});
Assert.deepEqual(
mouseEvents,
["click"],
"Expected to get the right mouse events."
);
}
/**
* Ensures that a <video> inside of a <browser> is scrolled into view,
* and then returns the coordinates of its Picture-in-Picture toggle as well
* as whether or not the <video> element is showing the built-in controls.
*
* @param {Element} browser The <xul:browser> that has the <video> loaded in it.
* @param {String} videoID The ID of the video that has the toggle.
*
* @return Promise
* @resolves With the following Object structure:
* {
* controls: <Boolean>,
* }
*
* Where controls represents whether or not the video has the default control set
* displayed.
*/
async function prepareForToggleClick(browser, videoID) {
// Synthesize a mouse move just outside of the video to ensure that
// the video is in a non-hovering state. We'll go 5 pixels to the
// left and above the top-left corner.
await BrowserTestUtils.synthesizeMouse(
`#${videoID}`,
-5,
-5,
{
type: "mousemove",
},
browser,
false
);
// For each video, make sure it's scrolled into view, and get the rect for
// the toggle while we're at it.
let args = { videoID };
return SpecialPowers.spawn(browser, [args], async args => {
let { videoID } = args;
let video = content.document.getElementById(videoID);
video.scrollIntoView({ behaviour: "instant" });
if (!video.controls) {
// For no-controls <video> elements, an IntersectionObserver is used
// to know when we the PictureInPictureChild should begin tracking
// mousemove events. We don't exactly know when that IntersectionObserver
// will fire, so we poll a special testing function that will tell us when
// the video that we care about is being tracked.
let { PictureInPictureToggleChild } = ChromeUtils.importESModule(
);
await ContentTaskUtils.waitForCondition(
() => {
return PictureInPictureToggleChild.isTracking(video);
},
"Waiting for PictureInPictureToggleChild to be tracking the video.",
100,
100
);
}
let shadowRoot = video.openOrClosedShadowRoot;
let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
await ContentTaskUtils.waitForCondition(
() => {
return !controlsOverlay.classList.contains("hovering");
},
"Waiting for the video to not be hovered.",
100,
100
);
return {
controls: video.controls,
};
});
}
/**
* Returns client rect info for the toggle if it's supposed to be visible
* on hover. Otherwise, returns client rect info for the video with the
* associated ID.
*
* @param {Element} browser The <xul:browser> that has the <video> loaded in it.
* @param {String} videoID The ID of the video that has the toggle.
*
* @return Promise
* @resolves With the following Object structure:
* {
* top: <Number>,
* left: <Number>,
* width: <Number>,
* height: <Number>,
* }
*/
async function getToggleClientRect(
browser,
videoID,
toggleStyles = DEFAULT_TOGGLE_STYLES
) {
let args = { videoID, toggleID: toggleStyles.rootID };
return ContentTask.spawn(browser, args, async args => {
const { Rect } = ChromeUtils.importESModule(
);
let { videoID, toggleID } = args;
let video = content.document.getElementById(videoID);
let shadowRoot = video.openOrClosedShadowRoot;
let toggle = shadowRoot.getElementById(toggleID);
let rect = Rect.fromRect(toggle.getBoundingClientRect());
let clickableChildren = toggle.querySelectorAll(".clickable");
for (let child of clickableChildren) {
let childRect = Rect.fromRect(child.getBoundingClientRect());
rect.expandToContain(childRect);
}
if (!rect.width && !rect.height) {
rect = video.getBoundingClientRect();
}
return {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
};
});
}
/**
* This function will hover over the middle of the video and then
* hover over the toggle.
* @param browser The current browser
* @param videoID The video element id
*/
async function hoverToggle(browser, videoID) {
await prepareForToggleClick(browser, videoID);
// Hover the mouse over the video to reveal the toggle.
await BrowserTestUtils.synthesizeMouseAtCenter(
`#${videoID}`,
{
type: "mousemove",
},
browser
);
await BrowserTestUtils.synthesizeMouseAtCenter(
`#${videoID}`,
{
type: "mouseover",
},
browser
);
info("Checking toggle policy");
await assertTogglePolicy(browser, videoID, null);
let toggleClientRect = await getToggleClientRect(browser, videoID);
info("Hovering the toggle rect now.");
let toggleCenterX = toggleClientRect.left + toggleClientRect.width / 2;
let toggleCenterY = toggleClientRect.top + toggleClientRect.height / 2;
await BrowserTestUtils.synthesizeMouseAtPoint(
toggleCenterX,
toggleCenterY,
{
type: "mousemove",
},
browser
);
await BrowserTestUtils.synthesizeMouseAtPoint(
toggleCenterX,
toggleCenterY,
{
type: "mouseover",
},
browser
);
}
/**
* Test helper for the Picture-in-Picture toggle. Loads a page, and then
* tests the provided video elements for the toggle both appearing and
* opening the Picture-in-Picture window in the expected cases.
*
* @param {String} testURL The URL of the page with the <video> elements.
* @param {Object} expectations An object with the following schema:
* <video-element-id>: {
* canToggle: {Boolean}
* policy: {Number} (optional)
* styleRules: {Object} (optional)
* }
* If canToggle is true, then it's expected that moving the mouse over the
* video and then clicking in the toggle region should open a
* Picture-in-Picture window. If canToggle is false, we expect that a click
* in this region will not result in the window opening.
*
* If policy is defined, then it should be one of the values in the
* TOGGLE_POLICIES from PictureInPictureControls.sys.mjs.
*
* See the documentation for the DEFAULT_TOGGLE_STYLES object for a sense
* of what styleRules is expected to be. If left undefined, styleRules will
* default to DEFAULT_TOGGLE_STYLES.
*
* @param {async Function} prepFn An optional asynchronous function to run
* before running the toggle test. The function is passed the opened
* <xul:browser> as its only argument once the testURL has finished loading.
*
* @return Promise
* @resolves When the test is complete and the tab with the loaded page is
* removed.
*/
async function testToggle(testURL, expectations, prepFn = async () => {}) {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: testURL,
},
async browser => {
await prepFn(browser);
await ensureVideosReady(browser);
for (let [
videoID,
{ canToggle, policy, toggleStyles, shouldSeeClickEventAfterToggle },
] of Object.entries(expectations)) {
await SimpleTest.promiseFocus(browser);
info(`Testing video with id: ${videoID}`);
await testToggleHelper(
browser,
videoID,
canToggle,
policy,
toggleStyles,
shouldSeeClickEventAfterToggle
);
}
}
);
}
/**
* Test helper for the Picture-in-Picture toggle. Given a loaded page with some
* videos on it, tests that the toggle behaves as expected when interacted
* with by the mouse.
*
* @param {Element} browser The <xul:browser> that has the <video> loaded in it.
* @param {String} videoID The ID of the video that has the toggle.
* @param {Boolean} canToggle True if we expect the toggle to be visible and
* clickable by the mouse for the associated video.
* @param {Number} policy Optional argument. If policy is defined, then it should
* be one of the values in the TOGGLE_POLICIES from PictureInPictureControls.sys.mjs.
* @param {Object} toggleStyles Optional argument. See the documentation for the
* DEFAULT_TOGGLE_STYLES object for a sense of what styleRules is expected to be.
*
* @return Promise
* @resolves When the check for the toggle is complete.
*/
async function testToggleHelper(
browser,
videoID,
canToggle,
policy,
toggleStyles,
shouldSeeClickEventAfterToggle
) {
let { controls } = await prepareForToggleClick(browser, videoID);
// Hover the mouse over the video to reveal the toggle.
await BrowserTestUtils.synthesizeMouseAtCenter(
`#${videoID}`,
{
type: "mousemove",
},
browser
);
await BrowserTestUtils.synthesizeMouseAtCenter(
`#${videoID}`,
{
type: "mouseover",
},
browser
);
info("Checking toggle policy");
await assertTogglePolicy(browser, videoID, policy, toggleStyles);
if (canToggle) {
info("Waiting for toggle to become visible");
await toggleOpacityReachesThreshold(
browser,
videoID,
"hoverVideo",
toggleStyles
);
}
let toggleClientRect = await getToggleClientRect(
browser,
videoID,
toggleStyles
);
info("Hovering the toggle rect now.");
let toggleCenterX = toggleClientRect.left + toggleClientRect.width / 2;
let toggleCenterY = toggleClientRect.top + toggleClientRect.height / 2;
await BrowserTestUtils.synthesizeMouseAtPoint(
toggleCenterX,
toggleCenterY,
{
type: "mousemove",
},
browser
);
await BrowserTestUtils.synthesizeMouseAtPoint(
toggleCenterX,
toggleCenterY,
{
type: "mouseover",
},
browser
);
if (canToggle) {
info("Waiting for toggle to reach full opacity");
await toggleOpacityReachesThreshold(
browser,
videoID,
"hoverToggle",
toggleStyles
);
}
// First, ensure that a non-primary mouse click is ignored.
info("Right-clicking on toggle.");
await BrowserTestUtils.synthesizeMouseAtPoint(
toggleCenterX,
toggleCenterY,
{ button: 2 },
browser
);
// For videos without the built-in controls, we expect that all mouse events
// should have fired - otherwise, the events are all suppressed. For videos
// with controls, none of the events should be fired, as the controls overlay
// absorbs them all.
//
// Note that the right-click does not result in a "click" event firing.
await assertSawMouseEvents(browser, !controls, false);
// The message to open the Picture-in-Picture window would normally be sent
// immediately before this Promise resolved, so the window should have opened
// by now if it was going to happen.
for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) {
if (!win.closed) {
ok(false, "Found a Picture-in-Picture window unexpectedly.");
return;
}
}
ok(true, "No Picture-in-Picture window found.");
// Okay, now test with the primary mouse button.
if (canToggle) {
info(
"Clicking on toggle, and expecting a Picture-in-Picture window to open"
);
let domWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(null);
await BrowserTestUtils.synthesizeMouseAtPoint(
toggleCenterX,
toggleCenterY,
{},
browser
);
let win = await domWindowOpened;
ok(win, "A Picture-in-Picture window opened.");
await assertVideoIsBeingCloned(browser, "#" + videoID);
await BrowserTestUtils.closeWindow(win);
// We do get a "Click" sometimes, it depends on many
// factors such as whether the video has control and
// the style of the toggle.
if (shouldSeeClickEventAfterToggle) {
await assertSawClickEventOnly(browser);
} else {
// Make sure that clicking on the toggle resulted in no mouse button events
// being fired in content.
await assertSawMouseEvents(browser, false);
}
} else {
info(
"Clicking on toggle, and expecting no Picture-in-Picture window opens"
);
await BrowserTestUtils.synthesizeMouseAtPoint(
toggleCenterX,
toggleCenterY,
{},
browser
);
// If we aren't showing the toggle, we expect all mouse events to be seen.
await assertSawMouseEvents(browser, !controls);
// The message to open the Picture-in-Picture window would normally be sent
// immediately before this Promise resolved, so the window should have opened
// by now if it was going to happen.
for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) {
if (!win.closed) {
ok(false, "Found a Picture-in-Picture window unexpectedly.");
return;
}
}
ok(true, "No Picture-in-Picture window found.");
}
// Click on the very top-left pixel of the document and ensure that we
// see all of the mouse events for it.
await BrowserTestUtils.synthesizeMouseAtPoint(1, 1, {}, browser);
await assertSawMouseEvents(browser, true);
}
/**
* Helper function that ensures that a provided async function
* causes a window to fully enter fullscreen mode.
*
* @param window (DOM Window)
* The window that is expected to enter fullscreen mode.
* @param asyncFn (Async Function)
* The async function to run to trigger the fullscreen switch.
* @return Promise
* @resolves When the fullscreen entering transition completes.
*/
async function promiseFullscreenEntered(window, asyncFn) {
let entered = BrowserTestUtils.waitForEvent(
window,
"MozDOMFullscreen:Entered"
);
await asyncFn();
await entered;
await BrowserTestUtils.waitForCondition(() => {
return !TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS");
});
if (AppConstants.platform == "macosx") {
// On macOS, the fullscreen transition takes some extra time
// to complete, and we don't receive events for it. We need to
// wait for it to complete or else input events in the next test
// might get eaten up. This is the best we can currently do.
//
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
dump(`BJW promiseFullscreenEntered: waiting for 2 second timeout.\n`);
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
/**
* Helper function that ensures that a provided async function
* causes a window to fully exit fullscreen mode.
*
* @param window (DOM Window)
* The window that is expected to exit fullscreen mode.
* @param asyncFn (Async Function)
* The async function to run to trigger the fullscreen switch.
* @return Promise
* @resolves When the fullscreen exiting transition completes.
*/
async function promiseFullscreenExited(window, asyncFn) {
let exited = BrowserTestUtils.waitForEvent(window, "MozDOMFullscreen:Exited");
await asyncFn();
await exited;
await BrowserTestUtils.waitForCondition(() => {
return !TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS");
});
if (AppConstants.platform == "macosx") {
// On macOS, the fullscreen transition takes some extra time
// to complete, and we don't receive events for it. We need to
// wait for it to complete or else input events in the next test
// might get eaten up. This is the best we can currently do.
//
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
/**
* Helper function that ensures that the "This video is
* playing in Picture-in-Picture mode" message works,
* then closes the player window
*
* @param {Element} browser The <xul:browser> that has the <video> loaded in it.
* @param {String} videoID The ID of the video that has the toggle.
* @param {Element} pipWin The Picture-in-Picture window that was opened
* @param {Boolean} iframe True if the test is on an Iframe, which modifies
* the test behavior
*/
async function ensureMessageAndClosePiP(browser, videoID, pipWin, isIframe) {
try {
await assertShowingMessage(browser, videoID, true);
} finally {
let uaWidgetUpdate = null;
if (isIframe) {
uaWidgetUpdate = SpecialPowers.spawn(browser, [], async () => {
await ContentTaskUtils.waitForEvent(
content.windowRoot,
"UAWidgetSetupOrChange",
true /* capture */
);
});
} else {
uaWidgetUpdate = BrowserTestUtils.waitForContentEvent(
browser,
"UAWidgetSetupOrChange",
true /* capture */
);
}
await BrowserTestUtils.closeWindow(pipWin);
await uaWidgetUpdate;
}
}
/**
* Helper function that returns True if the specified video is paused
* and False if the specified video is not paused.
*
* @param {Element} browser The <xul:browser> that has the <video> loaded in it.
* @param {String} videoID The ID of the video to check.
*/
async function isVideoPaused(browser, videoID) {
return SpecialPowers.spawn(browser, [videoID], async videoID => {
return content.document.getElementById(videoID).paused;
});
}
/**
* Helper function that returns True if the specified video is muted
* and False if the specified video is not muted.
*
* @param {Element} browser The <xul:browser> that has the <video> loaded in it.
* @param {String} videoID The ID of the video to check.
*/
async function isVideoMuted(browser, videoID) {
return SpecialPowers.spawn(browser, [videoID], async videoID => {
return content.document.getElementById(videoID).muted;
});
}
/**
* Initializes videos and text tracks for the current test case.
* First track is the default track to be loaded onto the video.
* Once initialization is done, play then pause the requested video.
* so that text tracks are loaded.
* @param {Element} browser The <xul:browser> hosting the <video>
* @param {String} videoID The ID of the video being checked
* @param {Integer} defaultTrackIndex The index of the track to be loaded, or none if -1
* @param {String} trackMode the mode that the video's textTracks should be set to
*/
async function prepareVideosAndWebVTTTracks(
browser,
videoID,
defaultTrackIndex = 0,
trackMode = "showing"
) {
info("Preparing video and initial text tracks");
await ensureVideosReady(browser);
await SpecialPowers.spawn(
browser,
[{ videoID, defaultTrackIndex, trackMode }],
async args => {
let video = content.document.getElementById(args.videoID);
let tracks = video.textTracks;
is(tracks.length, 5, "Number of tracks loaded should be 5");
// Enable track for originating video
if (args.defaultTrackIndex >= 0) {
info(`Loading track ${args.defaultTrackIndex + 1}`);
let track = tracks[args.defaultTrackIndex];
tracks.mode = args.trackMode;
track.mode = args.trackMode;
}
// Briefly play the video to load text tracks onto the pip window.
info("Playing video to load text tracks");
video.play();
info("Pausing video");
video.pause();
ok(video.paused, "Video should be paused before proceeding with test");
}
);
}
/**
* Plays originating video until the next cue is loaded.
* Once the next cue is loaded, pause the video.
* @param {Element} browser The <xul:browser> hosting the <video>
* @param {String} videoID The ID of the video being checked
* @param {Integer} textTrackIndex The index of the track to be loaded, or none if -1
*/
async function waitForNextCue(browser, videoID, textTrackIndex = 0) {
if (textTrackIndex < 0) {
ok(false, "Cannot wait for next cue with invalid track index");
}
await SpecialPowers.spawn(
browser,
[{ videoID, textTrackIndex }],
async args => {
let video = content.document.getElementById(args.videoID);
info("Playing video to activate next cue");
video.play();
ok(!video.paused, "Video is playing");
info("Waiting until cuechange is called");
await ContentTaskUtils.waitForEvent(
video.textTracks[args.textTrackIndex],
"cuechange"
);
info("Pausing video to read text track");
video.pause();
ok(video.paused, "Video is paused");
}
);
}
/**
* The PiP window saves the positon when closed and sometimes we don't want
* this information to persist to other tests. This function will clear the
* position so the PiP window will open in the default position.
*/
function clearSavedPosition() {
let xulStore = Services.xulStore;
xulStore.setValue(PLAYER_URI, "picture-in-picture", "left", NaN);
xulStore.setValue(PLAYER_URI, "picture-in-picture", "top", NaN);
xulStore.setValue(PLAYER_URI, "picture-in-picture", "width", NaN);
xulStore.setValue(PLAYER_URI, "picture-in-picture", "height", NaN);
}
function overrideSavedPosition(left, top, width, height) {
let xulStore = Services.xulStore;
xulStore.setValue(PLAYER_URI, "picture-in-picture", "left", left);
xulStore.setValue(PLAYER_URI, "picture-in-picture", "top", top);
xulStore.setValue(PLAYER_URI, "picture-in-picture", "width", width);
xulStore.setValue(PLAYER_URI, "picture-in-picture", "height", height);
}
/**
* Function used to filter events when waiting for the correct number
* telemetry events.
* @param {String} expected The expected string or undefined
* @param {String} actual The actual string
* @returns true if the expected is undefined or if expected matches actual
*/
function matches(expected, actual) {
if (expected === undefined) {
return true;
}
return expected === actual;
}
/**
* Function that waits for the expected number of events aftering filtering.
* @param {Object} filter An object containing optional filters
* {
* category: (optional) The category of the event. Ex. "pictureinpicture"
* method: (optional) The method of the event. Ex. "create"
* object: (optional) The object of the event. Ex. "player"
* }
* @param {Number} length The number of events to wait for
* @param {String} process Should be "content" or "parent" depending on the event
*/
async function waitForTelemeryEvents(filter, length, process) {
let {
category: filterCategory,
method: filterMethod,
object: filterObject,
} = filter;
let events = [];
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
)[process];
if (!events) {
return false;
}
let filtered = events
.map(([, /* timestamp */ category, method, object, value, extra]) => {
// We don't care about the `timestamp` value.
// Tests that examine that value should use `snapshotEvents` directly.
return [category, method, object, value, extra];
})
.filter(([category, method, object]) => {
return (
matches(filterCategory, category) &&
matches(filterMethod, method) &&
matches(filterObject, object)
);
});
info(JSON.stringify(filtered, null, 2));
return filtered && filtered.length >= length;
},
`Waiting for ${length} pictureinpicture telemetry event(s) with filter ${JSON.stringify(
filter,
null,
2
)}`,
200,
100
);
}