head.js (18991B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL; 6 7 const TAB_STATE_NEEDS_RESTORE = 1; 8 const TAB_STATE_RESTORING = 2; 9 10 const ROOT = getRootDirectory(gTestPath); 11 const HTTPROOT = ROOT.replace( 12 "chrome://mochitests/content/", 13 "http://example.com/" 14 ); 15 const HTTPSROOT = ROOT.replace( 16 "chrome://mochitests/content/", 17 "https://example.com/" 18 ); 19 20 const { SessionSaver } = ChromeUtils.importESModule( 21 "resource:///modules/sessionstore/SessionSaver.sys.mjs" 22 ); 23 const { SessionFile } = ChromeUtils.importESModule( 24 "resource:///modules/sessionstore/SessionFile.sys.mjs" 25 ); 26 const { TabState } = ChromeUtils.importESModule( 27 "resource:///modules/sessionstore/TabState.sys.mjs" 28 ); 29 const { TabStateFlusher } = ChromeUtils.importESModule( 30 "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" 31 ); 32 const { SessionStoreTestUtils } = ChromeUtils.importESModule( 33 "resource://testing-common/SessionStoreTestUtils.sys.mjs" 34 ); 35 36 const { PageWireframes } = ChromeUtils.importESModule( 37 "resource:///modules/sessionstore/PageWireframes.sys.mjs" 38 ); 39 40 const ss = SessionStore; 41 SessionStoreTestUtils.init(this, window); 42 43 // Some tests here assume that all restored tabs are loaded without waiting for 44 // the user to bring them to the foreground. We ensure this by resetting the 45 // related preference (see the "firefox.js" defaults file for details). 46 Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false); 47 registerCleanupFunction(function () { 48 Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand"); 49 }); 50 51 // Obtain access to internals 52 Services.prefs.setBoolPref("browser.sessionstore.debug", true); 53 registerCleanupFunction(function () { 54 Services.prefs.clearUserPref("browser.sessionstore.debug"); 55 }); 56 57 // This kicks off the search service used on about:home and allows the 58 // session restore tests to be run standalone without triggering errors. 59 Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs; 60 61 function provideWindow(aCallback, aURL, aFeatures) { 62 function callbackSoon(aWindow) { 63 executeSoon(function executeCallbackSoon() { 64 aCallback(aWindow); 65 }); 66 } 67 68 let win = openDialog( 69 AppConstants.BROWSER_CHROME_URL, 70 "", 71 aFeatures || "chrome,all,dialog=no", 72 aURL || "about:blank" 73 ); 74 whenWindowLoaded(win, function onWindowLoaded(aWin) { 75 if (!aURL) { 76 info("Loaded a blank window."); 77 callbackSoon(aWin); 78 return; 79 } 80 81 aWin.gBrowser.selectedBrowser.addEventListener( 82 "load", 83 function () { 84 callbackSoon(aWin); 85 }, 86 { capture: true, once: true } 87 ); 88 }); 89 } 90 91 // This assumes that tests will at least have some state/entries 92 function waitForBrowserState(aState, aSetStateCallback) { 93 return SessionStoreTestUtils.waitForBrowserState(aState, aSetStateCallback); 94 } 95 96 function promiseBrowserState(aState) { 97 return SessionStoreTestUtils.promiseBrowserState(aState); 98 } 99 100 function promiseTabState(tab, state) { 101 if (typeof state != "string") { 102 state = JSON.stringify(state); 103 } 104 105 let promise = promiseTabRestored(tab); 106 ss.setTabState(tab, state); 107 return promise; 108 } 109 110 function promiseWindowRestoring(win) { 111 return new Promise(resolve => 112 win.addEventListener("SSWindowRestoring", resolve, { once: true }) 113 ); 114 } 115 116 function promiseWindowRestored(win) { 117 return new Promise(resolve => 118 win.addEventListener("SSWindowRestored", resolve, { once: true }) 119 ); 120 } 121 122 async function setBrowserState(state, win = window) { 123 ss.setBrowserState(typeof state != "string" ? JSON.stringify(state) : state); 124 await promiseWindowRestored(win); 125 } 126 127 async function setWindowState(win, state, overwrite = false) { 128 ss.setWindowState( 129 win, 130 typeof state != "string" ? JSON.stringify(state) : state, 131 overwrite 132 ); 133 await promiseWindowRestored(win); 134 } 135 136 function waitForTopic(aTopic, aTimeout, aCallback) { 137 let observing = false; 138 function removeObserver() { 139 if (!observing) { 140 return; 141 } 142 Services.obs.removeObserver(observer, aTopic); 143 observing = false; 144 } 145 146 let timeout = setTimeout(function () { 147 removeObserver(); 148 aCallback(false); 149 }, aTimeout); 150 151 function observer() { 152 removeObserver(); 153 timeout = clearTimeout(timeout); 154 executeSoon(() => aCallback(true)); 155 } 156 157 registerCleanupFunction(function () { 158 removeObserver(); 159 if (timeout) { 160 clearTimeout(timeout); 161 } 162 }); 163 164 observing = true; 165 Services.obs.addObserver(observer, aTopic); 166 } 167 168 /** 169 * Wait until session restore has finished collecting its data and is 170 * has written that data ("sessionstore-state-write-complete"). 171 * 172 * @param {function} aCallback If sessionstore-state-write-complete is sent 173 * within buffering interval + 100 ms, the callback is passed |true|, 174 * otherwise, it is passed |false|. 175 */ 176 function waitForSaveState(aCallback) { 177 let timeout = 178 100 + Services.prefs.getIntPref("browser.sessionstore.interval"); 179 return waitForTopic("sessionstore-state-write-complete", timeout, aCallback); 180 } 181 function promiseSaveState() { 182 return new Promise((resolve, reject) => { 183 waitForSaveState(isSuccessful => { 184 if (!isSuccessful) { 185 reject(new Error("Save state timeout")); 186 } else { 187 resolve(); 188 } 189 }); 190 }); 191 } 192 function forceSaveState() { 193 return SessionSaver.run(); 194 } 195 196 function promiseRecoveryFileContents() { 197 let promise = forceSaveState(); 198 return promise.then(function () { 199 return IOUtils.readUTF8(SessionFile.Paths.recovery, { 200 decompress: true, 201 }); 202 }); 203 } 204 205 var promiseForEachSessionRestoreFile = async function (cb) { 206 for (let key of SessionFile.Paths.loadOrder) { 207 let data = ""; 208 try { 209 data = await IOUtils.readUTF8(SessionFile.Paths[key], { 210 decompress: true, 211 }); 212 } catch (ex) { 213 // Ignore missing files 214 if (!(DOMException.isInstance(ex) && ex.name == "NotFoundError")) { 215 throw ex; 216 } 217 } 218 cb(data, key); 219 } 220 }; 221 222 function promiseBrowserLoaded( 223 aBrowser, 224 ignoreSubFrames = true, 225 wantLoad = null 226 ) { 227 return BrowserTestUtils.browserLoaded(aBrowser, !ignoreSubFrames, wantLoad); 228 } 229 230 function whenWindowLoaded(aWindow, aCallback) { 231 aWindow.addEventListener( 232 "load", 233 function () { 234 executeSoon(function executeWhenWindowLoaded() { 235 aCallback(aWindow); 236 }); 237 }, 238 { once: true } 239 ); 240 } 241 function promiseWindowLoaded(aWindow) { 242 return new Promise(resolve => whenWindowLoaded(aWindow, resolve)); 243 } 244 245 var gUniqueCounter = 0; 246 function r() { 247 return Date.now() + "-" + ++gUniqueCounter; 248 } 249 250 function* BrowserWindowIterator() { 251 for (let currentWindow of Services.wm.getEnumerator("navigator:browser")) { 252 if (!currentWindow.closed) { 253 yield currentWindow; 254 } 255 } 256 } 257 258 var gWebProgressListener = { 259 _callback: null, 260 261 setCallback(aCallback) { 262 if (!this._callback) { 263 window.gBrowser.addTabsProgressListener(this); 264 } 265 this._callback = aCallback; 266 }, 267 268 unsetCallback() { 269 if (this._callback) { 270 this._callback = null; 271 window.gBrowser.removeTabsProgressListener(this); 272 } 273 }, 274 275 onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, _aStatus) { 276 if ( 277 aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && 278 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && 279 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW 280 ) { 281 this._callback(aBrowser); 282 } 283 }, 284 }; 285 286 registerCleanupFunction(function () { 287 gWebProgressListener.unsetCallback(); 288 }); 289 290 var gProgressListener = { 291 _callback: null, 292 293 setCallback(callback) { 294 Services.obs.addObserver(this, "sessionstore-debug-tab-restored"); 295 this._callback = callback; 296 }, 297 298 unsetCallback() { 299 if (this._callback) { 300 this._callback = null; 301 Services.obs.removeObserver(this, "sessionstore-debug-tab-restored"); 302 } 303 }, 304 305 observe(browser) { 306 gProgressListener.onRestored(browser); 307 }, 308 309 onRestored(browser) { 310 if (ss.getInternalObjectState(browser) == TAB_STATE_RESTORING) { 311 let args = [browser].concat(gProgressListener._countTabs()); 312 gProgressListener._callback.apply(gProgressListener, args); 313 } 314 }, 315 316 _countTabs() { 317 let needsRestore = 0, 318 isRestoring = 0, 319 wasRestored = 0; 320 321 for (let win of BrowserWindowIterator()) { 322 for (let i = 0; i < win.gBrowser.tabs.length; i++) { 323 let browser = win.gBrowser.tabs[i].linkedBrowser; 324 let state = ss.getInternalObjectState(browser); 325 if (browser.isConnected && !state) { 326 wasRestored++; 327 } else if (state == TAB_STATE_RESTORING) { 328 isRestoring++; 329 } else if (state == TAB_STATE_NEEDS_RESTORE || !browser.isConnected) { 330 needsRestore++; 331 } 332 } 333 } 334 return [needsRestore, isRestoring, wasRestored]; 335 }, 336 }; 337 338 registerCleanupFunction(function () { 339 gProgressListener.unsetCallback(); 340 }); 341 342 // Close all but our primary window. 343 function promiseAllButPrimaryWindowClosed() { 344 let windows = []; 345 for (let win of BrowserWindowIterator()) { 346 if (win != window) { 347 windows.push(win); 348 } 349 } 350 351 return Promise.all(windows.map(BrowserTestUtils.closeWindow)); 352 } 353 354 // Forget all closed windows. 355 function forgetClosedWindows() { 356 while (ss.getClosedWindowCount() > 0) { 357 ss.forgetClosedWindow(0); 358 } 359 } 360 361 // Forget all closed tabs for a window 362 function forgetClosedTabs(win) { 363 const closedTabCount = ss.getClosedTabCountForWindow(win); 364 for (let i = 0; i < closedTabCount; i++) { 365 try { 366 ss.forgetClosedTab(win, 0); 367 } catch (err) { 368 // This will fail if there are tab groups in here 369 } 370 } 371 } 372 373 function forgetSavedTabGroups() { 374 const tabGroups = ss.getSavedTabGroups(); 375 tabGroups.forEach(tabGroup => ss.forgetSavedTabGroup(tabGroup.id)); 376 } 377 378 function forgetClosedTabGroups(win) { 379 const tabGroups = ss.getClosedTabGroups(win); 380 tabGroups.forEach(tabGroup => ss.forgetClosedTabGroup(win, tabGroup.id)); 381 } 382 383 /** 384 * When opening a new window it is not sufficient to wait for its load event. 385 * We need to use whenDelayedStartupFinshed() here as the browser window's 386 * delayedStartup() routine is executed one tick after the window's load event 387 * has been dispatched. browser-delayed-startup-finished might be deferred even 388 * further if parts of the window's initialization process take more time than 389 * expected (e.g. reading a big session state from disk). 390 */ 391 function whenNewWindowLoaded(aOptions, aCallback) { 392 let features = ""; 393 let url = "about:blank"; 394 395 if ((aOptions && aOptions.private) || false) { 396 features = ",private"; 397 url = "about:privatebrowsing"; 398 } 399 400 let win = openDialog( 401 AppConstants.BROWSER_CHROME_URL, 402 "", 403 "chrome,all,dialog=no" + features, 404 url 405 ); 406 let delayedStartup = promiseDelayedStartupFinished(win); 407 408 let browserLoaded = new Promise(resolve => { 409 if (url == "about:blank") { 410 resolve(); 411 return; 412 } 413 414 win.addEventListener( 415 "load", 416 function () { 417 let browser = win.gBrowser.selectedBrowser; 418 promiseBrowserLoaded(browser).then(resolve); 419 }, 420 { once: true } 421 ); 422 }); 423 424 Promise.all([delayedStartup, browserLoaded]).then(() => aCallback(win)); 425 } 426 function promiseNewWindowLoaded(aOptions) { 427 return new Promise(resolve => whenNewWindowLoaded(aOptions, resolve)); 428 } 429 430 /** 431 * This waits for the browser-delayed-startup-finished notification of a given 432 * window. It indicates that the windows has loaded completely and is ready to 433 * be used for testing. 434 */ 435 function whenDelayedStartupFinished(aWindow, aCallback) { 436 Services.obs.addObserver(function observer(aSubject, aTopic) { 437 if (aWindow == aSubject) { 438 Services.obs.removeObserver(observer, aTopic); 439 executeSoon(aCallback); 440 } 441 }, "browser-delayed-startup-finished"); 442 } 443 function promiseDelayedStartupFinished(aWindow) { 444 return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve)); 445 } 446 447 function promiseTabRestored(tab) { 448 return BrowserTestUtils.waitForEvent(tab, "SSTabRestored"); 449 } 450 451 function promiseTabRestoring(tab) { 452 return BrowserTestUtils.waitForEvent(tab, "SSTabRestoring"); 453 } 454 455 // Removes the given tab immediately and returns a promise that resolves when 456 // all pending status updates (messages) of the closing tab have been received. 457 function promiseRemoveTabAndSessionState(tab) { 458 let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab); 459 BrowserTestUtils.removeTab(tab); 460 return sessionUpdatePromise; 461 } 462 463 // Write DOMSessionStorage data to the given browser. 464 function modifySessionStorage(browser, storageData, storageOptions = {}) { 465 let browsingContext = browser.browsingContext; 466 if (storageOptions && "frameIndex" in storageOptions) { 467 browsingContext = browsingContext.children[storageOptions.frameIndex]; 468 } 469 470 return SpecialPowers.spawn( 471 browsingContext, 472 [[storageData, storageOptions]], 473 async function ([data]) { 474 let frame = content; 475 let keys = new Set(Object.keys(data)); 476 let isClearing = !keys.size; 477 let storage = frame.sessionStorage; 478 479 return new Promise(resolve => { 480 docShell.chromeEventHandler.addEventListener( 481 "MozSessionStorageChanged", 482 function onStorageChanged(event) { 483 if (event.storageArea == storage) { 484 keys.delete(event.key); 485 } 486 487 if (keys.size == 0) { 488 docShell.chromeEventHandler.removeEventListener( 489 "MozSessionStorageChanged", 490 onStorageChanged, 491 true 492 ); 493 resolve(); 494 } 495 }, 496 true 497 ); 498 499 if (isClearing) { 500 storage.clear(); 501 } else { 502 for (let key of keys) { 503 frame.sessionStorage[key] = data[key]; 504 } 505 } 506 }); 507 } 508 ); 509 } 510 511 function pushPrefs(...aPrefs) { 512 return SpecialPowers.pushPrefEnv({ set: aPrefs }); 513 } 514 515 function popPrefs() { 516 return SpecialPowers.popPrefEnv(); 517 } 518 519 function setScrollPosition(bc, x, y) { 520 return SpecialPowers.spawn(bc, [x, y], (childX, childY) => { 521 return new Promise(resolve => { 522 content.addEventListener( 523 "mozvisualscroll", 524 function onScroll(event) { 525 if (content.document.ownerGlobal.visualViewport == event.target) { 526 content.removeEventListener("mozvisualscroll", onScroll, { 527 mozSystemGroup: true, 528 }); 529 resolve(); 530 } 531 }, 532 { mozSystemGroup: true } 533 ); 534 content.scrollTo(childX, childY); 535 }); 536 }); 537 } 538 539 async function checkScroll(tab, expected, msg) { 540 let browser = tab.linkedBrowser; 541 await TabStateFlusher.flush(browser); 542 543 let scroll = JSON.parse(ss.getTabState(tab)).scroll || null; 544 is(JSON.stringify(scroll), JSON.stringify(expected), msg); 545 } 546 547 function whenDomWindowClosedHandled(aCallback) { 548 Services.obs.addObserver(function observer(aSubject, aTopic) { 549 Services.obs.removeObserver(observer, aTopic); 550 aCallback(); 551 }, "sessionstore-debug-domwindowclosed-handled"); 552 } 553 554 function getPropertyOfFormField(browserContext, selector, propName) { 555 return SpecialPowers.spawn( 556 browserContext, 557 [selector, propName], 558 (selectorChild, propNameChild) => { 559 return content.document.querySelector(selectorChild)[propNameChild]; 560 } 561 ); 562 } 563 564 function setPropertyOfFormField(browserContext, selector, propName, newValue) { 565 return SpecialPowers.spawn( 566 browserContext, 567 [selector, propName, newValue], 568 (selectorChild, propNameChild, newValueChild) => { 569 let node = content.document.querySelector(selectorChild); 570 node[propNameChild] = newValueChild; 571 572 let event = node.ownerDocument.createEvent("UIEvents"); 573 event.initUIEvent("input", true, true, node.ownerGlobal, 0); 574 node.dispatchEvent(event); 575 } 576 ); 577 } 578 579 function promiseOnHistoryReplaceEntry(browser) { 580 return new Promise(resolve => { 581 let sessionHistory = browser.browsingContext?.sessionHistory; 582 if (sessionHistory) { 583 var historyListener = { 584 OnHistoryNewEntry() {}, 585 OnHistoryGotoIndex() {}, 586 OnHistoryPurge() {}, 587 OnHistoryReload() { 588 return true; 589 }, 590 591 OnHistoryReplaceEntry() { 592 resolve(); 593 }, 594 595 QueryInterface: ChromeUtils.generateQI([ 596 "nsISHistoryListener", 597 "nsISupportsWeakReference", 598 ]), 599 }; 600 601 sessionHistory.addSHistoryListener(historyListener); 602 } 603 }); 604 } 605 606 function loadTestSubscript(filePath) { 607 Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this); 608 } 609 610 function addCoopTask(aFile, aTest, aUrlRoot) { 611 async function taskToBeAdded() { 612 info(`File ${aFile} has COOP headers enabled`); 613 let filePath = `browser/browser/components/sessionstore/test/${aFile}`; 614 let url = aUrlRoot + `coopHeaderCommon.sjs?fileRoot=${filePath}`; 615 await aTest(url); 616 } 617 Object.defineProperty(taskToBeAdded, "name", { value: aTest.name }); 618 add_task(taskToBeAdded); 619 } 620 621 function addNonCoopTask(aFile, aTest, aUrlRoot) { 622 async function taskToBeAdded() { 623 await aTest(aUrlRoot + aFile); 624 } 625 Object.defineProperty(taskToBeAdded, "name", { value: aTest.name }); 626 add_task(taskToBeAdded); 627 } 628 629 function openAndCloseTab(window, url) { 630 return SessionStoreTestUtils.openAndCloseTab(window, url); 631 } 632 633 /** 634 * This is regrettable, but when `promiseBrowserState` resolves, we're still 635 * midway through loading the tabs. To avoid race conditions in URLs for tabs 636 * being available, wait for all the loads to finish: 637 */ 638 function promiseSessionStoreLoads(numberOfLoads) { 639 let loadsSeen = 0; 640 return new Promise(resolve => { 641 Services.obs.addObserver(function obs(browser) { 642 loadsSeen++; 643 if (loadsSeen == numberOfLoads) { 644 resolve(); 645 } 646 // The typeof check is here to avoid one test messing with everything else by 647 // keeping the observer indefinitely. 648 if (typeof info == "undefined" || loadsSeen >= numberOfLoads) { 649 Services.obs.removeObserver(obs, "sessionstore-debug-tab-restored"); 650 } 651 info("Saw load for " + browser.currentURI.spec); 652 }, "sessionstore-debug-tab-restored"); 653 }); 654 } 655 656 function triggerClickOn(target, options) { 657 let promise = BrowserTestUtils.waitForEvent(target, "click"); 658 if (AppConstants.platform == "macosx") { 659 options.metaKey = options.ctrlKey; 660 delete options.ctrlKey; 661 } 662 EventUtils.synthesizeMouseAtCenter(target, options); 663 return promise; 664 } 665 666 async function openTabMenuFor(tab) { 667 let tabMenu = tab.ownerDocument.getElementById("tabContextMenu"); 668 669 let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown"); 670 EventUtils.synthesizeMouseAtCenter( 671 tab, 672 { type: "contextmenu" }, 673 tab.ownerGlobal 674 ); 675 await tabMenuShown; 676 677 return tabMenu; 678 }