gh-15115: Tests for urlbar close and split view (gh-15118)

Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
This commit is contained in:
Nikola Hristov
2026-08-28 00:45:37 +03:00
committed by GitHub
parent 4746d36699
commit 8df6fef1ac
3 changed files with 402 additions and 35 deletions

View File

@@ -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<object>} 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");
});
});

View File

@@ -12,3 +12,5 @@ support-files = [
["browser_floating_urlbar.js"]
["browser_issue_7385.js"]
["browser_urlbar_close_token.js"]

View File

@@ -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"
);
});
});