/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // A bunch of assumptions we make about the behavior of the parent process, // and which we use as sanity checks. If Firefox evolves, we will need to // update these values. // Note that Test Verify can really stress the cpu durations. const HARDCODED_ASSUMPTIONS_PROCESS = { minimalNumberOfThreads: 6, maximalNumberOfThreads: 1000, minimalCPUPercentage: 0, maximalCPUPercentage: 1000, minimalCPUTotalDurationMS: 10, maximalCPUTotalDurationMS: 10000000, minimalRAMBytesUsage: 1024 * 1024 /* 1 Megabyte */, maximalRAMBytesUsage: 1024 * 1024 * 1024 * 1024 * 1 /* 1 Tb */, }; const HARDCODED_ASSUMPTIONS_THREAD = { minimalCPUPercentage: 0, maximalCPUPercentage: 100, minimalCPUTotalDurationMS: 0, maximalCPUTotalDurationMS: 10000000, }; // How close we accept our rounding up/down. const APPROX_FACTOR = 1.51; const MS_PER_NS = 1000000; // Wait for `about:processes` to be updated. async function promiseAboutProcessesUpdated({ doc, force, tabAboutProcesses }) { let startTime = performance.now(); let updatePromise = new Promise(resolve => { doc.addEventListener("AboutProcessesUpdated", resolve, { once: true }); }); if (force) { await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => { info("Forcing about:processes refresh"); await content.Control.update(/* force = */ true); }); } await updatePromise; // Fluent will update the visible table content during the next // refresh driver tick, wait for it. // requestAnimationFrame calls us at the begining of the tick, we use // dispatchToMainThread to execute our code after the end of it. //XXX: Replace with proper wait for l10n completion once bug 1520659 is fixed. await new Promise(doc.defaultView.requestAnimationFrame); await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); ChromeUtils.addProfilerMarker( "promiseAboutProcessesUpdated", { startTime, category: "Test" }, force ? "force" : undefined ); } function promiseProcessDied({ childID }) { return new Promise(resolve => { let observer = properties => { properties.QueryInterface(Ci.nsIPropertyBag2); let subjectChildID = properties.get("childID"); if (subjectChildID == childID) { Services.obs.removeObserver(observer, "ipc:content-shutdown"); resolve(); } }; Services.obs.addObserver(observer, "ipc:content-shutdown"); }); } function isCloseEnough(value, expected) { if (value < 0 || expected < 0) { throw new Error(`Invalid isCloseEnough(${value}, ${expected})`); } if (Math.round(value) == Math.round(expected)) { return true; } if (expected == 0) { return false; } let ratio = value / expected; if (ratio <= APPROX_FACTOR && ratio >= 1 / APPROX_FACTOR) { return true; } return false; } function getMemoryMultiplier(unit, sign = "+") { let multiplier; switch (sign) { case "+": multiplier = 1; break; case "-": multiplier = -1; break; default: throw new Error("Invalid sign: " + sign); } switch (unit) { case "B": break; case "KB": multiplier *= 1024; break; case "MB": multiplier *= 1024 * 1024; break; case "GB": multiplier *= 1024 * 1024 * 1024; break; case "TB": multiplier *= 1024 * 1024 * 1024 * 1024; break; default: throw new Error("Invalid memory unit: " + unit); } return multiplier; } function getTimeMultiplier(unit) { switch (unit) { case "ns": return 1 / (1000 * 1000); case "µs": return 1 / 1000; case "ms": return 1; case "s": return 1000; case "m": return 60000; } throw new Error("Invalid time unit: " + unit); } async function testCpu(element, total, slope, assumptions) { info( `Testing CPU display ${element.textContent} - ${element.title} vs total ${total}, slope ${slope}` ); let barWidth = getComputedStyle(element).getPropertyValue("--bar-width"); if (slope) { Assert.greater( Number.parseFloat(barWidth), 0, "The bar width should be > 0 when there is some CPU use" ); } else { Assert.equal(barWidth, "-0.5", "There should be no CPU bar displayed"); } if (element.textContent == "(measuring)") { info("Still measuring"); return; } const CPU_TEXT_CONTENT_REGEXP = /\~0%|idle|[0-9.,]+%|[?]/; let extractedPercentage = CPU_TEXT_CONTENT_REGEXP.exec( element.textContent )[0]; switch (extractedPercentage) { case "idle": Assert.equal(slope, 0, "Idle means exactly 0%"); // Nothing else to do here. return; case "~0%": Assert.ok(slope > 0 && slope < 0.0001); break; case "?": Assert.ok(slope == null); // Nothing else to do here. return; default: { // `Number.parseFloat("99%")` returns `99`. let computedPercentage = Number.parseFloat(extractedPercentage); Assert.ok( isCloseEnough(computedPercentage, slope * 100), `The displayed approximation of the slope is reasonable: ${computedPercentage} vs ${ slope * 100 }` ); // Also, sanity checks. Assert.ok( computedPercentage / 100 >= assumptions.minimalCPUPercentage, `Not too little: ${computedPercentage / 100} >=? ${ assumptions.minimalCPUPercentage } ` ); Assert.ok( computedPercentage / 100 <= assumptions.maximalCPUPercentage, `Not too much: ${computedPercentage / 100} <=? ${ assumptions.maximalCPUPercentage } ` ); break; } } const CPU_TOOLTIP_REGEXP = /(?:.*: ([0-9.,]+) ?(ns|µs|ms|s|m|h|d))/; // Example: "Total CPU time: 4,470ms" let [, extractedTotal, extractedUnit] = CPU_TOOLTIP_REGEXP.exec( element.title ); let totalMS = total / MS_PER_NS; let computedTotal = // We produce localized numbers, with "," as a thousands separator in en-US builds, // but `parseFloat` doesn't understand the ",", so we need to remove it // before parsing. Number.parseFloat(extractedTotal.replace(/,/g, "")) * getTimeMultiplier(extractedUnit); Assert.ok( isCloseEnough(computedTotal, totalMS), `The displayed approximation of the total duration is reasonable: ${computedTotal} vs ${totalMS}` ); Assert.ok( totalMS <= assumptions.maximalCPUTotalDurationMS && totalMS >= assumptions.minimalCPUTotalDurationMS, `The total number of MS is reasonable ${totalMS}: [${assumptions.minimalCPUTotalDurationMS}, ${assumptions.maximalCPUTotalDurationMS}]` ); } async function testMemory(element, total, delta, assumptions) { info( `Testing memory display ${element.textContent} - ${element.title} vs total ${total}, delta ${delta}` ); const MEMORY_TEXT_CONTENT_REGEXP = /([0-9.,]+)(TB|GB|MB|KB|B)/; // Example: "383.55MB" let extracted = MEMORY_TEXT_CONTENT_REGEXP.exec(element.textContent); Assert.notEqual( extracted, null, `Can we parse ${element.textContent} with ${MEMORY_TEXT_CONTENT_REGEXP}?` ); let [, extractedTotal, extractedUnit] = extracted; let extractedTotalNumber = Number.parseFloat(extractedTotal); Assert.ok( extractedTotalNumber > 0, `Unitless total memory use is greater than 0: ${extractedTotal}` ); if (extractedUnit != "GB") { Assert.ok( extractedTotalNumber <= 1024, `Unitless total memory use is less than 1024: ${extractedTotal}` ); } // Now check that the conversion was meaningful. let computedTotal = getMemoryMultiplier(extractedUnit) * extractedTotalNumber; Assert.ok( isCloseEnough(computedTotal, total), `The displayed approximation of the total amount of memory is reasonable: ${computedTotal} vs ${total}` ); if (!AppConstants.ASAN) { // ASAN plays tricks with RAM (e.g. allocates the entirety of virtual memory), // which makes this test unrealistic. Assert.ok( assumptions.minimalRAMBytesUsage <= computedTotal && computedTotal <= assumptions.maximalRAMBytesUsage, `The total amount amount of memory is reasonable: ${computedTotal} in [${assumptions.minimalRAMBytesUsage}, ${assumptions.maximalRAMBytesUsage}]` ); } const MEMORY_TOOLTIP_REGEXP = /(?:.*: ([-+]?)([0-9.,]+)(GB|MB|KB|B))?/; // Example: "Evolution: -12.5MB" extracted = MEMORY_TOOLTIP_REGEXP.exec(element.title); Assert.notEqual( extracted, null, `Can we parse ${element.title} with ${MEMORY_TOOLTIP_REGEXP}?` ); let [, extractedDeltaSign, extractedDeltaTotal, extractedDeltaUnit] = extracted; if (extractedDeltaSign == null) { Assert.equal(delta || 0, 0); return; } let deltaTotalNumber = Number.parseFloat( // Remove the thousands separator that breaks parseFloat. extractedDeltaTotal.replace(/,/g, "") ); // Note: displaying 1024KB can happen if the value is slightly less than // 1024*1024B but rounded to 1024KB. Assert.ok( deltaTotalNumber > 0 && deltaTotalNumber <= 1024, `Unitless delta memory use is in (0, 1024): ${extractedDeltaTotal}` ); Assert.ok( ["B", "KB", "MB"].includes(extractedDeltaUnit), `Delta unit is reasonable: ${extractedDeltaUnit}` ); // Now check that the conversion was meaningful. // Let's just check that the number displayed is within 10% of `delta`. let computedDelta = getMemoryMultiplier(extractedDeltaUnit, extractedDeltaSign) * deltaTotalNumber; Assert.equal( computedDelta >= 0, delta >= 0, `Delta has the right sign: ${computedDelta} vs ${delta}` ); } function extractProcessDetails(row) { let children = row.children; let name = children[0]; let memory = children[1]; let cpu = children[2]; if (Services.prefs.getBoolPref("toolkit.aboutProcesses.showProfilerIcons")) { name = name.firstChild; Assert.ok( name.nextSibling.classList.contains("profiler-icon"), "The profiler icon should be shown" ); } let fluentArgs = row.ownerDocument.l10n.getAttributes(name).args; let threadDetailsRow = row.nextSibling; while (threadDetailsRow) { if (threadDetailsRow.classList.contains("process")) { threadDetailsRow = null; break; } if (threadDetailsRow.classList.contains("thread-summary")) { break; } threadDetailsRow = threadDetailsRow.nextSibling; } return { memory, cpu, pidContent: fluentArgs.pid, threads: threadDetailsRow, }; } function findTabRowByName(doc, name) { for (let row of doc.getElementsByClassName("name")) { if (!row.parentNode.classList.contains("window")) { continue; } let foundName = document.l10n.getAttributes(row).args.name; if (foundName != name) { continue; } return row.parentNode; } return null; } function findProcessRowByOrigin(doc, origin) { for (let row of doc.getElementsByClassName("process")) { if (row.process.origin == origin) { return row; } } return null; } async function setupTabWithOriginAndTitle(origin, title) { let tab = BrowserTestUtils.addTab(gBrowser, origin, { skipAnimation: true }); tab.testTitle = title; tab.testOrigin = origin; await BrowserTestUtils.browserLoaded(tab.linkedBrowser); await SpecialPowers.spawn(tab.linkedBrowser, [title], async title => { content.document.title = title; }); return tab; } async function setupAudioTab() { let origin = "about:blank"; let title = "utility audio"; let tab = BrowserTestUtils.addTab(gBrowser, origin, { skipAnimation: true }); tab.testTitle = title; tab.testOrigin = origin; await BrowserTestUtils.browserLoaded(tab.linkedBrowser); await SpecialPowers.spawn(tab.linkedBrowser, [title], async title => { content.document.title = title; const ROOT = "https://example.com/browser/toolkit/components/aboutprocesses/tests/browser"; let audio = content.document.createElement("audio"); audio.setAttribute("controls", "true"); audio.setAttribute("loop", true); audio.src = `${ROOT}/small-shot.mp3`; content.document.body.appendChild(audio); await audio.play(); }); return tab; } async function testAboutProcessesWithConfig({ showAllFrames, showThreads }) { const isFission = gFissionBrowser; await SpecialPowers.pushPrefEnv({ set: [ ["toolkit.aboutProcesses.showAllSubframes", showAllFrames], ["toolkit.aboutProcesses.showThreads", showThreads], // Force same-origin tabs to share a single process, to properly test // functionality involving multiple tabs within a single process with Fission. ["dom.ipc.processCount.webIsolated", 1], // Ensure utility audio decoder is enabled ["media.utility-process.enabled", true], ], }); // Install a test extension to also cover processes and sub-frames related to the // extension process. const extension = ExtensionTestUtils.loadExtension({ manifest: { browser_specific_settings: { gecko: { id: "test-aboutprocesses@mochi.test" }, }, }, background() { // Creates an about:blank iframe in the extension process to make sure that // Bug 1665099 doesn't regress. document.body.appendChild(document.createElement("iframe")); this.browser.test.sendMessage("bg-page-loaded"); }, }); await extension.startup(); await extension.awaitMessage("bg-page-loaded"); // Setup tabs asynchronously. // The about:processes tab. info("Setting up about:processes"); let promiseTabAboutProcesses = BrowserTestUtils.openNewForegroundTab({ gBrowser, opening: "about:processes", waitForLoad: true, }); info("Setting up example.com"); // Another tab that we'll pretend is hung. let promiseTabHung = (async function () { let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com", { skipAnimation: true, }); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); let p = BrowserTestUtils.browserLoaded( tab.linkedBrowser, true /* includeSubFrames */ ); await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { // Open an in-process iframe to test toolkit.aboutProcesses.showAllSubframes let frame = content.document.createElement("iframe"); content.document.body.appendChild(frame); }); await p; return tab; })(); let promiseAudioPlayback = setupAudioTab(); let promiseUserContextTab = (async function () { let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com", { userContextId: 1, skipAnimation: true, }); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); await SpecialPowers.spawn(tab.linkedBrowser, [], () => { content.document.title = "Tab with User Context"; }); return tab; })(); info("Setting up tabs we intend to close"); // The two following tabs share the same domain. // We use them to check that closing one doesn't close the other. let promiseTabCloseSeparately1 = setupTabWithOriginAndTitle( "http://example.org", "Close me 1 (separately)" ); let promiseTabCloseSeparately2 = setupTabWithOriginAndTitle( "http://example.org", "Close me 2 (separately)" ); // The two following tabs share the same domain. // We use them to check that closing the process kills them both. let promiseTabCloseProcess1 = setupTabWithOriginAndTitle( "http://example.net", "Close me 1 (process)" ); let promiseTabCloseProcess2 = setupTabWithOriginAndTitle( "http://example.net", "Close me 2 (process)" ); // The two following tabs share the same domain. // We use them to check that closing the process kills them both. let promiseTabCloseTogether1 = setupTabWithOriginAndTitle( "https://example.org", "Close me 1 (together)" ); let promiseTabCloseTogether2 = setupTabWithOriginAndTitle( "https://example.org", "Close me 2 (together)" ); // Wait for initialization to finish. let tabAboutProcesses = await promiseTabAboutProcesses; let tabHung = await promiseTabHung; let audioPlayback = await promiseAudioPlayback; let tabUserContext = await promiseUserContextTab; let tabCloseSeparately1 = await promiseTabCloseSeparately1; let tabCloseSeparately2 = await promiseTabCloseSeparately2; let tabCloseProcess1 = await promiseTabCloseProcess1; let tabCloseProcess2 = await promiseTabCloseProcess2; let tabCloseTogether1 = await promiseTabCloseTogether1; let tabCloseTogether2 = await promiseTabCloseTogether2; let doc = tabAboutProcesses.linkedBrowser.contentDocument; let tbody = doc.getElementById("process-tbody"); Assert.ok(!!tbody, "Found the #process-tbody element"); if (isFission) { // We're going to kill this process later, so tell it to add an // annotation so the leak checker knows it is okay there is no // leak log. await SpecialPowers.spawn(tabCloseProcess1.linkedBrowser, [], () => { ChromeUtils.privateNoteIntentionalCrash(); }); } info("Setting up fake process hang detector"); let hungChildID = tabHung.linkedBrowser.frameLoader.childID; // Keep informing about:processes that `tabHung` is hung. // Note: this is a background task, do not `await` it. let fakeProcessHangMonitor = async function () { for (let i = 0; i < 100; ++i) { if (!tabHung.linkedBrowser) { // Let's stop spamming as soon as we can. return; } Services.obs.notifyObservers( { childID: hungChildID, scriptBrowser: tabHung.linkedBrowser, scriptFileName: "chrome://browser/content/browser.js", QueryInterface: ChromeUtils.generateQI(["nsIHangReport"]), }, "process-hang-report" ); // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(resolve => setTimeout(resolve, 300)); } }; fakeProcessHangMonitor(); // about:processes will take a little time to appear and be populated. await promiseAboutProcessesUpdated({ doc, tabAboutProcesses }); Assert.ok(tbody.childElementCount, "The table should be populated"); Assert.ok( !!tbody.getElementsByClassName("hung").length, "The hung process should appear" ); info("Looking at the contents of about:processes"); let processesToBeFound = [ // The browser process. { name: "browser", predicate: row => row.process.type == "browser", }, // The hung process. { name: "hung", predicate: row => row.classList.contains("hung") && row.classList.contains("process") && ["web", "webIsolated"].includes(row.process.type), }, // Any non-hung process { name: "non-hung", predicate: row => !row.classList.contains("hung") && row.classList.contains("process") && ["web", "webIsolated"].includes(row.process.type), }, // A utility process with at least one actor. { name: "utility", predicate: row => row.process && row.process.type == "utility" && row.classList.contains("process") && row.nextSibling && row.nextSibling.classList.contains("actor"), }, ]; for (let finder of processesToBeFound) { info(`Running sanity tests on ${finder.name}`); let row = tbody.firstChild; while (row) { if (finder.predicate(row)) { break; } row = row.nextSibling; } Assert.ok(!!row, `found a table row for ${finder.name}`); let { memory, cpu, pidContent, threads } = extractProcessDetails(row); info("Sanity checks: pid"); let pid = Number.parseInt(pidContent); Assert.ok(pid > 0, `Checking pid ${pidContent}`); Assert.equal(pid, row.process.pid); info("Sanity checks: memory resident"); await testMemory( memory, row.process.totalRamSize, row.process.deltaRamSize, HARDCODED_ASSUMPTIONS_PROCESS ); info("Sanity checks: CPU (Total)"); await testCpu( cpu, row.process.totalCpu, row.process.slopeCpu, HARDCODED_ASSUMPTIONS_PROCESS ); // Testing threads. if (!showThreads) { info("In this mode, we shouldn't display any threads"); Assert.equal( threads, null, "In hidden threads mode, we shouldn't have any thread summary" ); } else { Assert.ok(threads, "We have a thread summary row"); let { number, active = 0, list, } = doc.l10n.getAttributes(threads.children[0].children[1]).args; info("Sanity checks: number of threads"); Assert.greaterOrEqual( number, HARDCODED_ASSUMPTIONS_PROCESS.minimalNumberOfThreads ); Assert.lessOrEqual( number, HARDCODED_ASSUMPTIONS_PROCESS.maximalNumberOfThreads ); Assert.equal( number, row.process.threads.length, "The number we display should be the number of threads" ); info("Sanity checks: number of active threads"); Assert.greaterOrEqual( active, 0, "The number of active threads should never be negative" ); Assert.lessOrEqual( active, number, "The number of active threads should not exceed the total number of threads" ); let activeThreads = row.process.threads.filter(t => t.active); Assert.equal( active, activeThreads.length, "The displayed number of active threads should be correct" ); let activeSet = new Set(); for (let t of activeThreads) { activeSet.add(t.name.replace(/ ?#[0-9]+$/, "")); } info("Sanity checks: thread list"); Assert.equal( list ? list.split(", ").length : 0, activeSet.size, "The thread summary list of active threads should have the expected length" ); info("Testing that we can open the list of threads"); let twisty = threads.getElementsByClassName("twisty")[0]; twisty.click(); // Fluent will update the text content of new rows during the // next refresh driver tick, wait for it. // requestAnimationFrame calls us at the begining of the tick, we use // dispatchToMainThread to execute our code after the end of it. //XXX: Replace with proper wait for l10n completion once bug 1520659 is fixed. await new Promise(doc.defaultView.requestAnimationFrame); await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); let numberOfThreadsFound = 0; for ( let threadRow = threads.nextSibling; threadRow && threadRow.classList.contains("thread"); threadRow = threadRow.nextSibling ) { numberOfThreadsFound++; } Assert.equal( numberOfThreadsFound, number, `We should see ${number} threads, found ${numberOfThreadsFound}` ); let threadIds = []; for ( let threadRow = threads.nextSibling; threadRow && threadRow.classList.contains("thread"); threadRow = threadRow.nextSibling ) { Assert.ok( threadRow.children.length >= 3 && threadRow.children[1].textContent, "The thread row should be populated" ); let children = threadRow.children; let cpu = children[1]; let l10nArgs = doc.l10n.getAttributes(children[0]).args; // Sanity checks: name Assert.ok(threadRow.thread.name, "Thread name is not empty"); Assert.equal( l10nArgs.name, threadRow.thread.name, "Displayed thread name is correct" ); // Sanity checks: tid let tidContent = l10nArgs.tid; let tid = Number.parseInt(tidContent); threadIds.push(tid); Assert.notEqual(tid, 0, "The tid should be set"); Assert.equal(tid, threadRow.thread.tid, "Displayed tid is correct"); // Sanity checks: CPU (per thread) await testCpu( cpu, threadRow.thread.totalCpu, threadRow.thread.slopeCpu, HARDCODED_ASSUMPTIONS_THREAD ); } // By default, threads are sorted by tid. let threadList = threadIds.join(","); Assert.equal( threadList, threadIds.sort((a, b) => a - b).join(","), "The thread rows are in the default sort order." ); } } await promiseAboutProcessesUpdated({ doc, force: true, tabAboutProcesses, }); // Testing subframes. info("Testing subframes"); let foundAtLeastOneInProcessSubframe = false; for (let row of doc.getElementsByClassName("window")) { let subframe = row.win; if (subframe.tab) { continue; } let url = doc.l10n.getAttributes(row.children[0]).args.url; Assert.equal(url, subframe.documentURI.spec); if (!subframe.isProcessRoot) { foundAtLeastOneInProcessSubframe = true; } } if (showAllFrames) { Assert.ok( foundAtLeastOneInProcessSubframe, "Found at least one about:blank in-process subframe" ); } else { Assert.ok( !foundAtLeastOneInProcessSubframe, "We shouldn't have any about:blank in-process subframe" ); } info("Double-clicking on a tab"); let whenTabSwitchedToWeb = BrowserTestUtils.switchTab(gBrowser, () => { // We pass a function to use `BrowserTestUtils.switchTab` not in its // role as a tab switcher but rather in its role as a function that // waits until something else has switched the tab. // We'll actually cause tab switching below, by doucle-clicking // in `about:processes`. }); await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => { // Locate and double-click on the representation of `tabHung`. let tbody = content.document.getElementById("process-tbody"); for (let row of tbody.getElementsByClassName("tab")) { if (row.parentNode.win.documentURI.spec != "http://example.com/") { continue; } // Simulate double-click. let evt = new content.window.MouseEvent("dblclick", { bubbles: true, cancelable: true, view: content.window, }); row.dispatchEvent(evt); return; } Assert.ok(false, "We should have found the hung tab"); }); info("Waiting for tab switch"); await whenTabSwitchedToWeb; Assert.equal( gBrowser.selectedTab.linkedBrowser.currentURI.spec, tabHung.linkedBrowser.currentURI.spec, "We should have focused the hung tab" ); await BrowserTestUtils.switchTab(gBrowser, tabAboutProcesses); info("Double-clicking on the extensions process"); let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons"); await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => { let extensionsRow = content.document.getElementsByClassName("extensions")[0]; Assert.ok(!!extensionsRow, "We should have found the extensions process"); let evt = new content.window.MouseEvent("dblclick", { bubbles: true, cancelable: true, view: content.window, }); extensionsRow.dispatchEvent(evt); }); info("Waiting for about:addons to open"); await tabPromise; Assert.equal( gBrowser.selectedTab.linkedBrowser.currentURI.spec, "about:addons", "We should now see the addon tab" ); BrowserTestUtils.removeTab(gBrowser.selectedTab); info("Testing tab closing"); // A list of processes we have killed and for which we're waiting // death confirmation. Only used in Fission. let waitForProcessesToDisappear = []; await promiseAboutProcessesUpdated({ doc, force: true, tabAboutProcesses, }); if (isFission) { // Before closing, all our origins should be present for (let origin of [ "http://example.com", // tabHung "http://example.net", // tabCloseProcess* "http://example.org", // tabCloseSeparately* "https://example.org", // tabCloseTogether* ]) { Assert.ok( findProcessRowByOrigin(doc, origin), `There is a process for origin ${origin}` ); } // Verify that the user context id has been correctly displayed. let userContextProcessRow = findProcessRowByOrigin( doc, "http://example.com^userContextId=1" ); Assert.ok( userContextProcessRow, "There is a separate process for the tab with a different user context" ); let name = userContextProcessRow.firstChild; if ( Services.prefs.getBoolPref("toolkit.aboutProcesses.showProfilerIcons") ) { name = name.firstChild; Assert.ok( name.nextSibling.classList.contains("profiler-icon"), "The profiler icon should be shown" ); } Assert.equal( doc.l10n.getAttributes(name).args.origin, "http://example.com — " + ContextualIdentityService.getUserContextLabel(1), "The user context ID should be replaced with the localized container name" ); // These origins will disappear. for (let origin of [ "http://example.net", // tabCloseProcess* "https://example.org", // tabCloseTogether* ]) { let row = findProcessRowByOrigin(doc, origin); let childID = row.process.childID; waitForProcessesToDisappear.push(promiseProcessDied({ childID })); } } // Close a few tabs. for (let tab of [tabCloseSeparately1, tabCloseTogether1, tabCloseTogether2]) { info("Closing a tab through about:processes"); let found = findTabRowByName(doc, tab.linkedBrowser.contentTitle); Assert.ok( found, `We should have found tab ${tab.linkedBrowser.contentTitle} to close it` ); let closeIcons = found.getElementsByClassName("close-icon"); Assert.equal( closeIcons.length, 1, "This tab should have exactly one close icon" ); closeIcons[0].click(); Assert.ok( found.classList.contains("killing"), "We should have marked the row as dying" ); } //...and a process, if we're in Fission. if (isFission) { info("Closing an entire process through about:processes"); let found = findProcessRowByOrigin(doc, "http://example.net"); let closeIcons = found.getElementsByClassName("close-icon"); Assert.equal( closeIcons.length, 1, "This process should have exactly one close icon" ); closeIcons[0].click(); Assert.ok( found.classList.contains("killing"), "We should have marked the row as dying" ); } // Give Firefox a little time to close the tabs and update about:processes. // This might take two updates as we're racing between collecting data and // processes actually being killed. await promiseAboutProcessesUpdated({ doc, force: true, tabAboutProcesses, }); // The tabs we have closed directly or indirectly should now be (closed or crashed) and invisible in about:processes. for (let { origin, tab } of [ { origin: "http://example.org", tab: tabCloseSeparately1 }, { origin: "https://example.org", tab: tabCloseTogether1 }, { origin: "https://example.org", tab: tabCloseTogether2 }, ...(isFission ? [ { origin: "http://example.net", tab: tabCloseProcess1 }, { origin: "http://example.net", tab: tabCloseProcess2 }, ] : []), ]) { // Tab shouldn't show up anymore in about:processes Assert.ok( !findTabRowByName(doc, origin), `Tab for ${origin} shouldn't show up anymore in about:processes` ); // ...and should be unloaded. Assert.ok( !tab.getAttribute("linkedPanel"), `The tab should now be unloaded (${tab.testOrigin} - ${tab.testTitle})` ); } // On the other hand, tabs we haven't closed should still be open and visible in about:processes. Assert.ok( tabCloseSeparately2.linkedBrowser, "Killing one tab in the domain should not have closed the other tab" ); let foundtabCloseSeparately2 = findTabRowByName( doc, tabCloseSeparately2.linkedBrowser.contentTitle ); Assert.ok( foundtabCloseSeparately2, "The second tab is still visible in about:processes" ); if (isFission) { // After closing, we must have closed some of our origins. for (let origin of [ "http://example.com", // tabHung "http://example.org", // tabCloseSeparately* ]) { Assert.ok( findProcessRowByOrigin(doc, origin), `There should still be a process row for origin ${origin}` ); } info("Waiting for processes to die"); await Promise.all(waitForProcessesToDisappear); info("Waiting for about:processes to be updated"); await promiseAboutProcessesUpdated({ doc, force: true, tabAboutProcesses, }); for (let origin of [ "http://example.net", // tabCloseProcess* "https://example.org", // tabCloseTogether* ]) { Assert.ok( !findProcessRowByOrigin(doc, origin), `Process ${origin} should disappear from about:processes` ); } } info("Additional sanity check for all processes"); for (let row of doc.getElementsByClassName("process")) { let { pidContent } = extractProcessDetails(row); Assert.equal(Number.parseInt(pidContent), row.process.pid); } BrowserTestUtils.removeTab(tabAboutProcesses); BrowserTestUtils.removeTab(tabHung); BrowserTestUtils.removeTab(tabUserContext); BrowserTestUtils.removeTab(tabCloseSeparately2); // We still need to remove these tabs. // We killed the process, but we don't want to leave zombie tabs lying around. BrowserTestUtils.removeTab(tabCloseProcess1); BrowserTestUtils.removeTab(tabCloseProcess2); BrowserTestUtils.removeTab(audioPlayback); await SpecialPowers.popPrefEnv(); await extension.unload(); }