Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const { UrlbarTestUtils } = ChromeUtils.importESModule(
);
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 <keyword><space>.
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 <keyword><space>.
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 <keyword><space> 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();
});