Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { AutoTabGroupingSuggestions } = ChromeUtils.importESModule(
"moz-src:///browser/components/aiwindow/ui/modules/AutoTabGroupingSuggestions.sys.mjs"
);
function makeTab({
pinned = false,
closing = false,
group = null,
hidden = false,
busy = false,
label = "Example Page",
} = {}) {
return {
pinned,
closing,
group,
hidden,
label,
hasAttribute(attr) {
return attr === "busy" && busy;
},
linkedBrowser: url ? { currentURI: Services.io.newURI(url) } : null,
};
}
function makeWin(tabs) {
return { gBrowser: { tabs } };
}
function makeCluster(size, cohesion = 0.5) {
return {
tabs: Array.from({ length: size }, (_, i) => ({ index: i })),
cohesion,
};
}
add_setup(function () {
Services.prefs.setIntPref(
"browser.smartwindow.autoTabGrouping.minTabsPerGroup",
2
);
Services.prefs.setIntPref("browser.smartwindow.autoTabGrouping.maxGroups", 3);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(
"browser.smartwindow.autoTabGrouping.minTabsPerGroup"
);
Services.prefs.clearUserPref(
"browser.smartwindow.autoTabGrouping.maxGroups"
);
});
});
add_task(function test_getCandidateTabs_keepsOnlyUngroupedWebTabs() {
const httpsTab = makeTab({ url: "https://a.example/" });
const httpTab = makeTab({ url: "http://b.example/" });
const pinnedTab = makeTab({ pinned: true });
const closingTab = makeTab({ closing: true });
const groupedTab = makeTab({ group: { id: "g1" } });
const hiddenTab = makeTab({ hidden: true });
const aboutTab = makeTab({ url: "about:newtab" });
const chromeTab = makeTab({ url: "chrome://browser/content/browser.xhtml" });
const noBrowserTab = makeTab({ url: null });
const win = makeWin([
httpsTab,
httpTab,
pinnedTab,
closingTab,
groupedTab,
hiddenTab,
aboutTab,
chromeTab,
noBrowserTab,
]);
const candidates = AutoTabGroupingSuggestions.getCandidateTabs(win);
Assert.deepEqual(
candidates,
[httpsTab, httpTab],
"Only ungrouped, non-pinned, visible http(s) tabs are candidates"
);
});
add_task(function test_getCandidateTabs_excludesStillLoadingTabs() {
const loaded = makeTab({ url: "https://a.example/" });
const busyTab = makeTab({ url: "https://b.example/", busy: true });
const untitledTab = makeTab({ url: "https://c.example/", label: "" });
const win = makeWin([loaded, busyTab, untitledTab]);
const candidates = AutoTabGroupingSuggestions.getCandidateTabs(win);
Assert.deepEqual(
candidates,
[loaded],
"Still-loading and untitled tabs are excluded so clustering sees resolved titles"
);
});
add_task(function test_getCandidateTabs_excludesAlreadyGroupedTabs() {
const ungrouped = makeTab({ url: "https://a.example/" });
const grouped = makeTab({ url: "https://b.example/", group: { id: "g1" } });
const win = makeWin([ungrouped, grouped]);
const candidates = AutoTabGroupingSuggestions.getCandidateTabs(win);
Assert.deepEqual(
candidates,
[ungrouped],
"Tabs already in a group are never re-suggested"
);
});
add_task(function test_selectClusters_filtersSortsAndCaps() {
// Sizes 1 and 1 are below minTabsPerGroup (2) and dropped; the rest are
// ordered largest-first and capped at maxGroups (3).
const clusters = [
makeCluster(1),
makeCluster(5),
makeCluster(3),
makeCluster(2),
makeCluster(4),
makeCluster(1),
];
const selected = AutoTabGroupingSuggestions.selectClusters(clusters);
Assert.equal(selected.length, 3, "Capped at maxGroups");
Assert.deepEqual(
selected.map(c => c.tabs.length),
[5, 4, 3],
"Largest clusters first, small ones dropped, capped at maxGroups"
);
});
add_task(function test_selectClusters_dropsLowCohesion() {
// A large but incohesive cluster (below the 0.1 threshold) is dropped even
// though it clears the size minimum, so weakly-related tabs aren't grouped.
const clusters = [makeCluster(5, 0.05), makeCluster(3, 0.4)];
const selected = AutoTabGroupingSuggestions.selectClusters(clusters);
Assert.deepEqual(
selected.map(c => c.tabs.length),
[3],
"Clusters below the cohesion threshold are dropped regardless of size"
);
});
add_task(function test_selectClusters_minCohesionPrefTunesThreshold() {
// Raising the pref to 0.5 now drops a 0.4-cohesion cluster that the default
// 0.1 threshold would have kept.
Services.prefs.setCharPref(
"browser.smartwindow.autoTabGrouping.minCohesion",
"0.5"
);
try {
const selected = AutoTabGroupingSuggestions.selectClusters([
makeCluster(5, 0.4),
makeCluster(3, 0.6),
]);
Assert.deepEqual(
selected.map(c => c.tabs.length),
[3],
"Only clusters at or above the pref-configured threshold are kept"
);
} finally {
Services.prefs.clearUserPref(
"browser.smartwindow.autoTabGrouping.minCohesion"
);
}
});
add_task(function test_selectClusters_handlesEmptyInput() {
Assert.deepEqual(
AutoTabGroupingSuggestions.selectClusters(undefined),
[],
"undefined clusters -> no proposals"
);
Assert.deepEqual(
AutoTabGroupingSuggestions.selectClusters([]),
[],
"no clusters -> no proposals"
);
Assert.deepEqual(
AutoTabGroupingSuggestions.selectClusters([makeCluster(1), { tabs: null }]),
[],
"clusters below the minimum (or missing tabs) -> no proposals"
);
});
add_task(async function test_buildProposals_dropsUntitledGroups() {
const originalManager = AutoTabGroupingSuggestions._manager;
AutoTabGroupingSuggestions._manager = {
async generateClusters() {
return {
clusterRepresentations: [makeCluster(3, 0.9), makeCluster(2, 0.9)],
};
},
async getPredictedLabelForGroup(tabs) {
// The larger cluster comes back unlabeled (a Trust & Safety signal).
return tabs.length === 3 ? "" : "Work";
},
};
AutoTabGroupingSuggestions._labelCache.clear();
try {
const proposals = await AutoTabGroupingSuggestions.buildProposals([]);
Assert.equal(proposals.length, 1, "The untitled group is dropped");
Assert.equal(proposals[0].label, "Work", "The labeled group is kept");
} finally {
AutoTabGroupingSuggestions._manager = originalManager;
AutoTabGroupingSuggestions._labelCache.clear();
}
});
add_task(async function test_buildProposals_cachesLabelsBySourceTabs() {
const originalManager = AutoTabGroupingSuggestions._manager;
const tabs = [
makeTab({ url: "https://a.example/" }),
makeTab({ url: "https://b.example/" }),
];
let labelCalls = 0;
AutoTabGroupingSuggestions._manager = {
async generateClusters() {
return { clusterRepresentations: [{ tabs, cohesion: 0.9 }] };
},
async getPredictedLabelForGroup() {
labelCalls++;
return "Work";
},
};
AutoTabGroupingSuggestions._labelCache.clear();
try {
const first = await AutoTabGroupingSuggestions.buildProposals([]);
const second = await AutoTabGroupingSuggestions.buildProposals([]);
Assert.equal(
labelCalls,
1,
"The label is generated once and reused for the same source tabs"
);
Assert.equal(first[0].label, "Work", "First run labels the group");
Assert.equal(
second[0].label,
"Work",
"Second run returns the cached label"
);
} finally {
AutoTabGroupingSuggestions._manager = originalManager;
AutoTabGroupingSuggestions._labelCache.clear();
}
});