Files
tubestation/browser/base/content/test/popups/head.js
Matthias Camenzind d634177ee2 Bug 1786048 - Part 3: Merge nsIEmbeddingSiteWindow into nsIBaseWindow. r=emilio
Implementations of nsIEmbeddingSiteWindow and nsIBaseWindow largely
overlap, and where they don't, the nsIEmbeddingSiteWindow implementation
of the otherwise shared interface is primarily stubbed out with the
exception of Get/SetDimensions().

This patch moves a reimplementation of Get/SetDimensions() from
nsIEmbeddingSiteWindow to nsIBaseWindow. The other methods of
nsIEmbeddingSiteWindow remain covered by nsIBaseWindow.
Get/SetDimensions() can be implemented as part of nsIWebBrowserChrome
where nsIBaseWindow is not necessary. This removes the need for
nsIEmbeddingSiteWindow.

Blur() has also been moved to nsIWebBrowserChrome, as only
nsContentTreeOwner has an actual implementation which we in theory also
want to call from BrowserChild/Parent, but the spec suggests to
"selectively or uniformly ignore calls".

GetVisibility() had an implementation in BrowserChild that pretended to
always be visible. Instead of providing an interface for that,
nsDocShell now handles the not implemented case for tree owners.

nsIEmbeddingSiteWindow::GetSiteWindow() used to call through to
nsIBaseWindow::GetParentNativeWindow().

The Get/SetDimensions() implementation has been replaced with a strongly
typed setter, which is now also used directly from nsGlobalWindowOuter
to avoid problems that come with autodetecting unchanged dimensions,
when the current dimensions are outdated (e.g. immediately reverting a
change can be ignored).

Differential Revision: https://phabricator.services.mozilla.com/D160260
2022-12-15 23:13:00 +00:00

491 lines
14 KiB
JavaScript

/* 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) {
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);
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}.`);
win[prop] = 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") {
this.propertyDeltas = aPropertyDeltas;
this.instant = aInstant;
this.msg = aMsg;
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})`;
info(`${msg}: ` + JSON.stringify(this.propertyDeltas));
await SpecialPowers.spawn(
aBrowsingContext,
[this.propertyDeltas, this.instant, ResizeMoveTest.PropInfo, msg],
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],
async (aLeft, aTop, aWidth, aHeight) => {
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.");
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.");
}
);
}
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}`
);
}
}
}