summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/popups/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/test/popups/head.js')
-rw-r--r--browser/base/content/test/popups/head.js574
1 files changed, 574 insertions, 0 deletions
diff --git a/browser/base/content/test/popups/head.js b/browser/base/content/test/popups/head.js
new file mode 100644
index 0000000000..f72bba7dca
--- /dev/null
+++ b/browser/base/content/test/popups/head.js
@@ -0,0 +1,574 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ SpecialPowers.Ci.nsIGfxInfo
+);
+
+async function waitForBlockedPopups(numberOfPopups, { doc }) {
+ let toolbarDoc = doc || document;
+ let menupopup = toolbarDoc.getElementById("blockedPopupOptions");
+ await BrowserTestUtils.waitForCondition(() => {
+ let popups = menupopup.querySelectorAll("[popupReportIndex]");
+ return popups.length == numberOfPopups;
+ }, `Waiting for ${numberOfPopups} popups`);
+}
+
+/*
+ * Tests that a sequence of size changes ultimately results in the latest
+ * requested size. The test also fails when an unexpected window size is
+ * observed in a resize event.
+ *
+ * aPropertyDeltas List of objects where keys describe the name of a window
+ * property and the values the difference to its initial
+ * value.
+ *
+ * aInstant Issue changes without additional waiting in between.
+ *
+ * A brief example of the resutling code that is effectively run for the
+ * following list of deltas:
+ * [{outerWidth: 5, outerHeight: 10}, {outerWidth: 10}]
+ *
+ * let initialWidth = win.outerWidth;
+ * let initialHeight = win.outerHeight;
+ *
+ * if (aInstant) {
+ * win.outerWidth = initialWidth + 5;
+ * win.outerHeight = initialHeight + 10;
+ *
+ * win.outerWidth = initialWidth + 10;
+ * } else {
+ * win.requestAnimationFrame(() => {
+ * win.outerWidth = initialWidth + 5;
+ * win.outerHeight = initialHeight + 10;
+ *
+ * win.requestAnimationFrame(() => {
+ * win.outerWidth = initialWidth + 10;
+ * });
+ * });
+ * }
+ */
+async function testPropertyDeltas(
+ aPropertyDeltas,
+ aInstant,
+ aPropInfo,
+ aMsg,
+ aWaitForCompletion
+) {
+ let msg = `[${aMsg}]`;
+
+ let win = this.content.popup || this.content.wrappedJSObject;
+
+ // Property names and mapping from ResizeMoveTest
+ let {
+ sizeProps,
+ positionProps /* can be empty/incomplete as workaround on Linux */,
+ readonlyProps,
+ crossBoundsMapping,
+ } = aPropInfo;
+
+ let stringifyState = state => {
+ let stateMsg = sizeProps
+ .concat(positionProps)
+ .filter(prop => state[prop] !== undefined)
+ .map(prop => `${prop}: ${state[prop]}`)
+ .join(", ");
+ return `{ ${stateMsg} }`;
+ };
+
+ let initialState = {};
+ let finalState = {};
+
+ info("Initializing all values to current state.");
+ for (let prop of sizeProps.concat(positionProps)) {
+ let value = win[prop];
+ initialState[prop] = value;
+ finalState[prop] = value;
+ }
+
+ // List of potential states during resize events. The current state is also
+ // considered valid, as the resize event might still be outstanding.
+ let validResizeStates = [initialState];
+
+ let updateFinalState = (aProp, aDelta) => {
+ if (
+ readonlyProps.includes(aProp) ||
+ !sizeProps.concat(positionProps).includes(aProp)
+ ) {
+ throw new Error(`Unexpected property "${aProp}".`);
+ }
+
+ // Update both properties of the same axis.
+ let otherProp = crossBoundsMapping[aProp];
+ finalState[aProp] = initialState[aProp] + aDelta;
+ finalState[otherProp] = initialState[otherProp] + aDelta;
+
+ // Mark size as valid in resize event.
+ if (sizeProps.includes(aProp)) {
+ let state = {};
+ sizeProps.forEach(p => (state[p] = finalState[p]));
+ validResizeStates.push(state);
+ }
+ };
+
+ info("Adding resize event listener.");
+ let resizeCount = 0;
+ let resizeListener = evt => {
+ resizeCount++;
+
+ let currentSizeState = {};
+ sizeProps.forEach(p => (currentSizeState[p] = win[p]));
+
+ info(
+ `${msg} ${resizeCount}. resize event: ${stringifyState(currentSizeState)}`
+ );
+ let matchingIndex = validResizeStates.findIndex(state =>
+ sizeProps.every(p => state[p] == currentSizeState[p])
+ );
+ if (matchingIndex < 0) {
+ info(`${msg} Size state should have been one of:`);
+ for (let state of validResizeStates) {
+ info(stringifyState(state));
+ }
+ }
+
+ if (win.gBrowser && evt.target != win) {
+ // Without e10s we receive content resize events in chrome windows.
+ todo(false, `${msg} Resize event target is our window.`);
+ return;
+ }
+
+ ok(
+ matchingIndex >= 0,
+ `${msg} Valid intermediate state. Current: ` +
+ stringifyState(currentSizeState)
+ );
+
+ // No longer allow current and preceding states.
+ validResizeStates.splice(0, matchingIndex + 1);
+ };
+ win.addEventListener("resize", resizeListener);
+
+ const useProperties = !Services.prefs.getBoolPref(
+ "dom.window_position_size_properties_replaceable.enabled",
+ true
+ );
+
+ info("Starting property changes.");
+ await new Promise(resolve => {
+ let index = 0;
+ let next = async () => {
+ let pre = `${msg} [${index + 1}/${aPropertyDeltas.length}]`;
+
+ let deltaObj = aPropertyDeltas[index];
+ for (let prop in deltaObj) {
+ updateFinalState(prop, deltaObj[prop]);
+
+ let targetValue = initialState[prop] + deltaObj[prop];
+ info(`${pre} Setting ${prop} to ${targetValue}.`);
+ if (useProperties) {
+ win[prop] = targetValue;
+ } else if (sizeProps.includes(prop)) {
+ win.resizeTo(finalState.outerWidth, finalState.outerHeight);
+ } else {
+ win.moveTo(finalState.screenX, finalState.screenY);
+ }
+ if (aWaitForCompletion) {
+ await ContentTaskUtils.waitForCondition(
+ () => win[prop] == targetValue,
+ `${msg} Waiting for ${prop} to be ${targetValue}.`
+ );
+ }
+ }
+
+ index++;
+ if (index < aPropertyDeltas.length) {
+ scheduleNext();
+ } else {
+ resolve();
+ }
+ };
+
+ let scheduleNext = () => {
+ if (aInstant) {
+ next();
+ } else {
+ info(`${msg} Requesting animation frame.`);
+ win.requestAnimationFrame(next);
+ }
+ };
+ scheduleNext();
+ });
+
+ try {
+ info(`${msg} Waiting for window to match the final state.`);
+ await ContentTaskUtils.waitForCondition(
+ () => sizeProps.concat(positionProps).every(p => win[p] == finalState[p]),
+ "Waiting for final state."
+ );
+ } catch (e) {}
+
+ info(`${msg} Checking final state.`);
+ info(`${msg} Exepected: ${stringifyState(finalState)}`);
+ info(`${msg} Actual: ${stringifyState(win)}`);
+ for (let prop of sizeProps.concat(positionProps)) {
+ is(win[prop], finalState[prop], `${msg} Expected final value for ${prop}`);
+ }
+
+ win.removeEventListener("resize", resizeListener);
+}
+
+function roundedCenter(aDimension, aOrigin) {
+ let center = aOrigin + Math.floor(aDimension / 2);
+ return center - (center % 100);
+}
+
+class ResizeMoveTest {
+ static WindowWidth = 200;
+ static WindowHeight = 200;
+ static WindowLeft = roundedCenter(screen.availWidth - 200, screen.left);
+ static WindowTop = roundedCenter(screen.availHeight - 200, screen.top);
+
+ static PropInfo = {
+ sizeProps: ["outerWidth", "outerHeight", "innerWidth", "innerHeight"],
+ positionProps: [
+ "screenX",
+ "screenY",
+ /* readonly */ "mozInnerScreenX",
+ /* readonly */ "mozInnerScreenY",
+ ],
+ readonlyProps: ["mozInnerScreenX", "mozInnerScreenY"],
+ crossAxisMapping: {
+ outerWidth: "outerHeight",
+ outerHeight: "outerWidth",
+ innerWidth: "innerHeight",
+ innerHeight: "innerWidth",
+ screenX: "screenY",
+ screenY: "screenX",
+ mozInnerScreenX: "mozInnerScreenY",
+ mozInnerScreenY: "mozInnerScreenX",
+ },
+ crossBoundsMapping: {
+ outerWidth: "innerWidth",
+ outerHeight: "innerHeight",
+ innerWidth: "outerWidth",
+ innerHeight: "outerHeight",
+ screenX: "mozInnerScreenX",
+ screenY: "mozInnerScreenY",
+ mozInnerScreenX: "screenX",
+ mozInnerScreenY: "screenY",
+ },
+ };
+
+ constructor(
+ aPropertyDeltas,
+ aInstant = false,
+ aMsg = "ResizeMoveTest",
+ aWaitForCompletion = false
+ ) {
+ this.propertyDeltas = aPropertyDeltas;
+ this.instant = aInstant;
+ this.msg = aMsg;
+ this.waitForCompletion = aWaitForCompletion;
+
+ // Allows to ignore positions while testing.
+ this.ignorePositions = false;
+ // Allows to ignore only mozInnerScreenX/Y properties while testing.
+ this.ignoreMozInnerScreen = false;
+ // Allows to skip checking the restored position after testing.
+ this.ignoreRestoredPosition = false;
+
+ if (AppConstants.platform == "linux" && !SpecialPowers.isHeadless) {
+ // We can occasionally start the test while nsWindow reports a wrong
+ // client offset (gdk origin and root_origin are out of sync). This
+ // results in false expectations for the final mozInnerScreenX/Y values.
+ this.ignoreMozInnerScreen = !ResizeMoveTest.hasCleanUpTask;
+
+ let { positionProps } = ResizeMoveTest.PropInfo;
+ let resizeOnlyTest = aPropertyDeltas.every(deltaObj =>
+ positionProps.every(prop => deltaObj[prop] === undefined)
+ );
+
+ let isWayland = gfxInfo.windowProtocol == "wayland";
+ if (resizeOnlyTest && isWayland) {
+ // On Wayland we can't move the window in general. The window also
+ // doesn't necessarily open our specified position.
+ this.ignoreRestoredPosition = true;
+ // We can catch bad screenX/Y at the start of the first test in a
+ // window.
+ this.ignorePositions = !ResizeMoveTest.hasCleanUpTask;
+ }
+ }
+
+ if (!ResizeMoveTest.hasCleanUpTask) {
+ ResizeMoveTest.hasCleanUpTask = true;
+ registerCleanupFunction(ResizeMoveTest.Cleanup);
+ }
+
+ add_task(async () => {
+ let tab = await ResizeMoveTest.GetOrCreateTab();
+ let browsingContext =
+ await ResizeMoveTest.GetOrCreatePopupBrowsingContext();
+ if (!browsingContext) {
+ return;
+ }
+
+ info("=== Running in content. ===");
+ await this.run(browsingContext, `${this.msg} (content)`);
+ await this.restorePopupState(browsingContext);
+
+ info("=== Running in chrome. ===");
+ let popupChrome = browsingContext.topChromeWindow;
+ await this.run(popupChrome.browsingContext, `${this.msg} (chrome)`);
+ await this.restorePopupState(browsingContext);
+
+ info("=== Running in opener. ===");
+ await this.run(tab.linkedBrowser, `${this.msg} (opener)`);
+ await this.restorePopupState(browsingContext);
+ });
+ }
+
+ async run(aBrowsingContext, aMsg) {
+ let testType = this.instant ? "instant" : "fanned out";
+ let msg = `${aMsg} (${testType})`;
+
+ let propInfo = {};
+ for (let k in ResizeMoveTest.PropInfo) {
+ propInfo[k] = ResizeMoveTest.PropInfo[k];
+ }
+ if (this.ignoreMozInnerScreen) {
+ todo(false, `[${aMsg}] Shouldn't ignore mozInnerScreenX/Y.`);
+ propInfo.positionProps = propInfo.positionProps.filter(
+ prop => !["mozInnerScreenX", "mozInnerScreenY"].includes(prop)
+ );
+ }
+ if (this.ignorePositions) {
+ todo(false, `[${aMsg}] Shouldn't ignore position.`);
+ propInfo.positionProps = [];
+ }
+
+ info(`${msg}: ` + JSON.stringify(this.propertyDeltas));
+ await SpecialPowers.spawn(
+ aBrowsingContext,
+ [
+ this.propertyDeltas,
+ this.instant,
+ propInfo,
+ msg,
+ this.waitForCompletion,
+ ],
+ testPropertyDeltas
+ );
+ }
+
+ async restorePopupState(aBrowsingContext) {
+ info("Restore popup state.");
+
+ let { deltaWidth, deltaHeight } = await SpecialPowers.spawn(
+ aBrowsingContext,
+ [],
+ () => {
+ return {
+ deltaWidth: this.content.outerWidth - this.content.innerWidth,
+ deltaHeight: this.content.outerHeight - this.content.innerHeight,
+ };
+ }
+ );
+
+ let chromeWindow = aBrowsingContext.topChromeWindow;
+ let {
+ WindowLeft: left,
+ WindowTop: top,
+ WindowWidth: width,
+ WindowHeight: height,
+ } = ResizeMoveTest;
+
+ chromeWindow.resizeTo(width + deltaWidth, height + deltaHeight);
+ chromeWindow.moveTo(left, top);
+
+ await SpecialPowers.spawn(
+ aBrowsingContext,
+ [left, top, width, height, this.ignoreRestoredPosition],
+ async (aLeft, aTop, aWidth, aHeight, aIgnorePosition) => {
+ let win = this.content.wrappedJSObject;
+
+ info("Waiting for restored size.");
+ await ContentTaskUtils.waitForCondition(
+ () => win.innerWidth == aWidth && win.innerHeight === aHeight,
+ "Waiting for restored size."
+ );
+ is(win.innerWidth, aWidth, "Restored width.");
+ is(win.innerHeight, aHeight, "Restored height.");
+
+ if (!aIgnorePosition) {
+ info("Waiting for restored position.");
+ await ContentTaskUtils.waitForCondition(
+ () => win.screenX == aLeft && win.screenY === aTop,
+ "Waiting for restored position."
+ );
+ is(win.screenX, aLeft, "Restored screenX.");
+ is(win.screenY, aTop, "Restored screenY.");
+ } else {
+ todo(false, "Shouldn't ignore restored position.");
+ }
+ }
+ );
+ }
+
+ static async GetOrCreateTab() {
+ if (ResizeMoveTest.tab) {
+ return ResizeMoveTest.tab;
+ }
+
+ info("Opening tab.");
+ ResizeMoveTest.tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.net/browser/browser/base/content/test/popups/popup_blocker_a.html"
+ );
+ return ResizeMoveTest.tab;
+ }
+
+ static async GetOrCreatePopupBrowsingContext() {
+ if (ResizeMoveTest.popupBrowsingContext) {
+ if (!ResizeMoveTest.popupBrowsingContext.isActive) {
+ return undefined;
+ }
+ return ResizeMoveTest.popupBrowsingContext;
+ }
+
+ let tab = await ResizeMoveTest.GetOrCreateTab();
+ info("Opening popup.");
+ ResizeMoveTest.popupBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [
+ ResizeMoveTest.WindowWidth,
+ ResizeMoveTest.WindowHeight,
+ ResizeMoveTest.WindowLeft,
+ ResizeMoveTest.WindowTop,
+ ],
+ async (aWidth, aHeight, aLeft, aTop) => {
+ let win = this.content.open(
+ this.content.document.location.href,
+ "_blank",
+ `left=${aLeft},top=${aTop},width=${aWidth},height=${aHeight}`
+ );
+ this.content.popup = win;
+
+ await new Promise(r => (win.onload = r));
+
+ return win.browsingContext;
+ }
+ );
+
+ return ResizeMoveTest.popupBrowsingContext;
+ }
+
+ static async Cleanup() {
+ let browsingContext = ResizeMoveTest.popupBrowsingContext;
+ if (browsingContext) {
+ await SpecialPowers.spawn(browsingContext, [], () => {
+ this.content.close();
+ });
+ delete ResizeMoveTest.popupBrowsingContext;
+ }
+
+ let tab = ResizeMoveTest.tab;
+ if (tab) {
+ await BrowserTestUtils.removeTab(tab);
+ delete ResizeMoveTest.tab;
+ }
+ ResizeMoveTest.hasCleanUpTask = false;
+ }
+}
+
+function chaosRequestLongerTimeout(aDoRequest) {
+ if (aDoRequest && parseInt(Services.env.get("MOZ_CHAOSMODE"), 16)) {
+ requestLongerTimeout(2);
+ }
+}
+
+function createGenericResizeTests(aFirstValue, aSecondValue, aInstant, aMsg) {
+ // Runtime almost doubles in chaos mode on Mac.
+ chaosRequestLongerTimeout(AppConstants.platform == "macosx");
+
+ let { crossBoundsMapping, crossAxisMapping } = ResizeMoveTest.PropInfo;
+
+ for (let prop of ["innerWidth", "outerHeight"]) {
+ // Mixing inner and outer property.
+ for (let secondProp of [prop, crossBoundsMapping[prop]]) {
+ let first = {};
+ first[prop] = aFirstValue;
+ let second = {};
+ second[secondProp] = aSecondValue;
+ new ResizeMoveTest(
+ [first, second],
+ aInstant,
+ `${aMsg} ${prop},${secondProp}`
+ );
+ }
+ }
+
+ for (let prop of ["innerHeight", "outerWidth"]) {
+ let first = {};
+ first[prop] = aFirstValue;
+ let second = {};
+ second[prop] = aSecondValue;
+
+ // Setting property of other axis before/between two changes.
+ let otherProps = [
+ crossAxisMapping[prop],
+ crossAxisMapping[crossBoundsMapping[prop]],
+ ];
+ for (let interferenceProp of otherProps) {
+ let interference = {};
+ interference[interferenceProp] = 20;
+ new ResizeMoveTest(
+ [first, interference, second],
+ aInstant,
+ `${aMsg} ${prop},${interferenceProp},${prop}`
+ );
+ new ResizeMoveTest(
+ [interference, first, second],
+ aInstant,
+ `${aMsg} ${interferenceProp},${prop},${prop}`
+ );
+ }
+ }
+}
+
+function createGenericMoveTests(aInstant, aMsg) {
+ // Runtime almost doubles in chaos mode on Mac.
+ chaosRequestLongerTimeout(AppConstants.platform == "macosx");
+
+ let { crossAxisMapping } = ResizeMoveTest.PropInfo;
+
+ for (let prop of ["screenX", "screenY"]) {
+ for (let [v1, v2, msg] of [
+ [9, 10, `${aMsg}`],
+ [11, 11, `${aMsg} repeat`],
+ [12, 0, `${aMsg} revert`],
+ ]) {
+ let first = {};
+ first[prop] = v1;
+ let second = {};
+ second[prop] = v2;
+ new ResizeMoveTest([first, second], aInstant, `${msg} ${prop},${prop}`);
+
+ let interferenceProp = crossAxisMapping[prop];
+ let interference = {};
+ interference[interferenceProp] = 20;
+ new ResizeMoveTest(
+ [first, interference, second],
+ aInstant,
+ `${aMsg} ${prop},${interferenceProp},${prop}`
+ );
+ new ResizeMoveTest(
+ [interference, first, second],
+ aInstant,
+ `${msg} ${interferenceProp},${prop},${prop}`
+ );
+ }
+ }
+}