Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
"use strict";
add_task(async function testSoloSiteGainingSecondTabFormsExpandedGroup() {
let tabOne = await setupTabWithOriginAndTitle(
"Second same-site tab 1"
);
let { tabAboutProcesses, doc } = await openTabView();
Assert.ok(
!findSiteHeaderRow(doc, "example.org"),
"Starts as a solo row with no header"
);
let globalToggle = doc.getElementById("expand-all-button");
Assert.ok(
globalToggle.disabled,
"Nothing to expand with only solo tabs, so the toggle is disabled"
);
let tbody = doc.getElementById("process-tbody");
let rowOne = findTabRow(doc, "Second same-site tab 1");
let rowOneIndex = [...tbody.children].indexOf(rowOne);
// Freezing after the load, so a slow load can't outlast the freeze.
let poll = pauseTabViewPolling(doc);
let tabTwo = await setupTabWithOriginAndTitle(
"Second same-site tab 2"
);
startMouseFreeze(doc);
await poll();
Assert.ok(
!findSiteHeaderRow(doc, "example.org"),
"The new group waits for the reorder"
);
Assert.equal(
[...tbody.children].indexOf(rowOne),
rowOneIndex,
"The existing row stays in place during the mouse freeze"
);
info("Sorting reorders despite the freeze");
clickRow(doc.getElementById("column-memory-resident"));
let header = findSiteHeaderRow(doc, "example.org");
Assert.ok(
header,
"A site header is created when a second same-site tab opens"
);
Assert.equal(
header.caret.getAttribute("aria-expanded"),
"true",
"A solo site that just gained a second tab auto-expands, instead of hiding the tab already being looked at"
);
Assert.ok(!globalToggle.disabled, "A multi-tab site makes the toggle active");
let groupedTabs = childRowsOf(header).map(row => row.tabData.tab);
Assert.ok(
groupedTabs.length == 2 &&
groupedTabs.includes(tabOne) &&
groupedTabs.includes(tabTwo),
"Both same-site tabs are placed under their site header"
);
BrowserTestUtils.removeTab(tabAboutProcesses);
BrowserTestUtils.removeTab(tabOne);
BrowserTestUtils.removeTab(tabTwo);
});
add_task(async function testFileTabsGroupUnderScheme() {
let fileURL = Services.io.newFileURI(
Services.dirsvc.get("TmpD", Ci.nsIFile)
).spec;
let fileTabs = [
await BrowserTestUtils.openNewForegroundTab(gBrowser, fileURL),
await BrowserTestUtils.openNewForegroundTab(gBrowser, fileURL),
];
let { tabAboutProcesses, doc } = await openTabView();
Assert.ok(
findSiteHeaderRow(doc, "file"),
"file: tabs group under their scheme, not a blank site"
);
BrowserTestUtils.removeTab(tabAboutProcesses);
for (let tab of fileTabs) {
BrowserTestUtils.removeTab(tab);
}
});
add_task(async function testSiteGroupingRendersHeaderAndChildRows() {
let tabOne = await setupTabWithOriginAndTitle(
"Group render 1"
);
let tabTwo = await setupTabWithOriginAndTitle(
"Group render 2"
);
let soloTab = await setupTabWithOriginAndTitle(
"Group render solo"
);
let { tabAboutProcesses, doc } = await openTabView();
let headerRow = findSiteHeaderRow(doc, "example.org");
Assert.ok(headerRow, "A site-header row exists for the grouped site");
let countBadge = headerRow.querySelector(".site-tab-count");
await doc.l10n.translateElements([countBadge]);
Assert.equal(
countBadge.textContent,
"(2 tabs)",
"Tab count badge reflects the two grouped tabs"
);
Assert.equal(
childRowsOf(headerRow).length,
2,
"Two child rows follow the header"
);
Assert.ok(
!findSiteHeaderRow(doc, "example.net"),
"A single-tab site gets no header row"
);
let soloRow = findTabRow(doc, "Group render solo");
Assert.ok(
soloRow && !soloRow.classList.contains("child-row"),
"The solo tab renders as a plain, non-child row"
);
BrowserTestUtils.removeTab(tabAboutProcesses);
BrowserTestUtils.removeTab(tabOne);
BrowserTestUtils.removeTab(tabTwo);
BrowserTestUtils.removeTab(soloTab);
});
add_task(
async function testNewSiteGroupStartsCollapsedThenCaretTogglesExpansion() {
let tabOne = await setupTabWithOriginAndTitle(
"Collapse toggle 1"
);
let tabTwo = await setupTabWithOriginAndTitle(
"Collapse toggle 2"
);
let { tabAboutProcesses, doc } = await openTabView();
let headerRow = findSiteHeaderRow(doc, "example.org");
Assert.equal(
headerRow.caret.getAttribute("aria-expanded"),
"false",
"A brand-new multi-tab group starts collapsed"
);
for (let child of childRowsOf(headerRow)) {
Assert.ok(
child.classList.contains("collapsed"),
"Child rows are hidden while the group is collapsed"
);
}
info("Clicking the caret expands the group");
clickRow(headerRow.querySelector(".caret-icon"));
Assert.equal(headerRow.caret.getAttribute("aria-expanded"), "true");
for (let child of childRowsOf(headerRow)) {
Assert.ok(
!child.classList.contains("collapsed"),
"Children are visible once expanded"
);
}
info("Clicking the caret again collapses it back");
clickRow(headerRow.querySelector(".caret-icon"));
Assert.equal(headerRow.caret.getAttribute("aria-expanded"), "false");
let label = headerRow.querySelector(".site-label");
Assert.equal(
headerRow.caret.getAttribute("aria-labelledby"),
label.id,
"The caret is named by its site"
);
// Mouse-only, like .site-tab-count -- the caret is the group's one tab stop.
Assert.ok(!label.hasAttribute("role"));
Assert.ok(!label.hasAttribute("tabindex"));
AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
try {
clickRow(label);
Assert.equal(
headerRow.caret.getAttribute("aria-expanded"),
"true",
"Clicking the site label expands the group"
);
clickRow(headerRow.querySelector(".site-tab-count"));
Assert.equal(
headerRow.caret.getAttribute("aria-expanded"),
"false",
"The (N tabs) note toggles it too"
);
} finally {
AccessibilityUtils.resetEnv();
}
BrowserTestUtils.removeTab(tabAboutProcesses);
BrowserTestUtils.removeTab(tabOne);
BrowserTestUtils.removeTab(tabTwo);
}
);
add_task(async function testStaleSoloStateDoesNotSurviveSiteDisappearing() {
let tabOne = await setupTabWithOriginAndTitle(
"Stale solo 1"
);
let { doc, tabAboutProcesses } = await openTabView();
// So no poll lands between the two new tabs, catching the site as solo.
let poll = pauseTabViewPolling(doc);
Assert.ok(
!findSiteHeaderRow(doc, "example.org"),
"Starts as a solo row with no header"
);
info("Closing the only example.org tab makes the site disappear entirely");
BrowserTestUtils.removeTab(tabOne);
await poll();
info(
"Two fresh example.org tabs appear together, with no solo phase in between"
);
let tabTwo = await setupTabWithOriginAndTitle(
"Stale solo 2"
);
let tabThree = await setupTabWithOriginAndTitle(
"Stale solo 3"
);
await poll();
let headerRow = findSiteHeaderRow(doc, "example.org");
Assert.ok(headerRow, "A header row exists for the new group");
Assert.equal(
headerRow.caret.getAttribute("aria-expanded"),
"false",
"The site's earlier, now-closed solo tab doesn't auto-expand the new group"
);
BrowserTestUtils.removeTab(tabAboutProcesses);
BrowserTestUtils.removeTab(tabTwo);
BrowserTestUtils.removeTab(tabThree);
});
add_task(async function testSiteHeaderCheckboxSelectsAllChildTabs() {
let tabOne = await setupTabWithOriginAndTitle(
"Header checkbox 1"
);
let tabTwo = await setupTabWithOriginAndTitle(
"Header checkbox 2"
);
let { tabAboutProcesses, doc } = await openTabView();
let headerRow = findSiteHeaderRow(doc, "example.org");
clickRow(headerRow.querySelector(".caret-icon"));
let children = childRowsOf(headerRow);
Assert.equal(
headerRow.checkbox.getAttribute("aria-labelledby"),
headerRow.label.id,
"The header checkbox is named by its site"
);
info("Checking the header checkbox selects every child tab");
clickRow(headerRow.querySelector(".select-checkbox"));
for (let child of children) {
Assert.ok(child.hasAttribute("selected"), "Child row is selected");
}
Assert.ok(headerRow.checkbox.checked, "Header checkbox reads checked");
Assert.ok(
!headerRow.checkbox.indeterminate,
"Not indeterminate when all children are selected"
);
info("Deselecting one child makes the header checkbox indeterminate");
clickRow(children[0].querySelector(".select-checkbox"));
Assert.ok(!headerRow.checkbox.checked, "No longer fully checked");
Assert.ok(
headerRow.checkbox.indeterminate,
"Indeterminate with a partial child selection"
);
Assert.ok(
headerRow.hasAttribute("selected"),
"Header highlights while any child is selected"
);
info("Checking the header checkbox again re-selects every child");
clickRow(headerRow.querySelector(".select-checkbox"));
for (let child of children) {
Assert.ok(child.hasAttribute("selected"), "Every child selected again");
}
info("Unchecking the header checkbox clears every child's selection");
clickRow(headerRow.querySelector(".select-checkbox"));
for (let child of children) {
Assert.ok(!child.hasAttribute("selected"), "Every child cleared");
}
Assert.ok(
!headerRow.hasAttribute("selected"),
"Header not highlighted with nothing selected"
);
BrowserTestUtils.removeTab(tabAboutProcesses);
BrowserTestUtils.removeTab(tabOne);
BrowserTestUtils.removeTab(tabTwo);
});
add_task(async function testSiteRowTogglesToReloadWhenAllTabsDiscarded() {
let tabOne = await setupTabWithOriginAndTitle(
"Site reload toggle 1"
);
let tabTwo = await setupTabWithOriginAndTitle(
"Site reload toggle 2"
);
let { tabAboutProcesses, doc } = await openTabView();
// Only the actions' own refreshes can update the rows.
pauseTabViewPolling(doc);
let tbody = doc.getElementById("process-tbody");
let headerRow = findSiteHeaderRow(doc, "example.org");
info("Making one of the group's own tabs the active tab");
gBrowser.selectedTab = tabOne;
Services.fog.testResetFOG();
info("Double-clicking unload acts once");
let unloadIcon = headerRow.querySelector(".unload-icon");
Assert.equal(
doc.l10n.getAttributes(unloadIcon).args.site,
"example.org",
"The site's unload button names its site"
);
unloadIcon.click();
unloadIcon.click();
await BrowserTestUtils.waitForMutationCondition(
tbody,
{ subtree: true, childList: true, attributes: true },
() => findSiteHeaderRow(doc, "example.org")?.querySelector(".reload-icon"),
{
msg: "Every tab in the group, the active one included, is unloaded and the site row flips to reload",
}
);
Assert.ok(tabOne.hasAttribute("discarded"));
Assert.ok(tabTwo.hasAttribute("discarded"));
assertOneEvent(
Glean.resourceMonitor.tabActionTaken,
{ action_source: "site_header" },
"Only one unload is recorded, as a site_header action"
);
gBrowser.selectedTab = tabAboutProcesses;
headerRow = findSiteHeaderRow(doc, "example.org");
Assert.ok(
headerRow.classList.contains("killed"),
"The site row dims once every child tab is unloaded"
);
Assert.equal(
headerRow.querySelector(".memory").textContent,
"—",
"A fully-discarded group's memory reads as no-data, not a literal 0B sum"
);
await promiseRowActionDone(headerRow);
headerRow.querySelector(".reload-icon").click();
await BrowserTestUtils.waitForMutationCondition(
tbody,
{ subtree: true, childList: true, attributes: true },
() => findSiteHeaderRow(doc, "example.org")?.querySelector(".unload-icon"),
{ msg: "Reloading flips the site row back to unload right away" }
);
for (let tab of [tabOne, tabTwo]) {
await BrowserTestUtils.waitForMutationCondition(
tab,
{ attributeFilter: ["discarded"] },
() => !tab.hasAttribute("discarded"),
{ msg: "Each child tab finishes restoring" }
);
}
BrowserTestUtils.removeTab(tabAboutProcesses);
BrowserTestUtils.removeTab(tabOne);
BrowserTestUtils.removeTab(tabTwo);
});
add_task(async function testGlobalExpandCollapseAllSitesSyncsWithRowState() {
let siteOne = [
await setupTabWithOriginAndTitle("https://example.org", "Global sync A1"),
await setupTabWithOriginAndTitle("https://example.org", "Global sync A2"),
];
let siteTwo = [
await setupTabWithOriginAndTitle("https://example.net", "Global sync B1"),
await setupTabWithOriginAndTitle("https://example.net", "Global sync B2"),
];
let { tabAboutProcesses, doc } = await openTabView();
let globalToggle = doc.getElementById("expand-all-button");
let allExpanded = () => globalToggle.classList.contains("expanded");
Assert.ok(!allExpanded(), "Starts as expand-all with two collapsed groups");
info("Clicking the global toggle expands every group");
clickRow(globalToggle);
Assert.ok(
allExpanded(),
"Flips to collapse-all once every group is expanded"
);
for (let siteKey of ["example.org", "example.net"]) {
Assert.equal(
findSiteHeaderRow(doc, siteKey).caret.getAttribute("aria-expanded"),
"true",
`${siteKey}'s group is expanded`
);
}
info("Collapsing just one group via its own caret desyncs from all-expanded");
clickRow(findSiteHeaderRow(doc, "example.org").querySelector(".caret-icon"));
Assert.ok(
!allExpanded(),
"The global toggle flips back once not every group is expanded"
);
info("Re-expanding that one group via its own caret resyncs to all-expanded");
clickRow(findSiteHeaderRow(doc, "example.org").querySelector(".caret-icon"));
Assert.ok(
allExpanded(),
"The global toggle flips forward again once every group is expanded"
);
info("Clicking the global toggle again collapses every group");
clickRow(globalToggle);
Assert.ok(
!allExpanded(),
"The global toggle returns to expand-all after collapsing every group"
);
for (let siteKey of ["example.org", "example.net"]) {
Assert.equal(
findSiteHeaderRow(doc, siteKey).caret.getAttribute("aria-expanded"),
"false",
`${siteKey}'s group is collapsed`
);
}
BrowserTestUtils.removeTab(tabAboutProcesses);
for (let tab of [...siteOne, ...siteTwo]) {
BrowserTestUtils.removeTab(tab);
}
});
add_task(async function testNavigationKeepsDisplayedGroupUntilReorder() {
let siteOne = [
await setupTabWithOriginAndTitle("https://example.org", "Frozen site A1"),
await setupTabWithOriginAndTitle("https://example.org", "Frozen site A2"),
];
let siteTwo = [
await setupTabWithOriginAndTitle("https://example.net", "Frozen site B1"),
await setupTabWithOriginAndTitle("https://example.net", "Frozen site B2"),
];
let { tabAboutProcesses, doc } = await openTabView();
let headerOne = findSiteHeaderRow(doc, "example.org");
let headerTwo = findSiteHeaderRow(doc, "example.net");
clickRow(headerOne.caret);
Assert.equal(headerOne.caret.getAttribute("aria-expanded"), "true");
Assert.equal(headerTwo.caret.getAttribute("aria-expanded"), "false");
let movingRow = childRowsOf(headerOne).find(
row => row.tabData.tab == siteOne[0]
);
let previousSibling = movingRow.previousElementSibling;
let poll = pauseTabViewPolling(doc);
let browser = siteOne[0].linkedBrowser;
let loaded = BrowserTestUtils.browserLoaded(
browser,
false,
);
BrowserTestUtils.startLoadingURIString(browser, "https://example.net/");
await loaded;
for (let i = 0; i < 2; i++) {
startMouseFreeze(doc);
await poll();
Assert.ok(
headerOne.group.tabs.some(tabData => tabData.tab == siteOne[0]),
"The navigating tab stays in its displayed group during the freeze"
);
Assert.ok(
!headerTwo.group.tabs.some(tabData => tabData.tab == siteOne[0]),
"The destination group excludes the tab until reorder"
);
Assert.equal(movingRow.previousElementSibling, previousSibling);
Assert.ok(!movingRow.classList.contains("collapsed"));
Assert.equal(
movingRow.tabData.siteKey,
"example.net",
"Live tab data updates"
);
}
clickRow(doc.getElementById("column-name"));
Assert.ok(!findSiteHeaderRow(doc, "example.org"));
Assert.ok(
headerTwo.group.tabs.some(tabData => tabData.tab == siteOne[0]),
"The tab joins its destination group when rows reorder"
);
BrowserTestUtils.removeTab(tabAboutProcesses);
for (let tab of [...siteOne, ...siteTwo]) {
BrowserTestUtils.removeTab(tab);
}
});
add_task(async function testSiteKeysAndLabels() {
let extension = ExtensionTestUtils.loadExtension({
manifest: { name: "Site label test extension" },
});
await extension.startup();
let { tabAboutProcesses } = await openTabView();
let keys = await SpecialPowers.spawn(
tabAboutProcesses.linkedBrowser,
[extension.uuid],
uuid => {
function keyFor(spec) {
return content.getSiteKey(content.Services.io.newURI(spec));
}
function labelFor(spec) {
let uri = content.Services.io.newURI(spec);
return content.getSiteLabel(uri, content.getSiteKey(uri));
}
return {
ip: keyFor("http://127.0.0.1/"),
singleLabelHost: keyFor("http://localhost/"),
aboutBlank: keyFor("about:blank"),
aboutReader: keyFor("about:reader?url=https://example.com/"),
data: keyFor("data:text/html,<p>hi</p>"),
extensionLabel: labelFor(`moz-extension://${uuid}/page.html`),
};
}
);
await extension.unload();
Assert.equal(
keys.idnKey,
"xn--hxajbheg2az3al.xn--jxalpdlp",
"Sites are keyed by their ASCII form"
);
Assert.equal(
keys.idnLabel,
"παράδειγμα.δοκιμή",
"...but labeled with punycode decoded"
);
Assert.equal(
keys.extensionLabel,
"Site label test extension",
"Extension pages are labeled with the extension's name"
);
Assert.equal(keys.ip, "127.0.0.1", "IP host falls back to uri.host");
Assert.equal(
keys.singleLabelHost,
"localhost",
"Single-label host falls back to uri.host"
);
Assert.equal(keys.aboutBlank, "about:blank", "about: pages group per page");
Assert.equal(keys.aboutReader, "about:reader", "...ignoring the query");
Assert.equal(keys.data, "data", "Other host-less URIs group by scheme");
BrowserTestUtils.removeTab(tabAboutProcesses);
});