head.js (17233B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService( 7 SpecialPowers.Ci.nsIGfxInfo 8 ); 9 10 async function waitForBlockedPopups(numberOfPopups, { doc }) { 11 let toolbarDoc = doc || document; 12 let menupopup = toolbarDoc.getElementById("blockedPopupOptions"); 13 await BrowserTestUtils.waitForCondition(() => { 14 let popups = menupopup.querySelectorAll("[popupReportIndex]"); 15 return popups.length == numberOfPopups; 16 }, `Waiting for ${numberOfPopups} popups`); 17 } 18 19 /* 20 * Tests that a sequence of size changes ultimately results in the latest 21 * requested size. The test also fails when an unexpected window size is 22 * observed in a resize event. 23 * 24 * aPropertyDeltas List of objects where keys describe the name of a window 25 * property and the values the difference to its initial 26 * value. 27 * 28 * aInstant Issue changes without additional waiting in between. 29 * 30 * A brief example of the resutling code that is effectively run for the 31 * following list of deltas: 32 * [{outerWidth: 5, outerHeight: 10}, {outerWidth: 10}] 33 * 34 * let initialWidth = win.outerWidth; 35 * let initialHeight = win.outerHeight; 36 * 37 * if (aInstant) { 38 * win.outerWidth = initialWidth + 5; 39 * win.outerHeight = initialHeight + 10; 40 * 41 * win.outerWidth = initialWidth + 10; 42 * } else { 43 * win.requestAnimationFrame(() => { 44 * win.outerWidth = initialWidth + 5; 45 * win.outerHeight = initialHeight + 10; 46 * 47 * win.requestAnimationFrame(() => { 48 * win.outerWidth = initialWidth + 10; 49 * }); 50 * }); 51 * } 52 */ 53 async function testPropertyDeltas( 54 aPropertyDeltas, 55 aInstant, 56 aPropInfo, 57 aMsg, 58 aWaitForCompletion 59 ) { 60 let msg = `[${aMsg}]`; 61 62 let win = this.content.popup || this.content.wrappedJSObject; 63 64 // Property names and mapping from ResizeMoveTest 65 let { 66 sizeProps, 67 positionProps /* can be empty/incomplete as workaround on Linux */, 68 readonlyProps, 69 crossBoundsMapping, 70 } = aPropInfo; 71 72 let stringifyState = state => { 73 let stateMsg = sizeProps 74 .concat(positionProps) 75 .filter(prop => state[prop] !== undefined) 76 .map(prop => `${prop}: ${state[prop]}`) 77 .join(", "); 78 return `{ ${stateMsg} }`; 79 }; 80 81 let initialState = {}; 82 let finalState = {}; 83 84 info("Initializing all values to current state."); 85 for (let prop of sizeProps.concat(positionProps)) { 86 let value = win[prop]; 87 initialState[prop] = value; 88 finalState[prop] = value; 89 } 90 91 // List of potential states during resize events. The current state is also 92 // considered valid, as the resize event might still be outstanding. 93 let validResizeStates = [initialState]; 94 95 let updateFinalState = (aProp, aDelta) => { 96 if ( 97 readonlyProps.includes(aProp) || 98 !sizeProps.concat(positionProps).includes(aProp) 99 ) { 100 throw new Error(`Unexpected property "${aProp}".`); 101 } 102 103 // Update both properties of the same axis. 104 let otherProp = crossBoundsMapping[aProp]; 105 finalState[aProp] = initialState[aProp] + aDelta; 106 finalState[otherProp] = initialState[otherProp] + aDelta; 107 108 // Mark size as valid in resize event. 109 if (sizeProps.includes(aProp)) { 110 let state = {}; 111 sizeProps.forEach(p => (state[p] = finalState[p])); 112 validResizeStates.push(state); 113 } 114 }; 115 116 info("Adding resize event listener."); 117 let resizeCount = 0; 118 let resizeListener = evt => { 119 resizeCount++; 120 121 let currentSizeState = {}; 122 sizeProps.forEach(p => (currentSizeState[p] = win[p])); 123 124 info( 125 `${msg} ${resizeCount}. resize event: ${stringifyState(currentSizeState)}` 126 ); 127 let matchingIndex = validResizeStates.findIndex(state => 128 sizeProps.every(p => state[p] == currentSizeState[p]) 129 ); 130 if (matchingIndex < 0) { 131 info(`${msg} Size state should have been one of:`); 132 for (let state of validResizeStates) { 133 info(stringifyState(state)); 134 } 135 } 136 137 if (win.gBrowser && evt.target != win) { 138 // Without e10s we receive content resize events in chrome windows. 139 todo(false, `${msg} Resize event target is our window.`); 140 return; 141 } 142 143 Assert.greaterOrEqual( 144 matchingIndex, 145 0, 146 `${msg} Valid intermediate state. Current: ` + 147 stringifyState(currentSizeState) 148 ); 149 150 // No longer allow current and preceding states. 151 validResizeStates.splice(0, matchingIndex + 1); 152 }; 153 win.addEventListener("resize", resizeListener); 154 155 info("Starting property changes."); 156 await new Promise(resolve => { 157 let index = 0; 158 let next = async () => { 159 let pre = `${msg} [${index + 1}/${aPropertyDeltas.length}]`; 160 161 let deltaObj = aPropertyDeltas[index]; 162 for (let prop in deltaObj) { 163 updateFinalState(prop, deltaObj[prop]); 164 165 let targetValue = initialState[prop] + deltaObj[prop]; 166 info(`${pre} Setting ${prop} to ${targetValue}.`); 167 if (sizeProps.includes(prop)) { 168 win.resizeTo(finalState.outerWidth, finalState.outerHeight); 169 } else { 170 win.moveTo(finalState.screenX, finalState.screenY); 171 } 172 if (aWaitForCompletion) { 173 await ContentTaskUtils.waitForCondition( 174 () => win[prop] == targetValue, 175 `${msg} Waiting for ${prop} to be ${targetValue}.` 176 ); 177 } 178 } 179 180 index++; 181 if (index < aPropertyDeltas.length) { 182 scheduleNext(); 183 } else { 184 resolve(); 185 } 186 }; 187 188 let scheduleNext = () => { 189 if (aInstant) { 190 next(); 191 } else { 192 info(`${msg} Requesting animation frame.`); 193 win.requestAnimationFrame(next); 194 } 195 }; 196 scheduleNext(); 197 }); 198 199 try { 200 info(`${msg} Waiting for window to match the final state.`); 201 await ContentTaskUtils.waitForCondition( 202 () => sizeProps.concat(positionProps).every(p => win[p] == finalState[p]), 203 "Waiting for final state." 204 ); 205 } catch (e) {} 206 207 info(`${msg} Checking final state.`); 208 info(`${msg} Exepected: ${stringifyState(finalState)}`); 209 info(`${msg} Actual: ${stringifyState(win)}`); 210 for (let prop of sizeProps.concat(positionProps)) { 211 is(win[prop], finalState[prop], `${msg} Expected final value for ${prop}`); 212 } 213 214 win.removeEventListener("resize", resizeListener); 215 } 216 217 function roundedCenter(aDimension, aOrigin) { 218 let center = aOrigin + Math.floor(aDimension / 2); 219 return center - (center % 100); 220 } 221 222 class ResizeMoveTest { 223 static WindowWidth = 200; 224 static WindowHeight = 200; 225 static WindowLeft = roundedCenter(screen.availWidth - 200, screen.left); 226 static WindowTop = roundedCenter(screen.availHeight - 200, screen.top); 227 228 static PropInfo = { 229 sizeProps: ["outerWidth", "outerHeight", "innerWidth", "innerHeight"], 230 positionProps: [ 231 "screenX", 232 "screenY", 233 /* readonly */ "mozInnerScreenX", 234 /* readonly */ "mozInnerScreenY", 235 ], 236 readonlyProps: ["mozInnerScreenX", "mozInnerScreenY"], 237 crossAxisMapping: { 238 outerWidth: "outerHeight", 239 outerHeight: "outerWidth", 240 innerWidth: "innerHeight", 241 innerHeight: "innerWidth", 242 screenX: "screenY", 243 screenY: "screenX", 244 mozInnerScreenX: "mozInnerScreenY", 245 mozInnerScreenY: "mozInnerScreenX", 246 }, 247 crossBoundsMapping: { 248 outerWidth: "innerWidth", 249 outerHeight: "innerHeight", 250 innerWidth: "outerWidth", 251 innerHeight: "outerHeight", 252 screenX: "mozInnerScreenX", 253 screenY: "mozInnerScreenY", 254 mozInnerScreenX: "screenX", 255 mozInnerScreenY: "screenY", 256 }, 257 }; 258 259 constructor( 260 aPropertyDeltas, 261 aInstant = false, 262 aMsg = "ResizeMoveTest", 263 aWaitForCompletion = false 264 ) { 265 this.propertyDeltas = aPropertyDeltas; 266 this.instant = aInstant; 267 this.msg = aMsg; 268 this.waitForCompletion = aWaitForCompletion; 269 270 // Allows to ignore positions while testing. 271 this.ignorePositions = false; 272 // Allows to ignore only mozInnerScreenX/Y properties while testing. 273 this.ignoreMozInnerScreen = false; 274 // Allows to skip checking the restored position after testing. 275 this.ignoreRestoredPosition = false; 276 277 if (AppConstants.platform == "linux" && !SpecialPowers.isHeadless) { 278 // We can occasionally start the test while nsWindow reports a wrong 279 // client offset (gdk origin and root_origin are out of sync). This 280 // results in false expectations for the final mozInnerScreenX/Y values. 281 this.ignoreMozInnerScreen = !ResizeMoveTest.hasCleanUpTask; 282 283 let { positionProps } = ResizeMoveTest.PropInfo; 284 let resizeOnlyTest = aPropertyDeltas.every(deltaObj => 285 positionProps.every(prop => deltaObj[prop] === undefined) 286 ); 287 288 let isWayland = gfxInfo.windowProtocol == "wayland"; 289 if (resizeOnlyTest && isWayland) { 290 // On Wayland we can't move the window in general. The window also 291 // doesn't necessarily open our specified position. 292 this.ignoreRestoredPosition = true; 293 // We can catch bad screenX/Y at the start of the first test in a 294 // window. 295 this.ignorePositions = !ResizeMoveTest.hasCleanUpTask; 296 } 297 } 298 299 if (!ResizeMoveTest.hasCleanUpTask) { 300 ResizeMoveTest.hasCleanUpTask = true; 301 registerCleanupFunction(ResizeMoveTest.Cleanup); 302 } 303 304 add_task(async () => { 305 let tab = await ResizeMoveTest.GetOrCreateTab(); 306 let browsingContext = 307 await ResizeMoveTest.GetOrCreatePopupBrowsingContext(); 308 if (!browsingContext) { 309 return; 310 } 311 312 info("=== Running in content. ==="); 313 await this.run(browsingContext, `${this.msg} (content)`); 314 await this.restorePopupState(browsingContext); 315 316 info("=== Running in chrome. ==="); 317 let popupChrome = browsingContext.topChromeWindow; 318 await this.run(popupChrome.browsingContext, `${this.msg} (chrome)`); 319 await this.restorePopupState(browsingContext); 320 321 info("=== Running in opener. ==="); 322 await this.run(tab.linkedBrowser, `${this.msg} (opener)`); 323 await this.restorePopupState(browsingContext); 324 }); 325 } 326 327 async run(aBrowsingContext, aMsg) { 328 let testType = this.instant ? "instant" : "fanned out"; 329 let msg = `${aMsg} (${testType})`; 330 331 let propInfo = {}; 332 for (let k in ResizeMoveTest.PropInfo) { 333 propInfo[k] = ResizeMoveTest.PropInfo[k]; 334 } 335 if (this.ignoreMozInnerScreen) { 336 todo(false, `[${aMsg}] Shouldn't ignore mozInnerScreenX/Y.`); 337 propInfo.positionProps = propInfo.positionProps.filter( 338 prop => !["mozInnerScreenX", "mozInnerScreenY"].includes(prop) 339 ); 340 } 341 if (this.ignorePositions) { 342 todo(false, `[${aMsg}] Shouldn't ignore position.`); 343 propInfo.positionProps = []; 344 } 345 346 info(`${msg}: ` + JSON.stringify(this.propertyDeltas)); 347 await SpecialPowers.spawn( 348 aBrowsingContext, 349 [ 350 this.propertyDeltas, 351 this.instant, 352 propInfo, 353 msg, 354 this.waitForCompletion, 355 ], 356 testPropertyDeltas 357 ); 358 } 359 360 async restorePopupState(aBrowsingContext) { 361 info("Restore popup state."); 362 363 let { deltaWidth, deltaHeight } = await SpecialPowers.spawn( 364 aBrowsingContext, 365 [], 366 () => { 367 return { 368 deltaWidth: this.content.outerWidth - this.content.innerWidth, 369 deltaHeight: this.content.outerHeight - this.content.innerHeight, 370 }; 371 } 372 ); 373 374 let chromeWindow = aBrowsingContext.topChromeWindow; 375 let { 376 WindowLeft: left, 377 WindowTop: top, 378 WindowWidth: width, 379 WindowHeight: height, 380 } = ResizeMoveTest; 381 382 chromeWindow.resizeTo(width + deltaWidth, height + deltaHeight); 383 chromeWindow.moveTo(left, top); 384 385 await SpecialPowers.spawn( 386 aBrowsingContext, 387 [left, top, width, height, this.ignoreRestoredPosition], 388 async (aLeft, aTop, aWidth, aHeight, aIgnorePosition) => { 389 let win = this.content.wrappedJSObject; 390 391 info("Waiting for restored size."); 392 await ContentTaskUtils.waitForCondition( 393 () => win.innerWidth == aWidth && win.innerHeight === aHeight, 394 "Waiting for restored size." 395 ); 396 is(win.innerWidth, aWidth, "Restored width."); 397 is(win.innerHeight, aHeight, "Restored height."); 398 399 if (!aIgnorePosition) { 400 info("Waiting for restored position."); 401 await ContentTaskUtils.waitForCondition( 402 () => win.screenX == aLeft && win.screenY === aTop, 403 "Waiting for restored position." 404 ); 405 is(win.screenX, aLeft, "Restored screenX."); 406 is(win.screenY, aTop, "Restored screenY."); 407 } else { 408 todo(false, "Shouldn't ignore restored position."); 409 } 410 } 411 ); 412 } 413 414 static async GetOrCreateTab() { 415 if (ResizeMoveTest.tab) { 416 return ResizeMoveTest.tab; 417 } 418 419 info("Opening tab."); 420 ResizeMoveTest.tab = await BrowserTestUtils.openNewForegroundTab( 421 window.gBrowser, 422 "https://example.net/browser/browser/base/content/test/popups/popup_blocker_a.html" 423 ); 424 return ResizeMoveTest.tab; 425 } 426 427 static async GetOrCreatePopupBrowsingContext() { 428 if (ResizeMoveTest.popupBrowsingContext) { 429 if (!ResizeMoveTest.popupBrowsingContext.isActive) { 430 return undefined; 431 } 432 return ResizeMoveTest.popupBrowsingContext; 433 } 434 435 let tab = await ResizeMoveTest.GetOrCreateTab(); 436 info("Opening popup."); 437 ResizeMoveTest.popupBrowsingContext = await SpecialPowers.spawn( 438 tab.linkedBrowser, 439 [ 440 ResizeMoveTest.WindowWidth, 441 ResizeMoveTest.WindowHeight, 442 ResizeMoveTest.WindowLeft, 443 ResizeMoveTest.WindowTop, 444 ], 445 async (aWidth, aHeight, aLeft, aTop) => { 446 let win = this.content.open( 447 this.content.document.location.href, 448 "_blank", 449 `left=${aLeft},top=${aTop},width=${aWidth},height=${aHeight}` 450 ); 451 this.content.popup = win; 452 453 await new Promise(r => (win.onload = r)); 454 455 return win.browsingContext; 456 } 457 ); 458 459 return ResizeMoveTest.popupBrowsingContext; 460 } 461 462 static async Cleanup() { 463 let browsingContext = ResizeMoveTest.popupBrowsingContext; 464 if (browsingContext) { 465 await SpecialPowers.spawn(browsingContext, [], () => { 466 this.content.close(); 467 }); 468 delete ResizeMoveTest.popupBrowsingContext; 469 } 470 471 let tab = ResizeMoveTest.tab; 472 if (tab) { 473 await BrowserTestUtils.removeTab(tab); 474 delete ResizeMoveTest.tab; 475 } 476 ResizeMoveTest.hasCleanUpTask = false; 477 } 478 } 479 480 function chaosRequestLongerTimeout(aDoRequest) { 481 if (aDoRequest && parseInt(Services.env.get("MOZ_CHAOSMODE"), 16)) { 482 requestLongerTimeout(2); 483 } 484 } 485 486 function createGenericResizeTests(aFirstValue, aSecondValue, aInstant, aMsg) { 487 // Runtime almost doubles in chaos mode on Mac. 488 chaosRequestLongerTimeout(AppConstants.platform == "macosx"); 489 490 let { crossBoundsMapping, crossAxisMapping } = ResizeMoveTest.PropInfo; 491 492 for (let prop of ["innerWidth", "outerHeight"]) { 493 // Mixing inner and outer property. 494 for (let secondProp of [prop, crossBoundsMapping[prop]]) { 495 let first = {}; 496 first[prop] = aFirstValue; 497 let second = {}; 498 second[secondProp] = aSecondValue; 499 new ResizeMoveTest( 500 [first, second], 501 aInstant, 502 `${aMsg} ${prop},${secondProp}` 503 ); 504 } 505 } 506 507 for (let prop of ["innerHeight", "outerWidth"]) { 508 let first = {}; 509 first[prop] = aFirstValue; 510 let second = {}; 511 second[prop] = aSecondValue; 512 513 // Setting property of other axis before/between two changes. 514 let otherProps = [ 515 crossAxisMapping[prop], 516 crossAxisMapping[crossBoundsMapping[prop]], 517 ]; 518 for (let interferenceProp of otherProps) { 519 let interference = {}; 520 interference[interferenceProp] = 20; 521 new ResizeMoveTest( 522 [first, interference, second], 523 aInstant, 524 `${aMsg} ${prop},${interferenceProp},${prop}` 525 ); 526 new ResizeMoveTest( 527 [interference, first, second], 528 aInstant, 529 `${aMsg} ${interferenceProp},${prop},${prop}` 530 ); 531 } 532 } 533 } 534 535 function createGenericMoveTests(aInstant, aMsg) { 536 // Runtime almost doubles in chaos mode on Mac. 537 chaosRequestLongerTimeout(AppConstants.platform == "macosx"); 538 539 let { crossAxisMapping } = ResizeMoveTest.PropInfo; 540 541 for (let prop of ["screenX", "screenY"]) { 542 for (let [v1, v2, msg] of [ 543 [9, 10, `${aMsg}`], 544 [11, 11, `${aMsg} repeat`], 545 [12, 0, `${aMsg} revert`], 546 ]) { 547 let first = {}; 548 first[prop] = v1; 549 let second = {}; 550 second[prop] = v2; 551 new ResizeMoveTest([first, second], aInstant, `${msg} ${prop},${prop}`); 552 553 let interferenceProp = crossAxisMapping[prop]; 554 let interference = {}; 555 interference[interferenceProp] = 20; 556 new ResizeMoveTest( 557 [first, interference, second], 558 aInstant, 559 `${aMsg} ${prop},${interferenceProp},${prop}` 560 ); 561 new ResizeMoveTest( 562 [interference, first, second], 563 aInstant, 564 `${msg} ${interferenceProp},${prop},${prop}` 565 ); 566 } 567 } 568 }