commit d77786ccccc767d9b139ed1dd8d026d4b8b73d12
parent dc5e86c79bf3d6459e305be849dabbe56d5ca8da
Author: Duncan McIntosh <dmcintosh@mozilla.com>
Date: Wed, 1 Oct 2025 13:51:38 +0000
Bug 1982087 - Additionally distinguish Taskbar Tabs by their manifest's scope, if provided. r=mossop
Differential Revision: https://phabricator.services.mozilla.com/D264331
Diffstat:
4 files changed, 185 insertions(+), 31 deletions(-)
diff --git a/browser/components/taskbartabs/TaskbarTabs.sys.mjs b/browser/components/taskbartabs/TaskbarTabs.sys.mjs
@@ -59,6 +59,11 @@ export const TaskbarTabs = new (class {
return this.#registry.findOrCreateTaskbarTab(...args);
}
+ async findTaskbarTab(...args) {
+ await this.#ready;
+ return this.#registry.findTaskbarTab(...args);
+ }
+
/**
* Moves an existing tab into a new Taskbar Tab window.
*
diff --git a/browser/components/taskbartabs/TaskbarTabsRegistry.sys.mjs b/browser/components/taskbartabs/TaskbarTabsRegistry.sys.mjs
@@ -39,7 +39,12 @@ async function getJsonSchema() {
class TaskbarTab {
// Unique identifier for the Taskbar Tab.
#id;
- // List of hosts associated with this Taskbar tab.
+ // List of scopes associated with this Taskbar Tab. A scope has a 'hostname'
+ // property, and a 'prefix' property. If a 'prefix' is set, then the path
+ // must literally start with that prefix; this matches the 'within scope'
+ // algorithm of the Web App Manifest specification.
+ //
+ // @type {{ hostname: string; [prefix]: string }[]}
#scopes = [];
// Container the Taskbar Tab is opened in when opened from the Taskbar.
#userContextId;
@@ -237,10 +242,23 @@ export class TaskbarTabsRegistry {
return taskbarTab;
}
+ let scope = { hostname: aUrl.host };
+ if ("scope" in manifest) {
+ // Note: manifest.scope will not be set unless the start_url is
+ // within scope. As such, this scope always contains the start_url.
+ // If a manifest is used but there isn't a scope, it uses the parent
+ // of the start_url; e.g. '/a/b/c.html' --> '/a/b'.
+ const scopeUri = Services.io.newURI(manifest.scope);
+ scope = {
+ hostname: scopeUri.host,
+ prefix: scopeUri.filePath,
+ };
+ }
+
let id = Services.uuid.generateUUID().toString().slice(1, -1);
taskbarTab = new TaskbarTab({
id,
- scopes: [{ hostname: aUrl.host }],
+ scopes: [scope],
userContextId: aUserContextId,
name: manifest.name ?? generateName(aUrl),
startUrl: manifest.start_url ?? aUrl.prePath,
@@ -299,19 +317,32 @@ export class TaskbarTabsRegistry {
}
for (const tt of this.#taskbarTabs) {
+ let bestPrefix = "";
for (const scope of tt.scopes) {
- if (aUrl.host === scope.hostname) {
- if (aUserContextId !== tt.userContextId) {
- lazy.logConsole.info(
- `Matched TaskbarTab for URL ${aUrl.host} to ${scope.hostname}, but container ${aUserContextId} mismatched ${tt.userContextId}.`
- );
- } else {
- lazy.logConsole.info(
- `Matched TaskbarTab for URL ${aUrl.host} to ${scope.hostname} with container ${aUserContextId}.`
- );
- return tt;
+ if (aUrl.host !== scope.hostname) {
+ continue;
+ }
+ if ("prefix" in scope) {
+ if (scope.prefix.length < bestPrefix.length) {
+ // We've already found something better.
+ continue;
+ }
+ if (!aUrl.filePath.startsWith(scope.prefix)) {
+ // This URL wouldn't be within scope.
+ continue;
}
}
+
+ if (aUserContextId !== tt.userContextId) {
+ lazy.logConsole.info(
+ `Matched TaskbarTab for URL ${aUrl.host} to ${scope.hostname}, but container ${aUserContextId} mismatched ${tt.userContextId}.`
+ );
+ } else {
+ lazy.logConsole.info(
+ `Matched TaskbarTab for URL ${aUrl.host} to ${scope.hostname} with container ${aUserContextId}.`
+ );
+ return tt;
+ }
}
}
diff --git a/browser/components/taskbartabs/test/browser/browser_taskbarTabs_manifest.js b/browser/components/taskbartabs/test/browser/browser_taskbarTabs_manifest.js
@@ -100,30 +100,113 @@ add_task(async function test_nameAndStartUrl() {
httpUrl("/example/path/string/here"),
"Page action detected correct start URL"
);
- });
+ }, "/example/path/string/here");
});
-async function usingManifest(aCallback) {
- const location = httpUrl("/taskbartabs-manifest.json");
+add_task(async function test_scope() {
+ gManifest = {
+ scope: "/example/",
+ };
- await BrowserTestUtils.withNewTab(httpUrl("/"), async browser => {
- await SpecialPowers.spawn(browser, [location], async url => {
- content.document.body.innerHTML = `<link rel="manifest" href="${url}">`;
+ await usingManifest((aWindow, aTaskbarTab) => {
+ Assert.deepEqual(aTaskbarTab.scopes[0], {
+ hostname: "localhost",
+ prefix: "/example/",
});
+ }, "/example/file");
+});
- const tab = window.gBrowser.getTabForBrowser(browser);
- let result = await TaskbarTabs.moveTabIntoTaskbarTab(tab);
+add_task(async function test_startUrlOutOfScope() {
+ gManifest = {
+ scope: "/example/",
+ start_url: "/example",
+ };
- const uri = Services.io.newURI(httpUrl("/"));
- const tt = await TaskbarTabs.findOrCreateTaskbarTab(uri, 0);
- is(
- await TaskbarTabsUtils.getTaskbarTabIdFromWindow(result.window),
- tt.id,
- "moveTabIntoTaskbarTab created a Taskbar Tab"
- );
- aCallback(result.window, tt);
+ await usingManifest((aWindow, aTaskbarTab) => {
+ Assert.deepEqual(aTaskbarTab.scopes[0], {
+ hostname: "localhost",
+ prefix: "/",
+ });
+ }, "/example");
+});
- await TaskbarTabs.removeTaskbarTab(tt.id);
- await BrowserTestUtils.closeWindow(result.window);
- });
+add_task(async function test_scopeDistinguishesTaskbarTabs() {
+ const find = prefix =>
+ TaskbarTabs.findTaskbarTab(Services.io.newURI(httpUrl(prefix)), 0);
+
+ gManifest = {
+ scope: "/example/",
+ };
+
+ await usingManifest(async (aWindowOuter, aTaskbarTabOuter) => {
+ gManifest = {
+ scope: "/example/another/",
+ };
+
+ await usingManifest(async (aWindowInner, aTaskbarTabInner) => {
+ is(
+ (await find("/example/another/still")).id,
+ aTaskbarTabInner.id,
+ "/example/another/still matches to inner Taskbar Tab"
+ );
+ is(
+ (await find("/example/another/")).id,
+ aTaskbarTabInner.id,
+ "/example/another/ matches to inner Taskbar Tab"
+ );
+ is(
+ (await find("/example/another")).id,
+ aTaskbarTabOuter.id,
+ "/example/another matches to outer Taskbar Tab"
+ );
+ is(
+ (await find("/example/different")).id,
+ aTaskbarTabOuter.id,
+ "/example/different matches to outer Taskbar Tab"
+ );
+ is(
+ (await find("/example/")).id,
+ aTaskbarTabOuter.id,
+ "/example/ matches to outer Taskbar Tab"
+ );
+
+ is(await find("/example"), null, "/example does not match a Taskbar Tab");
+ is(
+ await find("/unrelated"),
+ null,
+ "/unrelated does not match any Taskbar Tab"
+ );
+ }, "/example/another/main");
+ }, "/example/main");
+});
+
+async function usingManifest(aCallback, aLocation = "/") {
+ const location = httpUrl("/taskbartabs-manifest.json");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: httpUrl(aLocation),
+ gBrowser: window.gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [location], async url => {
+ content.document.body.innerHTML = `<link rel="manifest" href="${url}">`;
+ });
+
+ const tab = window.gBrowser.getTabForBrowser(browser);
+ let result = await TaskbarTabs.moveTabIntoTaskbarTab(tab);
+
+ const uri = Services.io.newURI(httpUrl(aLocation));
+ const tt = await TaskbarTabs.findOrCreateTaskbarTab(uri, 0);
+ is(
+ await TaskbarTabsUtils.getTaskbarTabIdFromWindow(result.window),
+ tt.id,
+ "moveTabIntoTaskbarTab created a Taskbar Tab"
+ );
+ await aCallback(result.window, tt);
+
+ await TaskbarTabs.removeTaskbarTab(tt.id);
+ await BrowserTestUtils.closeWindow(result.window);
+ }
+ );
}
diff --git a/browser/components/taskbartabs/test/xpcshell/test_TaskbarTabsRegistry.js b/browser/components/taskbartabs/test/xpcshell/test_TaskbarTabsRegistry.js
@@ -305,3 +305,38 @@ add_task(async function test_shortcutRelativePath_is_saved() {
"Shortcut relative path should be saved"
);
});
+
+add_task(async function test_multiple_match_longest_prefix() {
+ const registry = new TaskbarTabsRegistry();
+
+ const uriWithPrefix = prefix =>
+ Services.io.newURI("https://example.com" + prefix);
+
+ const createWithScope = uri =>
+ registry.findOrCreateTaskbarTab(uri, 0, {
+ manifest: {
+ scope: uri.spec,
+ },
+ });
+ const find = prefix => registry.findTaskbarTab(uriWithPrefix(prefix), 0);
+
+ // Register them in an arbitrary order.
+ const ttAB = createWithScope(uriWithPrefix("/ab"));
+ const ttA = createWithScope(uriWithPrefix("/a"));
+ const ttABC = createWithScope(uriWithPrefix("/abc"));
+ const ttABCD = createWithScope(uriWithPrefix("/abc/d/"));
+
+ equal(find("/q"), null, "/q does not exist");
+ equal(find("/a").id, ttA.id, "/a matches /a");
+ equal(find("/az").id, ttA.id, "/a matches /az");
+
+ equal(find("/ab").id, ttAB.id, "/ab matches /ab");
+ equal(find("/abq").id, ttAB.id, "/abq matches /ab");
+
+ equal(find("/abc").id, ttABC.id, "/abc matches /abc");
+ equal(find("/abc/").id, ttABC.id, "/abc/ matches /abc");
+ equal(find("/abc/d").id, ttABC.id, "/abc/d matches /abc (not /abc/d/)");
+
+ equal(find("/abc/d/").id, ttABCD.id, "/abc/d/ matches /abc/d/");
+ equal(find("/abc/d/efgh").id, ttABCD.id, "/abc/d/efgh matches /abc/d/");
+});