diff options
Diffstat (limited to 'browser/base/content/test/popups/head.js')
-rw-r--r-- | browser/base/content/test/popups/head.js | 574 |
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}` + ); + } + } +} |