From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../extensions/test/browser/browser_ext_omnibox.js | 504 +++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 browser/components/extensions/test/browser/browser_ext_omnibox.js (limited to 'browser/components/extensions/test/browser/browser_ext_omnibox.js') diff --git a/browser/components/extensions/test/browser/browser_ext_omnibox.js b/browser/components/extensions/test/browser/browser_ext_omnibox.js new file mode 100644 index 0000000000..f7c27af14d --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_omnibox.js @@ -0,0 +1,504 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +const keyword = "VeryUniqueKeywordThatDoesNeverMatchAnyTestUrl"; + +// This test does a lot. To ease debugging, we'll sometimes print the lines. +function getCallerLines() { + const lines = Array.from( + new Error().stack.split("\n").slice(1), + line => /browser_ext_omnibox.js:(\d+):\d+$/.exec(line)?.[1] + ); + return "Caller lines: " + lines.filter(lineno => lineno != null).join(", "); +} + +add_setup(async () => { + // Override default timeout of 3000 ms, to make sure that the test progresses + // reasonably quickly. See comment in "function waitForResult" below. + // In this whole test, we respond ASAP to omnibox.onInputChanged events, so + // it should be safe to choose a relatively low timeout. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.extension.omnibox.timeout", 500]], + }); +}); + +add_task(async function () { + // This keyword needs to be unique to prevent history entries from unrelated + // tests from appearing in the suggestions list. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: keyword, + }, + }, + + background: function () { + browser.omnibox.onInputStarted.addListener(() => { + browser.test.sendMessage("on-input-started-fired"); + }); + + let synchronous = true; + let suggestions = null; + let suggestCallback = null; + + browser.omnibox.onInputChanged.addListener((text, suggest) => { + if (synchronous && suggestions) { + suggest(suggestions); + } else { + suggestCallback = suggest; + } + browser.test.sendMessage("on-input-changed-fired", { text }); + }); + + browser.omnibox.onInputCancelled.addListener(() => { + browser.test.sendMessage("on-input-cancelled-fired"); + }); + + browser.omnibox.onInputEntered.addListener((text, disposition) => { + browser.test.sendMessage("on-input-entered-fired", { + text, + disposition, + }); + }); + + browser.omnibox.onDeleteSuggestion.addListener(text => { + browser.test.sendMessage("on-delete-suggestion-fired", { text }); + }); + + browser.test.onMessage.addListener((msg, data) => { + switch (msg) { + case "set-suggestions": + suggestions = data.suggestions; + browser.test.sendMessage("suggestions-set"); + break; + case "set-default-suggestion": + browser.omnibox.setDefaultSuggestion(data.suggestion); + browser.test.sendMessage("default-suggestion-set"); + break; + case "set-synchronous": + synchronous = data.synchronous; + browser.test.sendMessage("set-synchronous-set"); + break; + case "test-multiple-suggest-calls": + suggestions.forEach(suggestion => suggestCallback([suggestion])); + browser.test.sendMessage("test-ready"); + break; + case "test-suggestions-after-delay": + Promise.resolve().then(() => { + suggestCallback(suggestions); + browser.test.sendMessage("test-ready"); + }); + break; + } + }); + }, + }); + + async function expectEvent(event, expected) { + info(`Waiting for event: ${event} (${getCallerLines()})`); + let actual = await extension.awaitMessage(event); + if (!expected) { + ok(true, `Expected "${event} to have fired."`); + return; + } + if (expected.text != undefined) { + is( + actual.text, + expected.text, + `Expected "${event}" to have fired with text: "${expected.text}".` + ); + } + if (expected.disposition) { + is( + actual.disposition, + expected.disposition, + `Expected "${event}" to have fired with disposition: "${expected.disposition}".` + ); + } + } + + async function waitForResult(index) { + info(`waitForResult (${getCallerLines()})`); + // When omnibox.onInputChanged is triggered, the "startQuery" method in + // UrlbarProviderOmnibox.sys.mjs's startQuery will wait for a fixed amount + // of time before releasing the promise, which we observe by the call to + // UrlbarTestUtils here. + // + // To reduce the time that the test takes, we lower this in add_setup, by + // overriding the browser.urlbar.extension.omnibox.timeout preference. + // + // While this is not specific to the "waitForResult" test helper here, the + // issue is only observed in waitForResult because it is usually the first + // method called after observing "on-input-changed-fired". + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + + // Ensure the addition is complete, for proper mouse events on the entries. + await new Promise(resolve => + window.requestIdleCallback(resolve, { timeout: 1000 }) + ); + return result; + } + + async function promiseClickOnItem(index, details) { + // The Address Bar panel is animated and updated on a timer, thus it may not + // yet be listening to events when we try to click on it. This uses a + // polling strategy to repeat the click, if it doesn't go through. + let clicked = false; + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + index + ); + element.addEventListener( + "mousedown", + () => { + clicked = true; + }, + { once: true } + ); + while (!clicked) { + EventUtils.synthesizeMouseAtCenter(element, details); + await new Promise(r => window.requestIdleCallback(r, { timeout: 1000 })); + } + } + + let inputSessionSerial = 0; + async function startInputSession() { + gURLBar.focus(); + gURLBar.value = keyword; + EventUtils.sendString(" "); + await expectEvent("on-input-started-fired"); + // Always use a different input at every invokation, so that + // waitForResult can distinguish different cases. + let char = (inputSessionSerial++ % 10).toString(); + EventUtils.sendString(char); + + await expectEvent("on-input-changed-fired", { text: char }); + return char; + } + + async function testInputEvents() { + gURLBar.focus(); + + // Start an input session by typing in . + EventUtils.sendString(keyword + " "); + await expectEvent("on-input-started-fired"); + + // Test canceling the input before any changed events fire. + EventUtils.synthesizeKey("KEY_Backspace"); + await expectEvent("on-input-cancelled-fired"); + + EventUtils.sendString(" "); + await expectEvent("on-input-started-fired"); + + // Test submitting the input before any changed events fire. + EventUtils.synthesizeKey("KEY_Enter"); + await expectEvent("on-input-entered-fired"); + + gURLBar.focus(); + + // Start an input session by typing in . + EventUtils.sendString(keyword + " "); + await expectEvent("on-input-started-fired"); + + // We should expect input changed events now that the keyword is active. + EventUtils.sendString("b"); + await expectEvent("on-input-changed-fired", { text: "b" }); + + EventUtils.sendString("c"); + await expectEvent("on-input-changed-fired", { text: "bc" }); + + EventUtils.synthesizeKey("KEY_Backspace"); + await expectEvent("on-input-changed-fired", { text: "b" }); + + // Even though the input is We should not expect an + // input started event to fire since the keyword is active. + EventUtils.synthesizeKey("KEY_Backspace"); + await expectEvent("on-input-changed-fired", { text: "" }); + + // Make the keyword inactive by hitting backspace. + EventUtils.synthesizeKey("KEY_Backspace"); + await expectEvent("on-input-cancelled-fired"); + + // Activate the keyword by typing a space. + // Expect onInputStarted to fire. + EventUtils.sendString(" "); + await expectEvent("on-input-started-fired"); + + // onInputChanged should fire even if a space is entered. + EventUtils.sendString(" "); + await expectEvent("on-input-changed-fired", { text: " " }); + + // The active session should cancel if the input blurs. + gURLBar.blur(); + await expectEvent("on-input-cancelled-fired"); + } + + async function testSuggestionDeletion() { + extension.sendMessage("set-suggestions", { + suggestions: [{ content: "a", description: "select a", deletable: true }], + }); + await extension.awaitMessage("suggestions-set"); + + gURLBar.focus(); + + EventUtils.sendString(keyword); + EventUtils.sendString(" select a"); + + await expectEvent("on-input-changed-fired"); + + // Select the suggestion + await EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Delete the suggestion + await EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + + await expectEvent("on-delete-suggestion-fired", { text: "select a" }); + } + + async function testHeuristicResult(expectedText, setDefaultSuggestion) { + if (setDefaultSuggestion) { + extension.sendMessage("set-default-suggestion", { + suggestion: { + description: expectedText, + }, + }); + await extension.awaitMessage("default-suggestion-set"); + } + + let text = await startInputSession(); + let result = await waitForResult(0); + + Assert.equal( + result.displayed.title, + expectedText, + `Expected heuristic result to have title: "${expectedText}".` + ); + + Assert.equal( + result.displayed.action, + `${keyword} ${text}`, + `Expected heuristic result to have displayurl: "${keyword} ${text}".` + ); + + let promiseEvent = expectEvent("on-input-entered-fired", { + text, + disposition: "currentTab", + }); + await promiseClickOnItem(0, {}); + await promiseEvent; + } + + async function testDisposition( + suggestionIndex, + expectedDisposition, + expectedText + ) { + await startInputSession(); + await waitForResult(suggestionIndex); + + // Select the suggestion. + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: suggestionIndex }); + + let promiseEvent = expectEvent("on-input-entered-fired", { + text: expectedText, + disposition: expectedDisposition, + }); + + if (expectedDisposition == "currentTab") { + await promiseClickOnItem(suggestionIndex, {}); + } else if (expectedDisposition == "newForegroundTab") { + await promiseClickOnItem(suggestionIndex, { accelKey: true }); + } else if (expectedDisposition == "newBackgroundTab") { + await promiseClickOnItem(suggestionIndex, { + shiftKey: true, + accelKey: true, + }); + } + await promiseEvent; + } + + async function testSuggestions(info) { + extension.sendMessage("set-synchronous", { synchronous: false }); + await extension.awaitMessage("set-synchronous-set"); + + let text = await startInputSession(); + + extension.sendMessage(info.test); + await extension.awaitMessage("test-ready"); + + await waitForResult(info.suggestions.length - 1); + // Skip the heuristic result. + let index = 1; + for (let { content, description } of info.suggestions) { + let item = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal( + item.displayed.title, + description, + `Expected suggestion to have title: "${description}".` + ); + Assert.equal( + item.displayed.action, + `${keyword} ${content}`, + `Expected suggestion to have displayurl: "${keyword} ${content}".` + ); + index++; + } + + let promiseEvent = expectEvent("on-input-entered-fired", { + text, + disposition: "currentTab", + }); + await promiseClickOnItem(0, {}); + await promiseEvent; + } + + await extension.startup(); + + await SimpleTest.promiseFocus(window); + + await testInputEvents(); + + await testSuggestionDeletion(); + + // Test the heuristic result with default suggestions. + await testHeuristicResult( + "Generated extension", + false /* setDefaultSuggestion */ + ); + await testHeuristicResult("hello world", true /* setDefaultSuggestion */); + await testHeuristicResult("foo bar", true /* setDefaultSuggestion */); + + let suggestions = [ + { content: "a", description: "select a" }, + { content: "b", description: "select b" }, + { content: "c", description: "select c" }, + ]; + + extension.sendMessage("set-suggestions", { suggestions }); + await extension.awaitMessage("suggestions-set"); + + // Test each suggestion and search disposition. + await testDisposition(1, "currentTab", suggestions[0].content); + await testDisposition(2, "newForegroundTab", suggestions[1].content); + await testDisposition(3, "newBackgroundTab", suggestions[2].content); + + extension.sendMessage("set-suggestions", { suggestions }); + await extension.awaitMessage("suggestions-set"); + + // Test adding suggestions asynchronously. + await testSuggestions({ + test: "test-multiple-suggest-calls", + suggestions, + }); + await testSuggestions({ + test: "test-suggestions-after-delay", + suggestions, + }); + + // When we're the first task to be added, `waitForExplicitFinish()` may not have + // been called yet. Let's just do that, otherwise the `monitorConsole` will make + // the test fail with a failing assertion. + SimpleTest.waitForExplicitFinish(); + // Start monitoring the console. + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [ + { + message: new RegExp( + `The keyword provided is already registered: "${keyword}"` + ), + }, + ]); + }); + + // Try registering another extension with the same keyword + let extension2 = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: keyword, + }, + }, + }); + + await extension2.startup(); + + // Stop monitoring the console and confirm the correct errors are logged. + SimpleTest.endMonitorConsole(); + await waitForConsole; + + await extension2.unload(); + await extension.unload(); +}); + +add_task(async function test_omnibox_event_page() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.eventPages.enabled", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: "eventpage@omnibox" } }, + omnibox: { + keyword: keyword, + }, + background: { persistent: false }, + }, + background() { + browser.omnibox.onInputStarted.addListener(() => { + browser.test.sendMessage("onInputStarted"); + }); + browser.omnibox.onInputEntered.addListener(() => {}); + browser.omnibox.onInputChanged.addListener(() => {}); + browser.omnibox.onInputCancelled.addListener(() => {}); + browser.omnibox.onDeleteSuggestion.addListener(() => {}); + browser.test.sendMessage("ready"); + }, + }); + + const EVENTS = [ + "onInputStarted", + "onInputEntered", + "onInputChanged", + "onInputCancelled", + "onDeleteSuggestion", + ]; + + await extension.startup(); + await extension.awaitMessage("ready"); + for (let event of EVENTS) { + assertPersistentListeners(extension, "omnibox", event, { + primed: false, + }); + } + + // test events waken background + await extension.terminateBackground(); + for (let event of EVENTS) { + assertPersistentListeners(extension, "omnibox", event, { + primed: true, + }); + } + + // Activate the keyword by typing a space. + // Expect onInputStarted to fire. + gURLBar.focus(); + gURLBar.value = keyword; + EventUtils.sendString(" "); + + await extension.awaitMessage("ready"); + await extension.awaitMessage("onInputStarted"); + ok(true, "persistent event woke background"); + for (let event of EVENTS) { + assertPersistentListeners(extension, "omnibox", event, { + primed: false, + }); + } + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); -- cgit v1.2.3