--- a/browser/base/content/test/performance/browser_urlbar_keyed_search_reflows.js
+++ b/browser/base/content/test/performance/browser_urlbar_keyed_search_reflows.js
@@ -40,16 +40,17 @@ const EXPECTED_REFLOWS_FIRST_OPEN = [
times: 5, // This number should only ever go down - never up.
},
{
stack: [
"adjustHeight@chrome://global/content/bindings/autocomplete.xml",
"_invalidate/this._adjustHeightTimeout<@chrome://global/content/bindings/autocomplete.xml",
],
+ minTimes: 39, // This number should only ever go down - never up.
times: 51, // This number should only ever go down - never up.
},
{
stack: [
"_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
"handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
"_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
--- a/browser/base/content/test/performance/browser_urlbar_search_reflows.js
+++ b/browser/base/content/test/performance/browser_urlbar_search_reflows.js
@@ -52,17 +52,17 @@ const EXPECTED_REFLOWS_FIRST_OPEN = [
stack: [
"_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
"handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
"_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
"_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
"_invalidate@chrome://global/content/bindings/autocomplete.xml",
"invalidate@chrome://global/content/bindings/autocomplete.xml"
],
- times: 60, // This number should only ever go down - never up.
+ times: 36, // This number should only ever go down - never up.
},
{
stack: [
"_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
"handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
"_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
"openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
@@ -97,16 +97,28 @@ const EXPECTED_REFLOWS_SECOND_OPEN = [
{
stack: [
"adjustHeight@chrome://global/content/bindings/autocomplete.xml",
"_invalidate/this._adjustHeightTimeout<@chrome://global/content/bindings/autocomplete.xml",
],
times: 3, // This number should only ever go down - never up.
},
+ {
+ stack: [
+ "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
+ "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
+ "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
+ "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
+ "_invalidate@chrome://global/content/bindings/autocomplete.xml",
+ "invalidate@chrome://global/content/bindings/autocomplete.xml"
+ ],
+ times: 24, // This number should only ever go down - never up.
+ },
+
// Bug 1359989
{
stack: [
"openPopup@chrome://global/content/bindings/popup.xml",
"_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
"openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
"openPopup@chrome://global/content/bindings/autocomplete.xml",
"set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
--- a/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
+++ b/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
@@ -31,16 +31,17 @@ add_task(async function init() {
});
// Keys up and down through the history panel, i.e., the panel that's shown when
// there's no text in the textbox.
add_task(async function history() {
gURLBar.focus();
EventUtils.synthesizeKey("VK_DOWN", {});
await promisePopupShown(gURLBar.popup);
+ await waitForAutocompleteResultAt(gMaxResults - 1)
assertState(-1, -1, "");
// Key down through each result.
for (let i = 0; i < gMaxResults; i++) {
EventUtils.synthesizeKey("VK_DOWN", {});
assertState(i, -1,
"example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
@@ -101,17 +102,17 @@ add_task(async function history() {
// Keys up and down through the non-history panel, i.e., the panel that's shown
// when you type something in the textbox.
add_task(async function() {
// Use a typed value that returns the visits added above but that doesn't
// trigger autofill since that would complicate the test.
let typedValue = "browser_urlbarOneOffs";
await promiseAutocompleteResultPopup(typedValue, window, true);
-
+ await waitForAutocompleteResultAt(gMaxResults - 1);
assertState(0, -1, typedValue);
// Key down through each result. The first result is already selected, which
// is why gMaxResults - 1 is the correct number of times to do this.
for (let i = 0; i < gMaxResults - 1; i++) {
EventUtils.synthesizeKey("VK_DOWN", {});
// i starts at zero so that the textValue passed to assertState is correct.
// But that means that i + 1 is the expected selected index, since initially
@@ -153,17 +154,17 @@ add_task(async function() {
await hidePopup();
});
// Checks that "Search with Current Search Engine" items are updated to "Search
// with One-Off Engine" when a one-off is selected.
add_task(async function searchWith() {
let typedValue = "foo";
await promiseAutocompleteResultPopup(typedValue);
-
+ await waitForAutocompleteResultAt(0);
assertState(0, -1, typedValue);
let item = gURLBar.popup.richlistbox.firstChild;
Assert.equal(item._actionText.textContent,
"Search with " + Services.search.currentEngine.name,
"Sanity check: first result's action text");
// Alt+Down to the first one-off. Now the first result and the first one-off
@@ -185,17 +186,17 @@ add_task(async function searchWith() {
// Clicks a one-off.
add_task(async function oneOffClick() {
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
// We are explicitly using something that looks like a url, to make the test
// stricter. Even if it looks like a url, we should search.
let typedValue = "foo.bar";
await promiseAutocompleteResultPopup(typedValue);
-
+ await waitForAutocompleteResultAt(1);
assertState(0, -1, typedValue);
let oneOffs = gURLBar.popup.oneOffSearchButtons.getSelectableButtons(true);
let resultsPromise =
BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false,
"http://mochi.test:8888/?terms=foo.bar");
EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
await resultsPromise;
@@ -206,17 +207,17 @@ add_task(async function oneOffClick() {
// Presses the Return key when a one-off is selected.
add_task(async function oneOffReturn() {
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
// We are explicitly using something that looks like a url, to make the test
// stricter. Even if it looks like a url, we should search.
let typedValue = "foo.bar";
await promiseAutocompleteResultPopup(typedValue, window, true);
-
+ await waitForAutocompleteResultAt(1);
assertState(0, -1, typedValue);
// Alt+Down to select the first one-off.
EventUtils.synthesizeKey("VK_DOWN", { altKey: true });
assertState(0, 0, typedValue);
let resultsPromise =
BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false,
--- a/browser/base/content/test/urlbar/head.js
+++ b/browser/base/content/test/urlbar/head.js
@@ -315,10 +315,12 @@ function promiseSpeculativeConnection(ht
}
async function waitForAutocompleteResultAt(index) {
let searchString = gURLBar.controller.searchString;
await BrowserTestUtils.waitForCondition(
() => gURLBar.popup.richlistbox.children.length > index &&
gURLBar.popup.richlistbox.children[index].getAttribute("ac-text") == searchString,
`Waiting for the autocomplete result for "${searchString}" at [${index}] to appear`);
+ // Ensure the addition is complete, for proper mouse events on the entries.
+ await new Promise(resolve => window.requestIdleCallback(resolve, {timeout: 1000}));
return gURLBar.popup.richlistbox.children[index];
}
--- a/browser/components/extensions/test/browser/browser_ext_omnibox.js
+++ b/browser/components/extensions/test/browser/browser_ext_omnibox.js
@@ -79,31 +79,45 @@ add_task(async function() {
`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 startInputSession() {
+ async function waitForAutocompleteResultAt(index) {
+ let searchString = gURLBar.controller.searchString;
+ await BrowserTestUtils.waitForCondition(
+ () => gURLBar.popup.richlistbox.children.length > index &&
+ gURLBar.popup.richlistbox.children[index].getAttribute("ac-text") == searchString,
+ `Waiting for the autocomplete result for "${searchString}" at [${index}] to appear`);
+ // Ensure the addition is complete, for proper mouse events on the entries.
+ await new Promise(resolve => window.requestIdleCallback(resolve, {timeout: 1000}));
+ return gURLBar.popup.richlistbox.children[index];
+ }
+
+ let inputSessionSerial = 0;
+ async function startInputSession(indexToWaitFor) {
gURLBar.focus();
gURLBar.value = keyword;
EventUtils.synthesizeKey(" ", {});
await expectEvent("on-input-started-fired");
- EventUtils.synthesizeKey("t", {});
- await expectEvent("on-input-changed-fired", {text: "t"});
+ // Always use a different input at every invokation, so that
+ // waitForAutocompleteResultAt can distinguish different cases.
+ let char = ((inputSessionSerial++) % 10).toString();
+ EventUtils.synthesizeKey(char, {});
+
+ await expectEvent("on-input-changed-fired", {text: char});
// Wait for the autocomplete search. Note that we cannot wait for the search
// to be complete, since the add-on doesn't communicate when it's done, so
// just check matches count.
- await BrowserTestUtils.waitForCondition(
- () => gURLBar.controller.matchCount >= 2 &&
- gURLBar.popup.richlistbox.children[1].getAttribute("ac-text") == gURLBar.controller.searchString,
- "waiting urlbar search to complete");
- return "t";
+ await waitForAutocompleteResultAt(indexToWaitFor);
+
+ return char;
}
async function testInputEvents() {
gURLBar.focus();
// Start an input session by typing in <keyword><space>.
for (let letter of keyword) {
EventUtils.synthesizeKey(letter, {});
@@ -169,17 +183,17 @@ add_task(async function() {
extension.sendMessage("set-default-suggestion", {
suggestion: {
description: expectedText,
},
});
await extension.awaitMessage("default-suggestion-set");
}
- let text = await startInputSession();
+ let text = await startInputSession(0);
let item = gURLBar.popup.richlistbox.children[0];
is(item.getAttribute("title"), expectedText,
`Expected heuristic result to have title: "${expectedText}".`);
is(item.getAttribute("displayurl"), `${keyword} ${text}`,
`Expected heuristic result to have displayurl: "${keyword} ${text}".`);
@@ -188,17 +202,17 @@ add_task(async function() {
await expectEvent("on-input-entered-fired", {
text,
disposition: "currentTab",
});
}
async function testDisposition(suggestionIndex, expectedDisposition, expectedText) {
- await startInputSession();
+ await startInputSession(suggestionIndex);
// Select the suggestion.
for (let i = 0; i < suggestionIndex; i++) {
EventUtils.synthesizeKey("VK_DOWN", {});
}
let item = gURLBar.popup.richlistbox.children[suggestionIndex];
if (expectedDisposition == "currentTab") {
@@ -224,17 +238,17 @@ add_task(async function() {
ok(!!item, "Expected item to exist");
is(item.getAttribute("title"), description,
`Expected suggestion to have title: "${description}".`);
is(item.getAttribute("displayurl"), `${keyword} ${content}`,
`Expected suggestion to have displayurl: "${keyword} ${content}".`);
}
- let text = await startInputSession();
+ let text = await startInputSession(info.suggestions.length - 1);
extension.sendMessage(info.test);
await extension.awaitMessage("test-ready");
info.suggestions.forEach(expectSuggestion);
EventUtils.synthesizeMouseAtCenter(gURLBar.popup.richlistbox.children[0], {});
await expectEvent("on-input-entered-fired", {
@@ -269,22 +283,20 @@ add_task(async function() {
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",
- skipHeuristic: true,
suggestions,
});
await testSuggestions({
test: "test-suggestions-after-delay",
- skipHeuristic: true,
suggestions,
});
// Start monitoring the console.
let waitForConsole = new Promise(resolve => {
SimpleTest.monitorConsole(resolve, [{
message: new RegExp(`The keyword provided is already registered: "${keyword}"`),
}]);
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -77,29 +77,32 @@ const FRECENCY_DEFAULT = 1000;
// Extensions are allowed to add suggestions if they have registered a keyword
// with the omnibox API. This is the maximum number of suggestions an extension
// is allowed to add for a given search string.
// This value includes the heuristic result.
const MAXIMUM_ALLOWED_EXTENSION_MATCHES = 6;
// After this time, we'll give up waiting for the extension to return matches.
-const MAXIMUM_ALLOWED_EXTENSION_TIME_MS = 5000;
+const MAXIMUM_ALLOWED_EXTENSION_TIME_MS = 3000;
// A regex that matches "single word" hostnames for whitelisting purposes.
// The hostname will already have been checked for general validity, so we
// don't need to be exhaustive here, so allow dashes anywhere.
const REGEXP_SINGLEWORD_HOST = new RegExp("^[a-z0-9-]+$", "i");
// Regex used to match userContextId.
const REGEXP_USER_CONTEXT_ID = /(?:^| )user-context-id:(\d+)/;
// Regex used to match one or more whitespace.
const REGEXP_SPACES = /\s+/;
+// The result is notified on a delay, to avoid rebuilding the panel at every match.
+const NOTIFYRESULT_DELAY_MS = 16;
+
// Sqlite result row index constants.
const QUERYINDEX_QUERYTYPE = 0;
const QUERYINDEX_URL = 1;
const QUERYINDEX_TITLE = 2;
const QUERYINDEX_BOOKMARKED = 3;
const QUERYINDEX_BOOKMARKTITLE = 4;
const QUERYINDEX_TAGS = 5;
const QUERYINDEX_VISITCOUNT = 6;
@@ -325,16 +328,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
"@mozilla.org/intl/texttosuburi;1",
"nsITextToSubURI");
function setTimeout(callback, ms) {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(callback, ms, timer.TYPE_ONE_SHOT);
+ return timer;
}
function convertBucketsCharPrefToArray(str) {
return str.split(",")
.map(v => {
let bucket = v.split(":");
return [ bucket[0].trim().toLowerCase(), Number(bucket[1]) ];
});
@@ -987,16 +991,19 @@ Search.prototype = {
* Stop this search.
* After invoking this method, we won't run any more searches or heuristics,
* and no new matches may be added to the current result.
*/
stop() {
// Avoid multiple calls or re-entrance.
if (!this.pending)
return;
+ if (this._notifyTimer)
+ this._notifyTimer.cancel();
+ this._notifyDelaysCount = 0;
if (this._sleepTimer)
this._sleepTimer.cancel();
if (this._sleepResolve) {
this._sleepResolve();
this._sleepResolve = null;
}
if (this._searchSuggestionController) {
this._searchSuggestionController.stop();
@@ -1122,17 +1129,16 @@ Search.prototype = {
searchSuggestionsCompletePromise = this._matchSearchSuggestions(searchString);
if (this.hasBehavior("restrict")) {
// Wait for the suggestions to be added.
await searchSuggestionsCompletePromise;
this._cleanUpNonCurrentMatches(MATCHTYPE.SUGGESTION);
// We're done if we're restricting to search suggestions.
// Notify the result completion then stop the search.
this._autocompleteSearch.finishSearch(true);
- this.stop();
return;
}
}
}
// In any case, clear previous suggestions.
searchSuggestionsCompletePromise.then(() => {
this._cleanUpNonCurrentMatches(MATCHTYPE.SUGGESTION);
});
@@ -1662,19 +1668,18 @@ Search.prototype = {
// Remove previous search matches sooner than the maximum timeout, otherwise
// matches may appear stale for a long time.
// This is necessary because WebExtensions don't have a method to notify
// that they are done providing results, so they could be pending forever.
setTimeout(() => this._cleanUpNonCurrentMatches(MATCHTYPE.EXTENSION), 100);
// Since the extension has no way to signale when it's done pushing
// results, we add a timeout racing with the addition.
- let timeoutPromise = new Promise((resolve, reject) => {
- setTimeout(() => reject(new Error("timeout waiting for the extension to add its results to the location bar")),
- MAXIMUM_ALLOWED_EXTENSION_TIME_MS);
+ let timeoutPromise = new Promise(resolve => {
+ setTimeout(resolve, MAXIMUM_ALLOWED_EXTENSION_TIME_MS);
});
return Promise.race([timeoutPromise, promise]).catch(Cu.reportError);
},
async _matchRemoteTabs() {
let matches = await PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
for (let {url, title, icon, deviceName} of matches) {
// It's rare that Sync supplies the icon for the page (but if it does, it
@@ -1904,17 +1909,17 @@ Search.prototype = {
match.finalCompleteValue);
this._currentMatchCount++;
this._counts[match.type]++;
if (this._currentMatchCount == 1)
TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT, this);
if (this._currentMatchCount == 6)
TelemetryStopwatch.finish(TELEMETRY_6_FIRST_RESULTS, this);
- this.notifyResults(true);
+ this.notifyResult(true, match.type == MATCHTYPE.HEURISTIC);
},
_getInsertIndexForMatch(match) {
let index = 0;
// The buckets change depending on the context, that is currently decided by
// the first added match (the heuristic one).
if (!this._buckets) {
// Convert the buckets to readable objects with a count property.
@@ -2004,17 +2009,17 @@ Search.prototype = {
while (bucket.count > bucket.insertIndex) {
this._result.removeMatchAt(index);
changed = true;
bucket.count--;
}
}
}
if (changed && notify) {
- this.notifyResults(true);
+ this.notifyResult(true);
}
},
/**
* If in restrict mode, cleanups non current matches for all the empty types.
*/
cleanUpRestrictNonCurrentMatches() {
if (this.hasBehavior("restrict") && this._previousSearchMatchTypes.length > 0) {
@@ -2359,34 +2364,57 @@ Search.prototype = {
query_type: QUERYTYPE_AUTOFILL_URL,
searchString,
revHost
});
return query;
},
- /**
- * Notifies the listener about results.
+ // The result is notified to the search listener on a timer, to chunk multiple
+ // match updates together and avoid rebuilding the popup at every new match.
+ _notifyTimer: null,
+
+ /**
+ * Notifies the current result to the listener.
*
* @param searchOngoing
- * Indicates whether the search is ongoing.
+ * Indicates whether the search result should be marked as ongoing.
+ * @param skipDelay
+ * Whether to notify immediately.
*/
- notifyResults(searchOngoing) {
- let result = this._result;
- let resultCode = this._currentMatchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
- if (searchOngoing) {
- resultCode += "_ONGOING";
+ _notifyDelaysCount: 0,
+ notifyResult(searchOngoing, skipDelay = false) {
+ let notify = () => {
+ this._notifyDelaysCount = 0;
+ let resultCode = this._currentMatchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+ if (searchOngoing) {
+ resultCode += "_ONGOING";
+ }
+ let result = this._result;
+ result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+ this._listener.onSearchResult(this._autocompleteSearch, result);
+ if (!searchOngoing) {
+ // Break possible cycles.
+ this._listener = null;
+ this._autocompleteSearch = null;
+ this.stop();
+ }
+ };
+ if (this._notifyTimer) {
+ this._notifyTimer.cancel();
}
- result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
- this._listener.onSearchResult(this._autocompleteSearch, result);
- if (!searchOngoing) {
- // Break possible cycles.
- this._listener = null;
- this._autocompleteSearch = null;
+ // In the worst case, we may get evenly spaced matches that would end up
+ // delaying the UI by N_MATCHES * NOTIFYRESULT_DELAY_MS. Thus, we clamp the
+ // number of times we may delay matches.
+ if (skipDelay || this._notifyDelaysCount > 3) {
+ notify();
+ } else {
+ this._notifyDelaysCount++;
+ this._notifyTimer = setTimeout(notify, NOTIFYRESULT_DELAY_MS);
}
},
};
// UnifiedComplete class
// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete
function UnifiedComplete() {
@@ -2574,32 +2602,31 @@ UnifiedComplete.prototype = {
let search = this._currentSearch;
if (!search)
return;
this._lastLowResultsSearchSuggestion = search._lastLowResultsSearchSuggestion;
if (!notify || !search.pending)
return;
-
// If we are in restrict mode and we reused the previous search results,
// it's possible we didn't go through all the cleanup methods due to early
// bailouts. Thus we could still have nonmatching results to remove.
search.cleanUpRestrictNonCurrentMatches();
// There is a possible race condition here.
// When a search completes it calls finishSearch that notifies results
// here. When the controller gets the last result it fires
// onSearchComplete.
// If onSearchComplete immediately starts a new search it will set a new
// _currentSearch, and on return the execution will continue here, after
- // notifyResults.
- // Thus, ensure that notifyResults is the last call in this method,
+ // notifyResult.
+ // Thus, ensure that notifyResult is the last call in this method,
// otherwise you might be touching the wrong search.
- search.notifyResults(false);
+ search.notifyResult(false);
},
// nsIAutoCompleteSearchDescriptor
get searchType() {
return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
},
--- a/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
@@ -190,17 +190,20 @@ add_task(async function test_removes_sug
name: extensionName,
emit(message, text, id) {
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
ExtensionSearchHandler.addSuggestions(keyword, id, [
{content: "foo", description: "first suggestion"},
{content: "bar", description: "second suggestion"},
{content: "baz", description: "third suggestion"},
]);
- controller.stopSearch();
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
}
}
};
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
await check_autocomplete({
search: `${keyword} unmatched`,
@@ -297,18 +300,21 @@ add_task(async function test_setting_the
let keyword = "test";
let extensionName = "Omnibox Example";
let mockExtension = {
name: extensionName,
emit(message, text, id) {
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
ExtensionSearchHandler.addSuggestions(keyword, id, []);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
}
- controller.stopSearch();
}
};
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
ExtensionSearchHandler.setDefaultSuggestion(keyword, {
description: "hello world"
});
@@ -353,17 +359,20 @@ add_task(async function test_maximum_num
{content: "d", description: "fourth suggestion"},
{content: "e", description: "fifth suggestion"},
{content: "f", description: "sixth suggestion"},
{content: "g", description: "seventh suggestion"},
{content: "h", description: "eigth suggestion"},
{content: "i", description: "ninth suggestion"},
{content: "j", description: "tenth suggestion"},
]);
- controller.stopSearch();
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
}
}
};
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
// Start an input session before testing MSG_INPUT_CHANGED.
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});