/** * Async utility function for ensuring that no unexpected uninterruptible * reflows occur during some period of time in a window. * * The helper works by running a JS function before each event is * dispatched that attempts to dirty the layout tree - the idea being * that this puts us in the "worst case scenario" so that any JS * that attempts to query for layout or style information will cause * a reflow to fire. We also dirty the layout tree after each reflow * occurs, for good measure. * * This sounds good in theory, but it's trickier in practice due to * various optimizations in our Layout engine. The default function * for dirtying the layout tree adds a margin to the first element * child it finds in the window to a maximum of 3px, and then goes * back to 0px again and loops. * * This is not sufficient for reflows that we expect to happen within * scrollable frames, as Gecko is able to side-step reflowing the * contents of a scrollable frame if outer frames are dirtied. Because * of this, it's currently possible to override the default node to * dirty with one more appropriate for the test. * * It is also theoretically possible for enough events to fire between * reflows such that the before and after state of the layout tree is * exactly the same, meaning that no reflow is required, which opens * us up to missing expected reflows. This seems to be possible in * theory, but hasn't yet shown up in practice - it's just something * to be aware of. * * Bug 1363361 has been filed for a more reliable way of dirtying layout. * * @param testFn (async function) * The async function that will exercise the browser activity that is * being tested for reflows. * @param expectedStacks (Array, optional) * An Array of Arrays representing stacks. * * Example: * * [ * // This reflow is caused by lorem ipsum * [ * "select@chrome://global/content/bindings/textbox.xml", * "focusAndSelectUrlBar@chrome://browser/content/browser.js", * "openLinkIn@chrome://browser/content/utilityOverlay.js", * "openUILinkIn@chrome://browser/content/utilityOverlay.js", * "BrowserOpenTab@chrome://browser/content/browser.js", * ], * * // This reflow is caused by lorem ipsum * [ * "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml", * "_fillTrailingGap@chrome://browser/content/tabbrowser.xml", * "_handleNewTab@chrome://browser/content/tabbrowser.xml", * "onxbltransitionend@chrome://browser/content/tabbrowser.xml", * ], * * ] * * Note that line numbers are not included in the stacks. * * Order of the reflows doesn't matter. Expected reflows that aren't seen * will cause an assertion failure. When this argument is not passed, * it defaults to the empty Array, meaning no reflows are expected. * @param window (browser window, optional) * The browser window to monitor. Defaults to the current window. */ async function withReflowObserver(testFn, expectedStacks = [], win = window) { let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let dirtyFrameFn = () => { try { dwu.ensureDirtyRootFrame(); } catch (e) { // If this fails, we should probably make note of it, but it's not fatal. info("Note: ensureDirtyRootFrame threw an exception."); } }; let els = Cc["@mozilla.org/eventlistenerservice;1"] .getService(Ci.nsIEventListenerService); // We're going to remove the stacks one by one as we see them so that // we can check for expected, unseen reflows, so let's clone the array. expectedStacks = expectedStacks.slice(0); let observer = { reflow(start, end) { // Gather information about the current code path, slicing out the current // frame. let path = (new Error().stack).split("\n").slice(1).map(line => { return line.replace(/:\d+:\d+$/, ""); }).join("|"); let pathWithLineNumbers = (new Error().stack).split("\n").slice(1); // Just in case, dirty the frame now that we've reflowed. dirtyFrameFn(); // Stack trace is empty. Reflow was triggered by native code, which // we ignore. if (path === "") { return; } let index = expectedStacks.findIndex(stack => path.startsWith(stack.join("|"))); if (index != -1) { Assert.ok(true, "expected uninterruptible reflow: '" + JSON.stringify(pathWithLineNumbers, null, "\t") + "'"); expectedStacks.splice(index, 1); } else { Assert.ok(false, "unexpected uninterruptible reflow \n" + JSON.stringify(pathWithLineNumbers, null, "\t") + "\n"); } }, reflowInterruptible(start, end) { // We're not interested in interruptible reflows, but might as well take the // opportuntiy to dirty the root frame. dirtyFrameFn(); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver, Ci.nsISupportsWeakReference]) }; let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell); docShell.addWeakReflowObserver(observer); els.addListenerForAllEvents(win, dirtyFrameFn, true); try { dirtyFrameFn(); await testFn(); } finally { for (let remainder of expectedStacks) { Assert.ok(false, `Unused expected reflow: ${JSON.stringify(remainder, null, "\t")}.\n` + "This is probably a good thing - just remove it from the " + "expected list."); } els.removeListenerForAllEvents(win, dirtyFrameFn, true); docShell.removeWeakReflowObserver(observer); } } async function ensureNoPreloadedBrowser() { // If we've got a preloaded browser, get rid of it so that it // doesn't interfere with the test if it's loading. We have to // do this before we disable preloading or changing the new tab // URL, otherwise _getPreloadedBrowser will return null, despite // the preloaded browser existing. let preloaded = gBrowser._getPreloadedBrowser(); if (preloaded) { preloaded.remove(); } await SpecialPowers.pushPrefEnv({ set: [["browser.newtab.preload", false]], }); let aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"] .getService(Ci.nsIAboutNewTabService); aboutNewTabService.newTabURL = "about:blank"; registerCleanupFunction(() => { aboutNewTabService.resetNewTabURL(); }); } /** * Calculate and return how many additional tabs can be fit into the * tabstrip without causing it to overflow. * * @return int * The maximum additional tabs that can be fit into the * tabstrip without causing it to overflow. */ function computeMaxTabCount() { let currentTabCount = gBrowser.tabs.length; let newTabButton = document.getAnonymousElementByAttribute(gBrowser.tabContainer, "class", "tabs-newtab-button"); let newTabRect = newTabButton.getBoundingClientRect(); let tabStripRect = gBrowser.tabContainer.mTabstrip.getBoundingClientRect(); let availableTabStripWidth = tabStripRect.width - newTabRect.width; let tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth, 10); let maxTabCount = Math.floor(availableTabStripWidth / tabMinWidth) - currentTabCount; Assert.ok(maxTabCount > 0, "Tabstrip needs to be wide enough to accomodate at least 1 more tab " + "without overflowing."); return maxTabCount; } /** * Helper function that opens up some number of about:blank tabs, and wait * until they're all fully open. * * @param howMany (int) * How many about:blank tabs to open. */ async function createTabs(howMany) { let uris = []; while (howMany--) { uris.push("about:blank"); } gBrowser.loadTabs(uris, true, false); await BrowserTestUtils.waitForCondition(() => { return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen); }); } /** * Removes all of the tabs except the originally selected * tab, and waits until all of the DOM nodes have been * completely removed from the tab strip. */ async function removeAllButFirstTab() { await SpecialPowers.pushPrefEnv({ set: [["browser.tabs.warnOnCloseOtherTabs", false]], }); gBrowser.removeAllTabsBut(gBrowser.tabs[0]); await BrowserTestUtils.waitForCondition(() => gBrowser.tabs.length == 1); await SpecialPowers.popPrefEnv(); }