gh-15380: Fixed not identifying applied stamps correctly (gh-15485)

This commit is contained in:
mr. m
2026-09-19 20:42:51 +02:00
committed by GitHub
parent 8645a5b9c8
commit 450aeeccd3
9 changed files with 333 additions and 37 deletions

View File

@@ -559,22 +559,24 @@
}
.urlbarView-prettyName,
.urlbarView-tabGroup,
.urlbarView-shortcutContent {
border-radius: 4px;
border-radius: 4px !important;
font-weight: 600;
margin-top: auto;
margin-bottom: auto;
margin-top: auto !important;
margin-bottom: auto !important;
}
.urlbarView-prettyName {
padding: 4px 6px;
background-color: color-mix(in srgb, var(--zen-branding-bg-reverse), transparent 90%);
margin-left: 6px;
.urlbarView-prettyName,
.urlbarView-tabGroup {
padding: 4px 6px !important;
background-color: color-mix(in srgb, var(--zen-branding-bg-reverse), transparent 90%) !important;
margin-left: 6px !important;
font-size: 12px;
align-items: center;
gap: 6px;
display: flex;
color: color-mix(in srgb, var(--zen-primary-color), currentColor 95%);
color: color-mix(in srgb, var(--zen-primary-color), currentColor 95%) !important;
& img {
-moz-context-properties: fill;
@@ -684,8 +686,9 @@
background-color: rgba(255, 255, 255, 0.9) !important;
}
& .urlbarView-prettyName {
background-color: color-mix(in srgb, var(--zen-branding-bg-reverse), transparent 80%);
& .urlbarView-prettyName,
& .urlbarView-tabGroup {
background-color: color-mix(in srgb, var(--zen-branding-bg-reverse), transparent 80%) !important;
}
}
}

View File

@@ -381,7 +381,7 @@
/* Hide .zen-media-focus-button if it doesn't fit in the toolbar */
@container media-controls (max-width: 165px) {
.zen-media-focus-button {
.zen-media-card:not([media-sharing]) .zen-media-focus-button {
display: none;
}
}

View File

@@ -156,12 +156,26 @@ class nsZenSpacesSyncModel {
* other device (gh-15426).
*/
#appliedStamp = new Map();
#appliedStampGen = 0;
/** The projection generation the current sidebar data belongs to. */
#currentStamp() {
return lazy.ZenSessionStore.getSidebarData()?.lastCollected || 0;
}
/**
* The current projection generation, dropping held ids once a newer
* collection has caught up (they can no longer be in the skew window).
*/
#appliedStampNow() {
const stamp = this.#currentStamp();
if (stamp !== this.#appliedStampGen) {
this.#appliedStamp.clear();
this.#appliedStampGen = stamp;
}
return stamp;
}
#data() {
if (!this.#file) {
this.#file = new JSONFile({
@@ -759,11 +773,11 @@ class nsZenSpacesSyncModel {
const uploaded = this.#data().uploaded;
const current = this.#digestAll();
const pending = this.#pendingIds();
const stamp = this.#currentStamp();
const stamp = this.#appliedStampNow();
const now = Date.now() / 1000;
const changes = {};
for (const [id, digest] of current) {
if (uploaded[id] !== digest) {
if (uploaded[id] !== digest && this.#appliedStamp.get(id) !== stamp) {
changes[id] = now;
}
}
@@ -795,9 +809,9 @@ class nsZenSpacesSyncModel {
const uploaded = this.#data().uploaded;
const current = this.#digestAll();
const pending = this.#pendingIds();
const stamp = this.#currentStamp();
const stamp = this.#appliedStampNow();
for (const [id, digest] of current) {
if (uploaded[id] !== digest) {
if (uploaded[id] !== digest && this.#appliedStamp.get(id) !== stamp) {
return true;
}
}
@@ -849,15 +863,13 @@ class nsZenSpacesSyncModel {
*/
noteApplied(id, cleartext) {
const data = this.#data();
const stamp = this.#appliedStampNow();
if (!cleartext) {
delete data.uploaded[id];
this.#appliedStamp.delete(id);
} else {
data.uploaded[id] = recordDigest(cleartext.kind, cleartext.data);
// Hold back a tombstone for this id until the projection is recollected
// past the generation it was applied in (gh-15426).
this.#appliedStamp.set(id, this.#currentStamp());
}
this.#appliedStamp.set(id, stamp);
syncLog(
`acknowledged incoming ${cleartext ? cleartext.kind : "tombstone"} ${id}`
);

View File

@@ -12,6 +12,8 @@ support-files = [
"head.js",
]
["browser_apply_skew.js"]
["browser_folder_delete.js"]
["browser_normal_tabs_apply.js"]

View File

@@ -0,0 +1,51 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_AppliedCreateNotTombstonedBeforeCollection() {
await gZenWorkspaces.promiseInitialized;
await collectProjections();
const id = "test-skew-create";
ZenSpacesSyncModel.noteApplied(id, tabRecord(id, { pinned: true }).cleartext);
Assert.ok(
!ZenSpacesSyncModel.itemExists(id),
"the id is acknowledged but not yet projected (the skew window)"
);
const changes = ZenSpacesSyncModel.computeChangedIDs();
Assert.ok(
!(id in changes),
"a just-applied create is not tombstoned before the projection catches up"
);
ZenSpacesSyncModel.noteApplied(id, null);
});
add_task(async function test_AppliedDeletionNotResurrectedBeforeCollection() {
await gZenWorkspaces.promiseInitialized;
const tab = await openSyncableTab("https://example.com/?skew-delete", {
pinned: true,
});
const id = tab.id;
await collectProjections();
ZenSpacesSyncModel.markUploaded([id]);
Assert.ok(
!(id in ZenSpacesSyncModel.computeChangedIDs()),
"the uploaded tab is clean before the deletion"
);
const failed = await ZenSpacesSyncApplier.applyBatch([tombstone(id)]);
Assert.deepEqual(failed, [], "the tombstone applies cleanly");
Assert.ok(!document.getElementById(id), "the tab is removed");
const changes = ZenSpacesSyncModel.computeChangedIDs();
Assert.ok(
!(id in changes),
"a just-applied deletion is not re-uploaded before the projection catches up"
);
ZenSpacesSyncModel.noteApplied(id, null);
});

View File

@@ -13,4 +13,6 @@ support-files = [
["browser_issue_7385.js"]
["browser_sidebar_provider.js"]
["browser_urlbar_close_token.js"]

View File

@@ -0,0 +1,221 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
UrlbarShared: "chrome://browser/content/urlbar/UrlbarShared.mjs",
UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
SessionSaver:
"moz-src:///browser/components/sessionstore/SessionSaver.sys.mjs",
TabStateFlusher:
"moz-src:///browser/components/sessionstore/TabStateFlusher.sys.mjs",
});
const PROVIDER_NAME = "ZenUrlbarProviderSidebar";
const TAB_URL = "https://example.com/";
async function collectSidebarData() {
await TabStateFlusher.flushWindow(window);
await SessionSaver.run();
}
async function searchSidebarRows(value) {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
waitForFocus,
value,
});
const rows = [];
for (let index = 0; index < UrlbarTestUtils.getResultCount(window); index++) {
const { result } = await UrlbarTestUtils.getRowAt(window, index);
if (result.providerName == PROVIDER_NAME) {
rows.push({ index, result });
}
}
return rows;
}
async function addLabelledTab(label) {
const tab = BrowserTestUtils.addTab(gBrowser, TAB_URL, {
skipAnimation: true,
});
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
tab.zenStaticLabel = label;
gBrowser._setTabLabel(tab, label);
return tab;
}
async function removeFolder(folder) {
const removeEvent = BrowserTestUtils.waitForEvent(folder, "TabGroupRemoved");
folder.delete();
await removeEvent;
}
add_task(async function test_custom_label_is_searchable() {
const tab = await addLabelledTab("Quarterly zeninvoices");
await collectSidebarData();
const rows = await searchSidebarRows("zeninvoices quarterly");
Assert.equal(rows.length, 1, "The renamed tab is the only match");
const { result } = rows[0];
Assert.equal(result.type, UrlbarShared.RESULT_TYPE.TAB_SWITCH);
Assert.equal(result.payload.url, TAB_URL);
Assert.equal(
result.payload.title,
"Quarterly zeninvoices",
"The custom label is shown instead of the page title"
);
Assert.deepEqual(
await searchSidebarRows("zeninvoices yearly"),
[],
"Every token needs to be part of the label"
);
await UrlbarTestUtils.promisePopupClose(window);
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_current_tab_is_not_suggested() {
const tab = await addLabelledTab("zencurrenttab");
gBrowser.selectedTab = tab;
await collectSidebarData();
Assert.deepEqual(
await searchSidebarRows("zencurrenttab"),
[],
"There is no point in switching to the tab we are already in"
);
await UrlbarTestUtils.promisePopupClose(window);
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_stale_sidebar_url_is_not_suggested() {
const tab = await addLabelledTab("zenstaletab");
await collectSidebarData();
BrowserTestUtils.startLoadingURIString(
tab.linkedBrowser,
"https://example.org/"
);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
Assert.deepEqual(
await searchSidebarRows("zenstaletab"),
[],
"A url that is not open anymore must not be offered as switch to tab"
);
await UrlbarTestUtils.promisePopupClose(window);
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_only_active_space_tabs() {
const originalSpace = gZenWorkspaces.activeWorkspace;
const tab = await addLabelledTab("zenotherspace");
await gZenWorkspaces.createAndSaveWorkspace("Sidebar Provider Space");
Assert.notEqual(
gZenWorkspaces.activeWorkspace,
originalSpace,
"The new space is the active one"
);
await collectSidebarData();
Assert.deepEqual(
await searchSidebarRows("zenotherspace"),
[],
"Tabs from other spaces are not matched"
);
await UrlbarTestUtils.promisePopupClose(window);
await gZenWorkspaces.removeWorkspace(gZenWorkspaces.activeWorkspace);
Assert.equal(gZenWorkspaces.activeWorkspace, originalSpace);
await collectSidebarData();
const rows = await searchSidebarRows("zenotherspace");
Assert.equal(rows.length, 1, "The tab is matched again in its own space");
await UrlbarTestUtils.promisePopupClose(window);
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_folders_path_and_ranking() {
const tab = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab1");
const tab2 = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab2");
const subfolder = await gZenFolders.createFolder([tab], {
renameFolder: false,
label: "zenfold archive notes",
});
const parent = await gZenFolders.createFolder([tab2], {
renameFolder: false,
label: "zenfold",
});
parent.tabs[0].after(subfolder);
await collectSidebarData();
const rows = await searchSidebarRows("zenfold");
Assert.equal(rows.length, 2, "Both folders are matched");
for (const { result } of rows) {
Assert.equal(result.type, UrlbarShared.RESULT_TYPE.DYNAMIC);
}
const [best, worst] = rows;
const space = gZenWorkspaces.getWorkspaceFromId(
gZenWorkspaces.activeWorkspace
);
Assert.equal(best.result.payload.zenFolderId, parent.id);
Assert.equal(
best.result.payload.path,
[space.name, "zenfold"].join(" / "),
"A root folder only shows its space and name"
);
Assert.equal(best.index, 1, "A full match sits right below the heuristic");
Assert.equal(worst.result.payload.zenFolderId, subfolder.id);
Assert.equal(
worst.result.payload.path,
[space.name, "zenfold", "zenfold archive notes"].join(" / "),
"A subfolder shows every parent folder"
);
Assert.greater(
worst.index,
best.index,
"The weaker the match, the further down the folder goes"
);
await UrlbarTestUtils.promisePopupClose(window);
await removeFolder(subfolder);
await removeFolder(parent);
});
add_task(async function test_picking_a_folder_reveals_it() {
const tab = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab1");
const tab2 = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab2");
const subfolder = await gZenFolders.createFolder([tab], {
renameFolder: false,
label: "zenreveal",
});
const parent = await gZenFolders.createFolder([tab2], {
renameFolder: false,
label: "parent",
});
parent.tabs[0].after(subfolder);
subfolder.collapsed = true;
parent.collapsed = true;
await collectSidebarData();
const rows = await searchSidebarRows("zenreveal");
Assert.equal(rows.length, 1, "The subfolder is matched");
UrlbarTestUtils.setSelectedRowIndex(window, rows[0].index);
await UrlbarTestUtils.promisePopupClose(window, () =>
EventUtils.synthesizeKey("KEY_Enter")
);
await TestUtils.waitForCondition(
() => !parent.collapsed && !subfolder.collapsed,
"The folder and its parent get expanded"
);
await removeFolder(subfolder);
await removeFolder(parent);
});

View File

@@ -49,21 +49,18 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider {
return;
}
const tokens = queryContext.tokens.map(t => t.lowerCaseValue);
const matches = (percentage, text) => {
const score = text => {
text = text.toLowerCase();
const matchCount = tokens.filter(token => text.includes(token)).length;
return matchCount / tokens.length >= percentage;
if (!tokens.every(token => text.includes(token))) {
return 0;
}
return tokens.reduce((sum, token) => sum + token.length, 0) / text.length;
};
this.#addFolders(sidebar, matches.bind(undefined, 0.7), addCallback);
this.#addTabs(
sidebar,
matches.bind(undefined, 0.4),
queryContext,
addCallback
);
this.#addFolders(sidebar, score, queryContext, addCallback);
this.#addTabs(sidebar, score, queryContext, addCallback);
}
#addTabs(sidebar, matches, queryContext, addCallback) {
#addTabs(sidebar, score, queryContext, addCallback) {
const openTabUrls = lazy.UrlbarProviderOpenTabs.getOpenTabUrls();
const activeSpace =
lazy.BrowserWindowTracker.getTopWindow().gZenWorkspaces.activeWorkspace;
@@ -76,7 +73,7 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider {
const entries = tabData.entries || [];
const entry = entries[(tabData.index || entries.length) - 1];
const label = tabData.zenStaticLabel || entry?.title;
if (typeof label !== "string" || !label || !matches(label)) {
if (typeof label !== "string" || !label || !score(label)) {
continue;
}
const url = entry?.url;
@@ -120,14 +117,17 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider {
}
}
#addFolders(sidebar, matches, addCallback) {
#addFolders(sidebar, score, queryContext, addCallback) {
const folders = new Map(
(sidebar.folders || []).map(folder => [folder.id, folder])
);
for (const folder of folders.values()) {
if (folder.splitViewGroup || !folder.name || !matches(folder.name)) {
continue;
}
const matched = [...folders.values()]
.filter(folder => !folder.splitViewGroup && folder.name)
.map(folder => ({ folder, score: score(folder.name) }))
.filter(match => match.score)
.sort((a, b) => b.score - a.score);
for (const match of matched) {
const { folder } = match;
const space = sidebar.spaces?.find(s => s.uuid == folder.workspaceId);
const path = [folder.name];
for (
@@ -153,6 +153,11 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider {
icon: "chrome://browser/skin/zen-icons/folder.svg",
path: path.join(" / "),
},
suggestedIndex:
1 +
Math.round(
(1 - Math.min(match.score, 1)) * (queryContext.maxResults - 2)
),
})
);
}

View File

@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.22.2b",
"displayVersion": "1.22.3b",
"github": {
"repo": "zen-browser/desktop"
},