tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit ca2ec1f19a8ef81e2c5ec6b637e241b40468adfa
parent 720bc69d6efaf36bd3e6f49d56a946cd5905e18a
Author: Daisuke Akatsuka <daisuke@birchill.co.jp>
Date:   Fri, 14 Nov 2025 03:19:54 +0000

Bug 1997535: Fix wrong needsNewContent calculation r=adw

Differential Revision: https://phabricator.services.mozilla.com/D271767

Diffstat:
Mbrowser/components/urlbar/UrlbarView.sys.mjs | 180++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mbrowser/components/urlbar/tests/UrlbarTestUtils.sys.mjs | 14++++++++++++++
Mbrowser/components/urlbar/tests/browser/browser.toml | 2++
Abrowser/components/urlbar/tests/browser/browser_view_reusable.js | 796+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 949 insertions(+), 43 deletions(-)

diff --git a/browser/components/urlbar/UrlbarView.sys.mjs b/browser/components/urlbar/UrlbarView.sys.mjs @@ -1708,19 +1708,64 @@ export class UrlbarView { item._elements.set("bottom", bottom); } - #addRowButtons(item, result) { - let container = this.#createElement("div"); - container.className = "urlbarView-row-buttons"; - item._elements.set("buttons", container); - item.appendChild(container); + #needsNewButtons(item, oldResult, newResult) { + if (!oldResult) { + return true; + } + if ( + !!this.#getResultMenuCommands(newResult) != + item._buttons.has("result-menu") + ) { + return true; + } + + if (!!oldResult.showFeedbackMenu != !!newResult.showFeedbackMenu) { + return true; + } + + if ( + oldResult.payload.buttons?.length != newResult.payload.buttons?.length || + !lazy.ObjectUtils.deepEqual( + oldResult.payload.buttons, + newResult.payload.buttons + ) + ) { + return true; + } + + return newResult.testForceNewContent; + } + + #updateRowButtons(item, oldResult, result) { for (let i = 0; i < result.payload.buttons?.length; i++) { - let button = result.payload.buttons[i]; // We hold the name to each button data in payload to enable to get the // data from button element by the name. This name is mainly used for // button that has menu (Split Button). + let button = result.payload.buttons[i]; button.name ??= i.toString(); - this.#addRowButton(item, button); + } + + if (!this.#needsNewButtons(item, oldResult, result)) { + return; + } + + let container = item._elements.get("buttons"); + if (container) { + container.innerHTML = ""; + } else { + container = this.#createElement("div"); + container.className = "urlbarView-row-buttons"; + item.appendChild(container); + item._elements.set("buttons", container); + } + + item._buttons.clear(); + + if (result.payload.buttons) { + for (let button of result.payload.buttons) { + this.#addRowButton(item, button); + } } // TODO: `buttonText` is intended only for WebExtensions. We should remove @@ -1862,52 +1907,94 @@ export class UrlbarView { return actionContainer; } + #needsNewContent(item, oldResult, newResult) { + if (!oldResult) { + return true; + } + + if ( + (oldResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) != + (newResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) + ) { + return true; + } + + if ( + oldResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC && + newResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC && + oldResult.payload.dynamicType != newResult.payload.dynamicType + ) { + return true; + } + + if (oldResult.isRichSuggestion != newResult.isRichSuggestion) { + return true; + } + + // Reusing a non-heuristic as a heuristic is risky as it may have DOM + // nodes/attributes/classes that are normally not present in a heuristic + // result. This may happen for example when switching from a zero-prefix + // search not having a heuristic to a search string one. + if (oldResult.heuristic != newResult.heuristic) { + return true; + } + + // Container switch-tab results have a more complex DOM content that is + // only updated correctly by another switch-tab result. + if ( + oldResult.type == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH && + newResult.type != oldResult.type && + lazy.UrlbarProviderOpenTabs.isContainerUserContextId( + oldResult.payload.userContextId + ) + ) { + return true; + } + + if ( + newResult.providerName == lazy.UrlbarProviderQuickSuggest.name && + // Check if the `RESULT_TYPE` is `DYNAMIC` because otherwise the + // `suggestionType` and `items` checks aren't relevant. + newResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC && + (oldResult.payload.suggestionType != newResult.payload.suggestionType || + oldResult.payload.items?.length != newResult.payload.items?.length) + ) { + return true; + } + + if (newResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) { + if (oldResult.providerName != newResult.providerName) { + return true; + } + + let provider = this.#providersManager.getProvider(newResult.providerName); + if ( + !lazy.ObjectUtils.deepEqual( + provider.getViewTemplate?.(oldResult), + provider.getViewTemplate?.(newResult) + ) + ) { + return true; + } + } + + return newResult.testForceNewContent; + } + // eslint-disable-next-line complexity #updateRow(item, result) { let oldResult = item.result; - let oldResultType = item.result?.type; - let provider = this.#providersManager.getProvider(result.providerName); item.result = result; item.removeAttribute("stale"); item.id = getUniqueId("urlbarView-row-"); - let needsNewContent = - oldResultType === undefined || - (oldResultType == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) != - (result.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) || - (oldResultType == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC && - result.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC && - oldResult.payload.dynamicType != result.payload.dynamicType) || - // Dynamic results that implement getViewTemplate will - // always need updating. - provider?.getViewTemplate || - oldResult.isRichSuggestion != result.isRichSuggestion || - !!this.#getResultMenuCommands(result) != item._buttons.has("menu") || - !!oldResult.showFeedbackMenu != !!result.showFeedbackMenu || - !lazy.ObjectUtils.deepEqual( - oldResult.payload.buttons, - result.payload.buttons - ) || - // Reusing a non-heuristic as a heuristic is risky as it may have DOM - // nodes/attributes/classes that are normally not present in a heuristic - // result. This may happen for example when switching from a zero-prefix - // search not having a heuristic to a search string one. - result.heuristic != oldResult.heuristic || - // Container switch-tab results have a more complex DOM content that is - // only updated correctly by another switch-tab result. - (oldResultType == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH && - lazy.UrlbarProviderOpenTabs.isContainerUserContextId( - oldResult.payload.userContextId - ) && - result.type != oldResultType) || - result.testForceNewContent; - - if (needsNewContent) { + if (this.#needsNewContent(item, oldResult, result)) { + // Recreate the row content except the buttons, which we'll reuse below. + let buttons = item._elements.get("buttons"); while (item.lastChild) { item.lastChild.remove(); } item._elements.clear(); - item._buttons.clear(); item._content = this.#createElement("span"); item._content.className = "urlbarView-row-inner"; item.appendChild(item._content); @@ -1930,8 +2017,15 @@ export class UrlbarView { } else { this.#createRowContent(item, result); } - this.#addRowButtons(item, result); + + if (buttons) { + item.appendChild(buttons); + item._elements.set("buttons", buttons); + } } + + this.#updateRowButtons(item, oldResult, result); + item._content.id = item.id + "-inner"; let isFirstChild = item === this.#rows.children[0]; diff --git a/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs @@ -1588,6 +1588,10 @@ class TestProvider extends UrlbarProvider { * If non-zero, each result will be added on this timeout. If zero, all * results will be added immediately and synchronously. * If there's no results, the query will be completed after this timeout. + * @param {Function} [options.getViewTemplate] + * If given, override the UrlbarProvider.getViewTemplate(). + * @param {Function} [options.getViewUpdate] + * If given, override the UrlbarProvider.getViewUpdate(). * @param {Function} [options.onCancel] * If given, a function that will be called when the provider's cancelQuery * method is called. @@ -1613,6 +1617,8 @@ class TestProvider extends UrlbarProvider { type = UrlbarUtils.PROVIDER_TYPE.PROFILE, priority = 0, addTimeout = 0, + getViewTemplate = null, + getViewUpdate = null, onCancel = null, onSelection = null, onEngagement = null, @@ -1642,6 +1648,14 @@ class TestProvider extends UrlbarProvider { this._type = UrlbarUtils.PROVIDER_TYPE.HEURISTIC; } + if (getViewTemplate) { + this.getViewTemplate = getViewTemplate.bind(this); + } + + if (getViewUpdate) { + this.getViewUpdate = getViewUpdate.bind(this); + } + if (onEngagement) { this.onEngagement = onEngagement.bind(this); } diff --git a/browser/components/urlbar/tests/browser/browser.toml b/browser/components/urlbar/tests/browser/browser.toml @@ -814,6 +814,8 @@ support-files = [ "searchSuggestionEngine.sjs", ] +["browser_view_reusable.js"] + ["browser_view_selectionByMouse.js"] skip-if = ["os == 'linux' && os_version == '18.04' && processor == 'x86_64' && asan"] # Bug 1789051 diff --git a/browser/components/urlbar/tests/browser/browser_view_reusable.js b/browser/components/urlbar/tests/browser/browser_view_reusable.js @@ -0,0 +1,796 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the elements for results are reusable. + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderQuickSuggest: + "moz-src:///browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs", +}); + +const SIMPLE_GET_VIEW_TEMPLATE = () => { + return { + children: [ + { + name: "text", + tag: "span", + }, + ], + }; +}; + +const SIMPLE_GET_VIEW_UPDATE = result => { + return { + text: { + textContent: result.payload.value, + }, + }; +}; + +add_setup(async function setup() { + let originals = UrlbarProvidersManager.providers; + UrlbarProvidersManager.providers = []; + registerCleanupFunction(async function () { + UrlbarProvidersManager.providers = originals; + }); +}); + +add_task(async function provider() { + const TEST_DATA = [ + { + first: "SameTestProvider", + second: "SameTestProvider", + expectedReused: true, + }, + { + first: "FirstTestProvider", + second: "SecondTestProvider", + expectedReused: false, + }, + ]; + + for (let { first, second, expectedReused } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: first, + results: [ + makeUrlResult({ + payload: { + url: "https://example.com/first", + title: "first example", + }, + }), + ], + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: second, + results: [ + makeUrlResult({ + payload: { + url: "https://example.com/second", + title: "second example", + }, + }), + ], + }), + expectedReused, + }); + } +}); + +add_task(async function isRichSuggestion() { + const TEST_DATA = [ + { + first: true, + second: true, + expectedReused: true, + }, + { + first: true, + second: false, + expectedReused: false, + }, + ]; + + for (let { first, second, expectedReused } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + isRichSuggestion: first, + payload: { + url: "https://example.com/first", + title: "first example", + }, + }), + ], + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + isRichSuggestion: second, + payload: { + url: "https://example.com/second", + title: "second example", + }, + }), + ], + }), + expectedReused, + }); + } +}); + +add_task(async function heuristic() { + const TEST_DATA = [ + { + first: true, + second: true, + expectedReused: true, + }, + { + first: true, + second: false, + expectedReused: false, + }, + ]; + + for (let { first, second, expectedReused } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + heuristic: first, + payload: { + url: "https://example.com/first", + title: "first example", + }, + }), + ], + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + heuristic: second, + payload: { + url: "https://example.com/second", + title: "second example", + }, + }), + ], + }), + expectedReused, + }); + } +}); + +add_task(async function result_menu() { + const TEST_DATA = [ + { + first: true, + second: true, + expectedReused: true, + expectedButtons: { + first: ["result-menu"], + second: ["result-menu"], + }, + }, + { + first: true, + second: false, + expectedReused: false, + expectedButtons: { + first: ["result-menu"], + second: [], + }, + }, + ]; + + for (let { first, second, expectedReused, expectedButtons } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + payload: { + isManageable: first, + url: "https://example.com/first", + title: "first example", + }, + }), + ], + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + payload: { + isBlockable: second, + url: "https://example.com/second", + title: "second example", + }, + }), + ], + }), + isButtonTest: true, + expectedReused, + expectedButtons, + }); + } +}); + +add_task(async function showFeedbackMenu() { + const TEST_DATA = [ + { + first: true, + second: true, + expectedReused: true, + expectedButtons: { + first: ["result-menu"], + second: ["result-menu"], + }, + }, + { + first: true, + second: false, + expectedReused: false, + expectedButtons: { + first: ["result-menu"], + second: ["result-menu"], + }, + }, + ]; + + for (let { first, second, expectedReused, expectedButtons } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + showFeedbackMenu: first, + payload: { + isBlockable: true, + url: "https://example.com/first", + title: "first example", + }, + }), + ], + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + showFeedbackMenu: second, + payload: { + isBlockable: true, + url: "https://example.com/second", + title: "second example", + }, + }), + ], + }), + expectedReused, + expectedButtons, + }); + } +}); + +add_task(async function buttons() { + const TEST_DATA = [ + { + first: [ + { + l10n: { id: "urlbar-search-tips-confirm" }, + }, + { + l10n: { id: "urlbar-search-mode-bookmarks" }, + }, + ], + second: [ + { + l10n: { id: "urlbar-search-tips-confirm" }, + }, + { + l10n: { id: "urlbar-search-mode-bookmarks" }, + }, + ], + expectedReused: true, + expectedButtons: { + first: ["0", "1"], + second: ["0", "1"], + }, + }, + { + first: [ + { + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + second: [ + { + l10n: { id: "urlbar-search-mode-bookmarks" }, + }, + ], + expectedReused: false, + expectedButtons: { + first: ["0"], + second: ["0"], + }, + }, + { + first: [ + { + l10n: { id: "urlbar-search-tips-confirm" }, + }, + { + l10n: { id: "urlbar-search-mode-bookmarks" }, + }, + ], + second: [ + { + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + expectedReused: false, + expectedButtons: { + first: ["0", "1"], + second: ["0"], + }, + }, + ]; + + for (let { first, second, expectedReused, expectedButtons } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeTipResult({ + payload: { + buttons: first, + type: "test", + }, + }), + ], + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeTipResult({ + payload: { + buttons: second, + type: "test", + }, + }), + ], + }), + expectedReused, + expectedButtons, + }); + } +}); + +add_task(async function switchTab() { + const TEST_DATA = [ + { + first: UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + second: UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + expectedReused: true, + }, + { + first: UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + second: UrlbarUtils.RESULT_TYPE.URL, + expectedReused: false, + }, + ]; + + for (let { first, second, expectedReused } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + type: first, + payload: { + userContextId: 1, + url: "https://example.com/first", + title: "first example", + }, + }), + ], + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + type: second, + payload: { + url: "https://example.com/second", + title: "second example", + }, + }), + ], + }), + expectedReused, + }); + } +}); + +add_task(async function dynamic_vs_not_dynamic() { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeDynamicResult({ + payload: { + dynamicType: "testDynamic", + value: "first provider", + }, + }), + ], + getViewTemplate: SIMPLE_GET_VIEW_TEMPLATE, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeUrlResult({ + payload: { + url: "https://example.com/second", + title: "second example", + }, + }), + ], + }), + expectedReused: false, + }); +}); + +add_task(async function dynamic_dynamicType() { + const TEST_DATA = [ + { + first: "same_type", + second: "same_type", + expectedReused: true, + }, + { + first: "first_type", + second: "second_type", + expectedReused: false, + }, + ]; + + for (let { first, second, expectedReused } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeDynamicResult({ + payload: { + dynamicType: first, + value: "first provider", + }, + }), + ], + getViewTemplate: SIMPLE_GET_VIEW_TEMPLATE, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeDynamicResult({ + payload: { + dynamicType: second, + value: "second provider", + }, + }), + ], + getViewTemplate: SIMPLE_GET_VIEW_TEMPLATE, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + expectedReused, + }); + } +}); + +add_task(async function dynamic_template() { + const TEST_DATA = [ + { + first: { + children: [ + { + name: "text", + tag: "span", + }, + ], + }, + second: { + children: [ + { + name: "text", + tag: "span", + }, + ], + }, + expectedReused: true, + }, + { + first: { + children: [ + { + name: "text", + tag: "span", + }, + ], + }, + second: { + children: [ + { + name: "text", + tag: "div", + }, + ], + }, + expectedReused: false, + }, + ]; + + for (let { first, second, expectedReused } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeDynamicResult({ + payload: { + dynamicType: "testDynamic", + value: "first provider", + template: first, + }, + }), + ], + getViewTemplate: result => result.payload.template, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: "TestProvider", + results: [ + makeDynamicResult({ + payload: { + dynamicType: "testDynamic", + value: "second provider", + template: second, + }, + }), + ], + getViewTemplate: result => result.payload.template, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + expectedReused, + }); + } +}); + +add_task(async function quickSuggest_suggestionType() { + const TEST_DATA = [ + { + first: "same_type", + second: "same_type", + expectedReused: true, + }, + { + first: "first_type", + second: "second_type", + expectedReused: false, + }, + ]; + + for (let { first, second, expectedReused } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: UrlbarProviderQuickSuggest.name, + results: [ + makeDynamicResult({ + payload: { + dynamicType: "testDynamic", + suggestionType: first, + value: "first provider", + }, + }), + ], + getViewTemplate: SIMPLE_GET_VIEW_TEMPLATE, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: UrlbarProviderQuickSuggest.name, + results: [ + makeDynamicResult({ + payload: { + dynamicType: "testDynamic", + suggestionType: second, + value: "second provider", + }, + }), + ], + getViewTemplate: SIMPLE_GET_VIEW_TEMPLATE, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + expectedReused, + }); + } +}); + +add_task(async function quickSuggest_items() { + const TEST_DATA = [ + { + first: [1, 2], + second: ["one", "two"], + expectedReused: true, + }, + { + first: [1, 2], + second: ["one", "two", "three"], + expectedReused: false, + }, + ]; + + for (let { first, second, expectedReused } of TEST_DATA) { + await doTest({ + firstProvider: new UrlbarTestUtils.TestProvider({ + name: UrlbarProviderQuickSuggest.name, + results: [ + makeDynamicResult({ + payload: { + dynamicType: "testDynamic", + items: first, + value: "first provider", + }, + }), + ], + getViewTemplate: SIMPLE_GET_VIEW_TEMPLATE, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + secondProvider: new UrlbarTestUtils.TestProvider({ + name: UrlbarProviderQuickSuggest.name, + results: [ + makeDynamicResult({ + payload: { + dynamicType: "testDynamic", + items: second, + value: "second provider", + }, + }), + ], + getViewTemplate: SIMPLE_GET_VIEW_TEMPLATE, + getViewUpdate: SIMPLE_GET_VIEW_UPDATE, + }), + expectedReused, + }); + } +}); + +function makeDynamicResult(override) { + return new UrlbarResult({ + type: UrlbarUtils.RESULT_TYPE.DYNAMIC, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + suggestedIndex: 0, + ...override, + }); +} + +function makeUrlResult(override) { + return new UrlbarResult({ + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + suggestedIndex: 0, + ...override, + }); +} + +function makeTipResult(override) { + return new UrlbarResult({ + type: UrlbarUtils.RESULT_TYPE.TIP, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + suggestedIndex: 0, + ...override, + }); +} + +async function doTest({ + firstProvider, + secondProvider, + expectedReused, + expectedButtons = null, +}) { + info("Show the results of first provider"); + UrlbarProvidersManager.registerProvider(firstProvider); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "any", + }); + + info("Hold the row element and its _elements"); + let { row: firstShownRow } = ( + await UrlbarTestUtils.getDetailsOfResultAt(window, 0) + ).element; + let firstShownContent = firstShownRow._content; + let firstShownButtons = new Map(firstShownRow._buttons); + let firstShownButtonsElement = firstShownRow._elements.get("buttons"); + let firstShownButtonsFirstElement = + firstShownButtonsElement?.firstElementChild; + UrlbarProvidersManager.unregisterProvider(firstProvider); + if (expectedButtons) { + info("Sanity check for buttons"); + assertButtons( + firstShownButtons, + firstShownButtonsElement, + expectedButtons.first + ); + } + + info("Show the results of second provider"); + UrlbarProvidersManager.registerProvider(secondProvider); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "any", + }); + + info("Check that the element is reused"); + let { row: secondShownRow } = ( + await UrlbarTestUtils.getDetailsOfResultAt(window, 0) + ).element; + + info("Assert results"); + let isContentReused = + firstShownRow == secondShownRow && + firstShownContent == secondShownRow._content && + firstShownContent.firstElementChild == + secondShownRow._content.firstElementChild; + + if (expectedButtons) { + info("Assert buttons"); + Assert.ok( + isContentReused, + "The content element should be reused if the changes was only buttons" + ); + + let secondShownButtons = secondShownRow._buttons; + let secondShownButtonsElement = secondShownRow._elements.get("buttons"); + let secondShownButtonsFirstElement = + secondShownButtonsElement.firstElementChild; + Assert.equal( + firstShownButtonsFirstElement == secondShownButtonsFirstElement, + expectedReused, + "Check whether the buttons element is reused or not" + ); + + assertButtons( + secondShownButtons, + secondShownButtonsElement, + expectedButtons.second + ); + } else { + info("Assert content"); + Assert.equal( + isContentReused, + expectedReused, + "Check whether the content element is reused or not" + ); + } + + UrlbarProvidersManager.unregisterProvider(secondProvider); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +function assertButtons(buttonsMap, buttonsElement, expected) { + Assert.equal([...buttonsMap.keys()].length, expected.length); + for (let name of expected) { + let button = buttonsMap.get(name); + Assert.ok(button); + Assert.ok(buttonsElement.contains(button)); + } +}