From 8df6fef1acf750e5870bacb85f5984374f8f02cb Mon Sep 17 00:00:00 2001 From: Nikola Hristov <139394063+iexavl@users.noreply.github.com> Date: Fri, 28 Aug 2026 00:45:37 +0300 Subject: [PATCH] gh-15115: Tests for urlbar close and split view (gh-15118) Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> --- .../split_view/browser_split_view_empty.js | 169 ++++++++--- src/zen/tests/urlbar/browser.toml | 2 + .../urlbar/browser_urlbar_close_token.js | 266 ++++++++++++++++++ 3 files changed, 402 insertions(+), 35 deletions(-) create mode 100644 src/zen/tests/urlbar/browser_urlbar_close_token.js diff --git a/src/zen/tests/split_view/browser_split_view_empty.js b/src/zen/tests/split_view/browser_split_view_empty.js index 6fbddbd74..84ec75389 100644 --- a/src/zen/tests/split_view/browser_split_view_empty.js +++ b/src/zen/tests/split_view/browser_split_view_empty.js @@ -7,50 +7,149 @@ const { UrlbarTestUtils } = ChromeUtils.importESModule( "resource://testing-common/UrlbarTestUtils.sys.mjs" ); +/** + * @param {object} [staleToken] - Token of a session the pane will supersede. + */ +async function stageEmptySplit(staleToken = null) { + let staleClosed = null; + + if (staleToken) { + staleClosed = BrowserTestUtils.waitForEvent( + window, + "ZenURLBarClosed", + false, + event => gZenUIManager.matchesCloseToken(staleToken, event) + ); + } + const staleClose = gURLBar._zenHandleUrlbarClose; + document.getElementById("cmd_zenNewEmptySplit").doCommand(); + await TestUtils.waitForCondition( + () => + gURLBar._zenHandleUrlbarClose && + gURLBar._zenHandleUrlbarClose !== staleClose, + "Waiting for the staged pane to open its own urlbar session" + ); + await UrlbarTestUtils.promisePopupOpen(window, () => {}); + + if (staleClosed) { + await staleClosed; + } + + ok(gURLBar.view.isOpen, "The staged pane's urlbar is still open"); + Assert.equal( + gBrowser.selectedTab, + gZenWorkspaces._emptyTab, + "The empty pane should be staged and selected" + ); +} + +/** + * Stages an empty split and commits it by picking the first urlbar result. + * + * @param {Tab} originalTab - The tab the split is being created from. + * @param {object} [staleToken] - Token of a session the pane will supersede. + */ +async function createEmptySplitAndCheck(originalTab, staleToken = null) { + await stageEmptySplit(staleToken); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "https://example.com", + }); + const waitForActivationPromise = BrowserTestUtils.waitForEvent( + window, + "ZenViewSplitter:SplitViewActivated" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(result.element.row, {}); + await waitForActivationPromise; + await new Promise(resolve => { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + setTimeout(async () => { + resolve(); + }, 100); + }); + const selectedTab = gBrowser.selectedTab; + ok( + gBrowser.tabpanels.hasAttribute("zen-split-view"), + "The split view should not have crashed with two tabs in it" + ); + ok( + !gZenWorkspaces._emptyTab.splitView, + "The empty tab should not be in split view" + ); + ok(!gZenWorkspaces._emptyTab.group, "The empty tab should not be in a group"); + ok(selectedTab.splitView, "The selected tab should be in split view"); + ok(originalTab.splitView, "The original tab should be in split view"); + Assert.equal( + gBrowser.tabpanels.querySelectorAll('[zen-split="true"]').length, + 2, + "There should be two split views present" + ); + await BrowserTestUtils.removeTab(selectedTab); +} + +/** + * @returns {Promise} The session's close token. + */ +async function openUrlbarSession() { + const closeToken = {}; + await UrlbarTestUtils.promisePopupOpen(window, () => { + ok( + gZenUIManager.handleNewTab(false, false, "tab", true, closeToken), + "The urlbar session should have opened" + ); + }); + ok(gURLBar.hasAttribute("zen-newtab"), "The session owns the urlbar"); + return closeToken; +} + add_task(async function test_Split_View_Empty() { + await BrowserTestUtils.withNewTab("https://example.com", async function () { + await createEmptySplitAndCheck(gBrowser.selectedTab); + }); +}); + +add_task(async function test_Split_View_Empty_With_Urlbar_Already_Open() { + await SimpleTest.promiseFocus(window); + await BrowserTestUtils.withNewTab("https://example.com", async function () { + const staleToken = await openUrlbarSession(); + await createEmptySplitAndCheck(gBrowser.selectedTab, staleToken); + }); +}); + +add_task(async function test_Split_View_Empty_Cancelled_With_Urlbar_Open() { + await SimpleTest.promiseFocus(window); await BrowserTestUtils.withNewTab("https://example.com", async function () { const originalTab = gBrowser.selectedTab; const command = document.getElementById("cmd_zenNewEmptySplit"); - command.doCommand(); - await UrlbarTestUtils.promisePopupOpen(window, () => {}); - await UrlbarTestUtils.promiseAutocompleteResultPopup({ - window, - waitForFocus, - value: "https://example.com", - }); - const waitForActivationPromise = BrowserTestUtils.waitForEvent( - window, - "ZenViewSplitter:SplitViewActivated" - ); - let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); - EventUtils.synthesizeMouseAtCenter(result.element.row, {}); - await waitForActivationPromise; - await new Promise(resolve => { - /* eslint-disable mozilla/no-arbitrary-setTimeout */ - setTimeout(async () => { - resolve(); - }, 100); - }); - const selectedTab = gBrowser.selectedTab; - ok( - gBrowser.tabpanels.hasAttribute("zen-split-view"), - "The split view should not have crashed with two tabs in it" - ); - ok( - !gZenWorkspaces._emptyTab.splitView, - "The empty tab should not be in split view" + const staleToken = await openUrlbarSession(); + await stageEmptySplit(staleToken); + + EventUtils.synthesizeKey("KEY_Escape"); + await TestUtils.waitForCondition( + () => + !gZenWorkspaces._emptyTab.splitView && + gBrowser.selectedTab === originalTab, + "Waiting for the staged pane to be discarded" ); + ok( !gZenWorkspaces._emptyTab.group, - "The empty tab should not be in a group" + "The empty tab should not be left in a group" ); - ok(selectedTab.splitView, "The selected tab should be in split view"); - ok(originalTab.splitView, "The original tab should be in split view"); Assert.equal( - gBrowser.tabpanels.querySelectorAll('[zen-split="true"]').length, - 2, - "There should be two split views present" + gZenViewSplitter._data.findIndex(group => + group.tabs.includes(gZenWorkspaces._emptyTab) + ), + -1, + "No split group should still hold the empty tab" ); - await BrowserTestUtils.removeTab(selectedTab); + ok( + !command.hasAttribute("disabled"), + "New Empty Split View should be usable again" + ); + ok(!originalTab.splitView, "The original tab should not be in split view"); }); }); diff --git a/src/zen/tests/urlbar/browser.toml b/src/zen/tests/urlbar/browser.toml index e2c4bbb40..34557d019 100644 --- a/src/zen/tests/urlbar/browser.toml +++ b/src/zen/tests/urlbar/browser.toml @@ -12,3 +12,5 @@ support-files = [ ["browser_floating_urlbar.js"] ["browser_issue_7385.js"] + +["browser_urlbar_close_token.js"] diff --git a/src/zen/tests/urlbar/browser_urlbar_close_token.js b/src/zen/tests/urlbar/browser_urlbar_close_token.js new file mode 100644 index 000000000..1fc89e833 --- /dev/null +++ b/src/zen/tests/urlbar/browser_urlbar_close_token.js @@ -0,0 +1,266 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +async function openUrlbarSession(closeToken) { + await UrlbarTestUtils.promisePopupOpen(window, () => { + ok( + gZenUIManager.handleNewTab(false, false, "tab", true, closeToken), + "The urlbar session should have opened" + ); + }); + Assert.equal(typeof closeToken.id, "number", "The session minted a token"); + return gURLBar._zenHandleUrlbarClose; +} + +function listenerFor(closeToken, seen) { + return event => { + if (!gZenUIManager.matchesCloseToken(closeToken, event)) { + return; + } + seen.push(event.detail.closeSeq); + }; +} + +function waitForCloseOf(closeToken) { + return BrowserTestUtils.waitForEvent( + window, + "ZenURLBarClosed", + false, + event => gZenUIManager.matchesCloseToken(closeToken, event) + ); +} + +add_task(async function test_matchesCloseToken() { + ok( + gZenUIManager.matchesCloseToken({ id: 7 }, { detail: { closeSeq: 7 } }), + "Matches correct token" + ); + ok( + !gZenUIManager.matchesCloseToken({ id: 7 }, { detail: { closeSeq: 8 } }), + "Does not match incorrect token" + ); + ok( + !gZenUIManager.matchesCloseToken({}, { detail: { closeSeq: undefined } }), + "Bad token doesn't match" + ); + ok( + !gZenUIManager.matchesCloseToken({ id: 7 }, { detail: {} }), + "An untagged event is rejected" + ); + ok( + !gZenUIManager.matchesCloseToken({ id: 7 }, undefined), + "A missing event is rejected" + ); + ok( + !gZenUIManager.matchesCloseToken(undefined, { detail: { closeSeq: 7 } }), + "A missing token is rejected" + ); +}); + +add_task(async function test_Every_Call_Mints_Its_Own_Token() { + ok(gZenUIManager.testingEnabled, "Testing is enabled"); + + const seen = new Set(); + for (let i = 0; i < 3; i++) { + const closeToken = {}; + ok( + !gZenUIManager.handleNewTab(false, false, "tab", false, closeToken), + "Path bails out under zen.testing.enabled" + ); + Assert.equal( + typeof closeToken.id, + "number", + "handleNewTab always mints a token" + ); + ok(!seen.has(closeToken.id), `Token ${closeToken.id} is unique`); + seen.add(closeToken.id); + } +}); + +add_task(async function test_Overlapping_Sessions_Get_Correct_Tokens() { + await SimpleTest.promiseFocus(window); + await BrowserTestUtils.withNewTab("https://example.com", async function () { + const seenByA = []; + const seenByB = []; + const dispatched = []; + const collectAll = event => dispatched.push(event.detail.closeSeq); + window.addEventListener("ZenURLBarClosed", collectAll); + + const tokenA = {}; + const closeA = await openUrlbarSession(tokenA); + const onA = listenerFor(tokenA, seenByA); + window.addEventListener("ZenURLBarClosed", onA); + ok(gURLBar.focused, "Session A focused the urlbar"); + + const tokenB = {}; + const onB = listenerFor(tokenB, seenByB); + window.addEventListener("ZenURLBarClosed", onB); + + const aClosed = waitForCloseOf(tokenA); + closeA(true); + ok(!gURLBar._zenHandleUrlbarClose, "A's handler is cleared synchronously"); + + const closeB = await openUrlbarSession(tokenB); + Assert.notEqual(tokenA.id, tokenB.id, "The two sessions differ"); + ok(gZenUIManager._isOwner(tokenB.id), "Session B owns the urlbar"); + ok(!gZenUIManager._isOwner(tokenA.id), "Session A no longer owns it"); + + await aClosed; + + Assert.deepEqual( + dispatched, + [tokenA.id], + "The superseded session still announced its own close" + ); + Assert.deepEqual(seenByA, [tokenA.id], "A's listener accepted A's close"); + Assert.deepEqual(seenByB, [], "B's listener rejected the foreign close"); + + ok(gURLBar.view.isOpen, "The view is open for session B"); + ok(gURLBar.hasAttribute("zen-newtab"), "Session B's zen-newtab is set"); + Assert.equal( + gURLBar._zenHandleUrlbarClose, + closeB, + "Session B's close handler is still installed" + ); + + const bClosed = waitForCloseOf(tokenB); + closeB(true); + await bClosed; + await TestUtils.waitForCondition( + () => !gURLBar.view.isOpen, + "Waiting for session B to close" + ); + + Assert.deepEqual(seenByB, [tokenB.id], "B's listener accepted B's close"); + Assert.deepEqual(seenByA, [tokenA.id], "A's listener did not fire again"); + Assert.deepEqual( + dispatched, + [tokenA.id, tokenB.id], + "Both sessions announced their close, in order" + ); + ok(!gURLBar.hasAttribute("zen-newtab"), "A live close clears zen-newtab"); + ok(!gURLBar._zenHandleUrlbarClose, "A live close clears the handler"); + ok(!gZenUIManager._isOwner(tokenB.id), "Ownership is released at the end"); + + window.removeEventListener("ZenURLBarClosed", collectAll); + window.removeEventListener("ZenURLBarClosed", onA); + window.removeEventListener("ZenURLBarClosed", onB); + }); +}); + +add_task(async function test_Superseded_Before_Its_Teardown_Runs() { + await SimpleTest.promiseFocus(window); + await BrowserTestUtils.withNewTab("https://example.com", async function () { + const seenByA = []; + const seenByB = []; + const tokenA = {}; + const closeA = await openUrlbarSession(tokenA); + const onA = listenerFor(tokenA, seenByA); + window.addEventListener("ZenURLBarClosed", onA); + + const tokenB = {}; + const onB = listenerFor(tokenB, seenByB); + window.addEventListener("ZenURLBarClosed", onB); + + const aClosed = waitForCloseOf(tokenA); + closeA(true); + ok( + gZenUIManager.handleNewTab(false, false, "tab", true, tokenB), + "Session B opened before A's teardown ran" + ); + const closeB = gURLBar._zenHandleUrlbarClose; + + await aClosed; + + Assert.deepEqual( + seenByA, + [tokenA.id], + "The superseded session still announced its close" + ); + Assert.deepEqual(seenByB, [], "B's listener rejected the foreign close"); + ok( + gURLBar.hasAttribute("zen-newtab"), + "A's skipped teardown did not strip the attribute B had just set" + ); + Assert.equal( + gURLBar._zenHandleUrlbarClose, + closeB, + "A's skipped teardown left B's close handler installed" + ); + ok(gZenUIManager._isOwner(tokenB.id), "Session B still owns the urlbar"); + + window.removeEventListener("ZenURLBarClosed", onA); + window.removeEventListener("ZenURLBarClosed", onB); + closeB(true); + await TestUtils.waitForCondition( + () => !gURLBar.hasAttribute("zen-newtab") && !gURLBar.view.isOpen, + "Waiting for session B to close" + ); + }); +}); + +add_task(async function test_Interleaved_Resolves_Correctly() { + await SimpleTest.promiseFocus(window); + await BrowserTestUtils.withNewTab("https://example.com", async function () { + const seenByA = []; + const tokenA = {}; + const closeA = await openUrlbarSession(tokenA); + const onA = listenerFor(tokenA, seenByA); + window.addEventListener("ZenURLBarClosed", onA); + + const tokenB = {}; + let closeB = null; + const openBDuringClose = event => { + ok( + gZenUIManager.matchesCloseToken(tokenA, event), + "Event token matches session A token" + ); + + window.removeEventListener("ZenURLBarClosed", openBDuringClose); + ok( + !gURLBar.hasAttribute("zen-newtab"), + "A's teardown ran before session B claimed the urlbar" + ); + ok( + gZenUIManager.handleNewTab(false, false, "tab", true, tokenB), + "Session B opened before A checked its ownership" + ); + + closeB = gURLBar._zenHandleUrlbarClose; + ok(closeB, "Global close handler installed."); + }; + window.addEventListener("ZenURLBarClosed", openBDuringClose); + + const aClosed = waitForCloseOf(tokenA); + closeA(true); + + await aClosed; + + Assert.equal(typeof tokenB.id, "number", "Session B really opened"); + Assert.deepEqual(seenByA, [tokenA.id], "A announced its own close"); + ok(gURLBar.view.isOpen, "A did not close the view B had just claimed"); + ok( + gURLBar.hasAttribute("zen-newtab"), + "Session B's bar has zen-newtab restored" + ); + Assert.equal( + gURLBar._zenHandleUrlbarClose, + closeB, + "Session B's close handler survived" + ); + ok(gZenUIManager._isOwner(tokenB.id), "Session B still owns the urlbar"); + + window.removeEventListener("ZenURLBarClosed", onA); + closeB(true); + await TestUtils.waitForCondition( + () => !gURLBar.hasAttribute("zen-newtab") && !gURLBar.view.isOpen, + "Waiting for session B to close" + ); + }); +});