summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutprocesses/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/aboutprocesses/tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser.toml32
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js7
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_selection.js124
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_shortcut.js16
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_all_frames.js6
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_frames_without_threads.js6
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_threads.js7
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_twisty.js134
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_utility_actors.js44
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/head.js1066
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/small-shot.mp3bin0 -> 6825 bytes
11 files changed, 1442 insertions, 0 deletions
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser.toml b/toolkit/components/aboutprocesses/tests/browser/browser.toml
new file mode 100644
index 0000000000..07bec9de5e
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser.toml
@@ -0,0 +1,32 @@
+[DEFAULT]
+head = "head.js"
+skip-if = [
+ "asan",
+ "tsan"
+] # With sanitizers, we regularly hit internal timeouts.
+support-files = ["small-shot.mp3"]
+
+["browser_aboutprocesses_default_options.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 td.action-icon, td.close-icon may not be focusable
+https_first_disabled = true
+
+["browser_aboutprocesses_selection.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 tr.process, tr.window, tr.thread, tr.thread-summary may not be focusable
+
+["browser_aboutprocesses_shortcut.js"]
+
+["browser_aboutprocesses_show_all_frames.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 td.action-icon, td.close-icon may not be focusable
+https_first_disabled = true
+
+["browser_aboutprocesses_show_frames_without_threads.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 td.action-icon, td.close-icon may not be focusable
+https_first_disabled = true
+
+["browser_aboutprocesses_show_threads.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 td.action-icon, td.close-icon may not be focusable
+https_first_disabled = true
+
+["browser_aboutprocesses_twisty.js"]
+
+["browser_aboutprocesses_utility_actors.js"]
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js
new file mode 100644
index 0000000000..15ed1c4501
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js
@@ -0,0 +1,7 @@
+// Test about:processes with default options.
+add_task(async function testDefaultOptions() {
+ return testAboutProcessesWithConfig({
+ showAllFrames: false,
+ showThreads: false,
+ });
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_selection.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_selection.js
new file mode 100644
index 0000000000..f208fb095d
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_selection.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var doc, tbody, tabAboutProcesses;
+
+const rowTypes = ["process", "window", "thread-summary", "thread"];
+
+function promiseUpdate() {
+ return promiseAboutProcessesUpdated({
+ doc,
+ tbody,
+ force: true,
+ tabAboutProcesses,
+ });
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("toolkit.aboutProcesses.showThreads", true);
+
+ info("Setting up about:processes");
+ tabAboutProcesses = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:processes",
+ waitForLoad: true,
+ });
+
+ doc = tabAboutProcesses.linkedBrowser.contentDocument;
+ tbody = doc.getElementById("process-tbody");
+ await promiseUpdate();
+
+ info("Open a list of threads to have thread rows displayed");
+ let twisty = doc.querySelector("tr.thread-summary .twisty");
+ twisty.click();
+ await promiseUpdate();
+});
+
+add_task(async function testSelectionPersistedAfterUpdate() {
+ for (let rowType of rowTypes) {
+ let row = doc.querySelector(`tr.${rowType}`);
+ Assert.ok(!!row, `Found ${rowType} row`);
+ Assert.ok(!row.hasAttribute("selected"), "The row should not be selected");
+
+ info("Click in the row to select it.");
+ row.click();
+ Assert.equal(
+ row.getAttribute("selected"),
+ "true",
+ "The row should be selected"
+ );
+ Assert.equal(
+ doc.querySelectorAll("[selected]").length,
+ 1,
+ "There should be only one selected row"
+ );
+
+ info("Wait for an update and ensure the selected row is still the same");
+ let rowId = row.rowId;
+ let findRowsWithId = rowId =>
+ [...doc.querySelectorAll("tr")].filter(r => r.rowId == rowId);
+ Assert.equal(
+ findRowsWithId(rowId).length,
+ 1,
+ "There should be only one row with id " + rowId
+ );
+ await promiseUpdate();
+ let selectedRow = doc.querySelector("[selected]");
+ if (rowType == "thread" && !selectedRow) {
+ info("The thread row might have disappeared if the thread has ended");
+ Assert.equal(
+ findRowsWithId(rowId).length,
+ 0,
+ "There should no longer be a row with id " + rowId
+ );
+ continue;
+ }
+ Assert.ok(
+ !!selectedRow,
+ "There should still be a selected row after an update"
+ );
+ Assert.equal(
+ selectedRow.rowId,
+ rowId,
+ "The selected row should have the same id as the row we clicked"
+ );
+ }
+});
+
+add_task(function testClickAgainToRemoveSelection() {
+ for (let rowType of rowTypes) {
+ let row = doc.querySelector(`tr.${rowType}`);
+ Assert.ok(!!row, `Found ${rowType} row`);
+ Assert.ok(!row.hasAttribute("selected"), "The row should not be selected");
+ info("Click in the row to select it.");
+ row.click();
+ Assert.equal(
+ row.getAttribute("selected"),
+ "true",
+ "The row should now be selected"
+ );
+ Assert.equal(
+ doc.querySelectorAll("[selected]").length,
+ 1,
+ "There should be only one selected row"
+ );
+
+ info("Click the row again to remove the selection.");
+ row.click();
+ Assert.ok(
+ !row.hasAttribute("selected"),
+ "The row should no longer be selected"
+ );
+ Assert.ok(
+ !doc.querySelector("[selected]"),
+ "There should be no selected row"
+ );
+ }
+});
+
+add_task(function cleanup() {
+ BrowserTestUtils.removeTab(tabAboutProcesses);
+ Services.prefs.clearUserPref("toolkit.aboutProcesses.showThreads");
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_shortcut.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_shortcut.js
new file mode 100644
index 0000000000..61b8be1fcd
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_shortcut.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ // Wait for the browser to be ready to receive keyboard events.
+ if (!gBrowser.selectedBrowser.hasLayers) {
+ await BrowserTestUtils.waitForEvent(window, "MozLayerTreeReady");
+ }
+
+ EventUtils.synthesizeKey("KEY_Escape", { shiftKey: true });
+
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ Assert.equal(gBrowser.selectedBrowser.currentURI.spec, "about:processes");
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_all_frames.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_all_frames.js
new file mode 100644
index 0000000000..2c437f9dc4
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_all_frames.js
@@ -0,0 +1,6 @@
+add_task(async function testShowFramesAndThreads() {
+ await testAboutProcessesWithConfig({
+ showAllFrames: true,
+ showThreads: true,
+ });
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_frames_without_threads.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_frames_without_threads.js
new file mode 100644
index 0000000000..178ceec9ed
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_frames_without_threads.js
@@ -0,0 +1,6 @@
+add_task(async function testShowFramesWithoutThreads() {
+ await testAboutProcessesWithConfig({
+ showAllFrames: true,
+ showThreads: false,
+ });
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_threads.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_threads.js
new file mode 100644
index 0000000000..10e630ab31
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_threads.js
@@ -0,0 +1,7 @@
+// Test about:processes with showThreads: true, showAllFrames: false.
+add_task(async function testShowThreads() {
+ return testAboutProcessesWithConfig({
+ showAllFrames: false,
+ showThreads: true,
+ });
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_twisty.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_twisty.js
new file mode 100644
index 0000000000..77fb0fbfbb
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_twisty.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let doc, tbody, tabAboutProcesses;
+
+const rowTypes = ["process", "window", "thread-summary", "thread"];
+
+function promiseUpdate() {
+ return promiseAboutProcessesUpdated({
+ doc,
+ tbody,
+ force: true,
+ tabAboutProcesses,
+ });
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("toolkit.aboutProcesses.showThreads", true);
+
+ info("Setting up about:processes");
+ tabAboutProcesses = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:processes",
+ waitForLoad: true,
+ });
+
+ doc = tabAboutProcesses.linkedBrowser.contentDocument;
+ tbody = doc.getElementById("process-tbody");
+ await promiseUpdate();
+});
+
+add_task(function testTwistyImageButtonSetup() {
+ let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
+ let groupRow = twistyBtn.parentNode.parentNode;
+ let groupRowId = groupRow.firstChild.children[1].getAttribute("id");
+ let groupRowLabelId = groupRowId.split(":")[1];
+
+ info("Verify twisty button is properly set up.");
+ Assert.ok(
+ twistyBtn.hasAttribute("aria-labelledby"),
+ "the Twisty image button has an aria-labelledby"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-labelledby"),
+ `${groupRowLabelId}-label ${groupRowId}`,
+ "the Twisty image button's aria-labelledby refers to a valid 'id' that is the Name of its row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("role"),
+ "button",
+ "the Twisty image is programmatically a button"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("tabindex"),
+ "0",
+ "the Twisty image button is included in the focus order"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "false",
+ "the Twisty image button is collapsed by default"
+ );
+});
+
+add_task(function testTwistyImageButtonClicking() {
+ let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
+ let groupRow = twistyBtn.parentNode.parentNode;
+
+ info(
+ "Verify we can toggle/open a list of threads by clicking the twisty button."
+ );
+ twistyBtn.click();
+
+ Assert.ok(
+ groupRow.nextSibling.classList.contains("thread") &&
+ !groupRow.nextSibling.classList.contains("thread-summary"),
+ "clicking a collapsed Twisty adds subitems after the row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "true",
+ "the Twisty image button is expanded after a click"
+ );
+});
+
+add_task(function testTwistyImageButtonKeypressing() {
+ let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
+ let groupRow = twistyBtn.parentNode.parentNode;
+
+ info(
+ `Verify we can toggle/close a list of threads by pressing Enter or
+ Space on the twisty button.`
+ );
+ // Verify the twisty button can be focused with a keyboard.
+ twistyBtn.focus();
+ Assert.equal(
+ twistyBtn,
+ doc.activeElement,
+ "the Twisty image button can be focused"
+ );
+
+ // Verify we can toggle subitems with a keyboard.
+ // Twisty is expanded
+ EventUtils.synthesizeKey("KEY_Enter");
+ Assert.ok(
+ !groupRow.nextSibling.classList.contains("thread") ||
+ groupRow.nextSibling.classList.contains("thread-summary"),
+ "pressing Enter on expanded Twisty hides a list of threads after the row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "false",
+ "the Twisty image button is collapsed after an Enter keypress"
+ );
+ // Twisty is collapsed
+ EventUtils.synthesizeKey(" ");
+ Assert.ok(
+ groupRow.nextSibling.classList.contains("thread") &&
+ !groupRow.nextSibling.classList.contains("thread-summary"),
+ "pressing Space on collapsed Twisty shows a list of threads after the row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "true",
+ "the Twisty image button is expanded after a Space keypress"
+ );
+});
+
+add_task(function cleanup() {
+ BrowserTestUtils.removeTab(tabAboutProcesses);
+ Services.prefs.clearUserPref("toolkit.aboutProcesses.showThreads");
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_utility_actors.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_utility_actors.js
new file mode 100644
index 0000000000..8ab7dcb747
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_utility_actors.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test about:processes preparation of utility actor names.
+add_task(async function testUtilityActorNames() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ opening: "about:processes",
+ waitForLoad: true,
+ },
+ browser => {
+ const View = browser.contentWindow.View;
+ const unknownActorName = "unknown";
+ const kDontExistFluentName =
+ View.utilityActorNameToFluentName("i-dont-exist");
+ const unknownFluentName =
+ View.utilityActorNameToFluentName(unknownActorName);
+
+ Assert.equal(
+ unknownFluentName,
+ kDontExistFluentName,
+ "Anything is unknown"
+ );
+
+ for (let actorName of ChromeUtils.getAllPossibleUtilityActorNames()) {
+ const fluentName = View.utilityActorNameToFluentName(actorName);
+ if (actorName === unknownActorName) {
+ Assert.ok(
+ fluentName === unknownFluentName,
+ `Actor name ${actorName} is expected unknown ${fluentName}`
+ );
+ } else {
+ Assert.ok(
+ fluentName !== unknownFluentName,
+ `Actor name ${actorName} is known ${fluentName}`
+ );
+ }
+ }
+ }
+ );
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/head.js b/toolkit/components/aboutprocesses/tests/browser/head.js
new file mode 100644
index 0000000000..7f22825fa2
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/head.js
@@ -0,0 +1,1066 @@
+/* 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();
+}
diff --git a/toolkit/components/aboutprocesses/tests/browser/small-shot.mp3 b/toolkit/components/aboutprocesses/tests/browser/small-shot.mp3
new file mode 100644
index 0000000000..f9397a5106
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/small-shot.mp3
Binary files differ