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:
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));
+ }
+}