Bug 1887007 - P1. Move formautofill autocomplete seach logic from AutofillProfileAutoComplete to AutoCompleteChild r=credential-management-reviewers,sgalich

This commit makes two significant changes:

1. Refactors AutofillProfileAutoComplete Logic to AutoCompleteChild:

Migrates logic from AutofillProfileAutoComplete to AutoCompleteChild,
transitioning state management from a per-process to a per-frame basis.
This lays the groundwork for future support of autofill functionality
across iframes.

2. Implements the Concept of "Autocomplete Entry Providers":

Introduces a framework for autocomplete providers (e.g., FormAutofill, LoginManager, FormHistory)
to integrate with the autocomplete system through a set of APIs, including:
JSWindowActorChild:
  - string actorName()
  - bool shouldSearchForAutoComplete(element);
  - jsval getAutoCompleteSearchOption(element);
  - jsval recordToAutoCompleteResult(searchString, element, record);
JSWindowActorParent:
  - searchAutoCompleteEntries(searchString, options)

Besides implement the above API, autocomplete provider must use `markAsAutoCompletableField` in
AutoCompleteChild to register fields for autocomplete, enabling the FormFillController to
initiate autocomplete searches when users click on the input field.

Note: This patch only integrates FormAutofill, integrating FormHistory and
LoginManager will be in other patches.

Differential Revision: https://phabricator.services.mozilla.com/D205444
This commit is contained in:
Dimi 2024-04-15 15:48:47 +00:00
Родитель ff1ff3ee70
Коммит c5078faae0
9 изменённых файлов: 461 добавлений и 77 удалений

Просмотреть файл

@ -13,6 +13,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
});
const gFormFillController = Cc[
"@mozilla.org/satchel/form-fill-controller;1"
].getService(Ci.nsIFormFillController);
let autoCompleteListeners = new Set();
export class AutoCompleteChild extends JSWindowActorChild {
@ -190,6 +194,129 @@ export class AutoCompleteChild extends JSWindowActorChild {
return results;
}
// Store the input to interested autocomplete providers mapping
#providersByInput = new WeakMap();
providersByInput(input) {
// This functions returns the interested providers that have called
// `markAsAutoCompletableField` for the given input and also the hard-coded
// autocomplete providers based on input type.
const providers = new Set(this.#providersByInput.get(input));
// The current design is that FormHisotry doesn't call `markAsAutoCompletable`
// for every eligilbe input. Instead, when FormFillController receives a focus event,
// it would control the <input> if the <input> is eligible to show form history.
// Because of the design, we need to ask FormHistory whether to search for autocomplete entries
// for every startSearch call
// TODO: uncomment after integrating FormHisotry with AutoCompleteChild
// providers.add(input.ownerGlobal.windowGlobalChild.getActor("FormHistory"));
return providers;
}
/**
* This API should be used by an autocomplete entry provider to mark an input field
* as eligible for autocomplete for its type.
* When users click on an autocompletable input, we will search autocomplete entries
* from all the providers that have called this API for the given <input>.
*
* An autocomplete provider should be a JSWindowActor and implements the following
* functions:
* - string actorName()
* - bool shouldSearchForAutoComplete(element);
* - jsval getAutoCompleteSearchOption(element);
* - jsval searchResultToAutoCompleteResult(searchString, element, record);
* See `FormAutofillChild` for example
*
* @param input - The HTML <input> element that is considered autocompletable by the
* given provider
* @param provider - A module that provides autocomplete entries for a <input>, for example,
* FormAutofill provides address or credit card autocomplete entries,
* LoginManager provides logins entreis.
*/
markAsAutoCompletableField(input, provider) {
gFormFillController.markAsAutoCompletableField(input);
let providers = this.#providersByInput.get(input);
if (!providers) {
providers = new Set();
this.#providersByInput.set(input, providers);
}
providers.add(provider);
}
// Record the current ongoing search request. This is used by stopSearch
// to prevent notifying the autocomplete controller after receiving search request
// results that were issued prior to the call to stop the search.
#ongoingSearches = new Set();
async startSearch(searchString, input, listener) {
// TODO: This should be removed once we implement triggering autocomplete
// from the parent.
this.lastProfileAutoCompleteFocusedInput = input;
// For all the autocomplete entry providers that previsouly marked
// this <input> as autocompletable, ask the provider whether we should
// search for autocomplete entries in the parent. This is because the current
// design doesn't rely on the provider constantly monitor the <input> and
// then mark/unmark an input. The provider generally calls the
// `markAsAutoCompletbleField` when it sees an <input> is eliglbe for autocomplete.
// Here we ask the provider to exam the <input> more detailedly to see
// whether we need to search for autocomplete entries at the time users
// click on the <input>
const providers = this.providersByInput(input);
const data = Array.from(providers)
.filter(p => p.shouldSearchForAutoComplete(input, searchString))
.map(p => ({
actorName: p.actorName,
options: p.getAutoCompleteSearchOption(input, searchString),
}));
let result = [];
// We don't return empty result when no provider requests seaching entries in the
// parent because for some special cases, the autocomplete entries are coming
// from the content. For example, <datalist>.
if (data.length) {
const promise = this.sendQuery("AutoComplete:StartSearch", {
searchString,
data,
});
this.#ongoingSearches.add(promise);
result = await promise;
// If the search is stopped, don't report back.
if (!this.#ongoingSearches.delete(promise)) {
return;
}
}
for (const provider of providers) {
// Search result could be empty. However, an autocomplete provider might
// want to show an autoclmplete popup when there is no search result. For example,
// <datalist> for FormHisotry, insecure warning for LoginManager.
const searchResult = result.find(r => r.actorName == provider.actorName);
const acResult = provider.searchResultToAutoCompleteResult(
searchString,
input,
searchResult
);
// We have not yet supported showing autocomplete entries from multiple providers,
// Note: The prioty is defined in AutoCompleteParent.
if (acResult) {
this.lastProfileAutoCompleteResult = acResult;
listener.onSearchCompletion(acResult);
return;
}
}
this.lastProfileAutoCompleteResult = null;
}
stopSearch() {
this.lastProfileAutoCompleteResult = null;
this.#ongoingSearches.clear();
}
}
AutoCompleteChild.prototype.QueryInterface = ChromeUtils.generateQI([

Просмотреть файл

@ -376,7 +376,7 @@ export class AutoCompleteParent extends JSWindowActorParent {
}
}
receiveMessage(message) {
async receiveMessage(message) {
let browser = this.browsingContext.top.embedderElement;
if (
@ -433,6 +433,12 @@ export class AutoCompleteParent extends JSWindowActorParent {
this.closePopup();
break;
}
case "AutoComplete:StartSearch": {
const { searchString, data } = message.data;
const result = await this.#startSearch(searchString, data);
return result;
}
}
// Returning false to pacify ESLint, but this return value is
// ignored by the messaging infrastructure.
@ -493,6 +499,45 @@ export class AutoCompleteParent extends JSWindowActorParent {
}
}
// This defines the supported autocomplete providers and the prioity to show the autocomplete
// entry.
#AUTOCOMPLETE_PROVIDERS = ["FormAutofill", "LoginManager", "FormHistory"];
/**
* Search across multiple module to gather autocomplete entries for a given search string.
*
* @param {string} searchString
* The input string used to query autocomplete entries across different
* autocomplete providers.
* @param {Array<Object>} providers
* An array of objects where each object has a `name` used to identify the actor
* name of the provider and `options` that are passed to the `searchAutoCompleteEntries`
* method of the actor.
* @returns {Array<Object>} An array of results objects with `name` of the provider and `entries`
* that are returned from the provider module's `searchAutoCompleteEntries` method.
*/
async #startSearch(searchString, providers) {
for (const name of this.#AUTOCOMPLETE_PROVIDERS) {
const provider = providers.find(p => p.actorName == name);
if (!provider) {
continue;
}
const { actorName, options } = provider;
const actor =
this.browsingContext.currentWindowGlobal.getActor(actorName);
const entries = await actor?.searchAutoCompleteEntries(
searchString,
options
);
// We have not yet supported showing autocomplete entries from multiple providers,
if (entries) {
return [{ actorName, ...entries }];
}
}
return [];
}
stopSearch() {}
/**

Просмотреть файл

@ -5,6 +5,7 @@
#include "nsISupports.idl"
interface nsIAutoCompleteInput;
interface nsIFormFillCompleteObserver;
webidl Element;
@ -69,4 +70,21 @@ interface nsIAutoCompletePopup : nsISupports
* @return The currently selected result item index
*/
void selectBy(in boolean reverse, in boolean page);
/*
* Search for a given string and notify a listener (either synchronously
* or asynchronously) of the result
*
* @param searchString - The string to search for
* @param searchParam - An extra parameter
* @param previousResult - A previous result to use for faster searching
* @param listener - A listener to notify when the search is complete
*/
void startSearch(in AString searchString, in Element element, in nsIFormFillCompleteObserver listener);
/*
* Stop the search that is in progress
*/
void stopSearch();
};

Просмотреть файл

@ -296,8 +296,6 @@ AutofillProfileAutoCompleteSearch.prototype = {
export const ProfileAutocomplete = {
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
lastProfileAutoCompleteResult: null,
lastProfileAutoCompleteFocusedInput: null,
_registered: false,
_factory: null,
@ -329,7 +327,6 @@ export const ProfileAutocomplete = {
this._factory.unregister();
this._factory = null;
this._registered = false;
this._lastAutoCompleteResult = null;
Services.obs.removeObserver(this, "autocomplete-will-enter-text");
},
@ -362,6 +359,17 @@ export const ProfileAutocomplete = {
return actor.selectedIndex;
},
// TODO: This will be removed after we implement triggering autofill from the parent
get lastProfileAutoCompleteResult() {
return lazy.FormAutofillContent.activeAutofillChild
.lastProfileAutoCompleteResult;
},
get lastProfileAutoCompleteFocusedInput() {
return lazy.FormAutofillContent.activeAutofillChild
.lastProfileAutoCompleteFocusedInput;
},
async _fillFromAutocompleteRow(focusedInput) {
this.debug("_fillFromAutocompleteRow:", focusedInput);
let formDetails = lazy.FormAutofillContent.activeFormDetails;

Просмотреть файл

@ -8,11 +8,16 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs",
AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs",
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ProfileAutocomplete:
@ -656,16 +661,21 @@ export class FormAutofillChild extends JSWindowActorChild {
}
}
previewProfile(selectedIndex) {
let lastAutoCompleteResult =
lazy.ProfileAutocomplete.lastProfileAutoCompleteResult;
let focusedInput = this.activeInput;
get lastProfileAutoCompleteResult() {
return this.manager.getActor("AutoComplete")?.lastProfileAutoCompleteResult;
}
get lastProfileAutoCompleteFocusedInput() {
return this.manager.getActor("AutoComplete")
?.lastProfileAutoCompleteFocusedInput;
}
previewProfile(selectedIndex) {
if (
selectedIndex === -1 ||
!focusedInput ||
!lastAutoCompleteResult ||
lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill"
!this.activeInput ||
this.lastProfileAutoCompleteResult?.getStyleAt(selectedIndex) !=
"autofill"
) {
lazy.ProfileAutocomplete._clearProfilePreview();
} else {
@ -701,6 +711,149 @@ export class FormAutofillChild extends JSWindowActorChild {
return;
}
formFillController.markAsAutofillField(field);
this.manager
.getActor("AutoComplete")
?.markAsAutoCompletableField(field, this);
}
get actorName() {
return "FormAutofill";
}
/**
* Get the search options when searching for autocomplete entries in the parent
*
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
* @returns {object} the search options for the input
*/
getAutoCompleteSearchOption(input) {
const fieldDetail = this._fieldDetailsManager
._getFormHandler(input)
?.getFieldDetailByElement(input);
const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
? "SignUpFormScenario"
: "";
return { fieldName: fieldDetail?.fieldName, scenarioName };
}
/**
* Ask the provider whether it might have autocomplete entry to show
* for the given input.
*
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
* @returns {boolean} true if we shold search for autocomplete entries
*/
shouldSearchForAutoComplete(input) {
const fieldDetail = this._fieldDetailsManager
._getFormHandler(input)
?.getFieldDetailByElement(input);
if (!fieldDetail) {
return false;
}
const fieldName = fieldDetail.fieldName;
const isAddressField = lazy.FormAutofillUtils.isAddressField(fieldName);
const searchPermitted = isAddressField
? lazy.FormAutofill.isAutofillAddressesEnabled
: lazy.FormAutofill.isAutofillCreditCardsEnabled;
// If the specified autofill feature is pref off, do not search
if (!searchPermitted) {
return false;
}
// No profile can fill the currently-focused input.
if (!lazy.FormAutofillContent.savedFieldNames.has(fieldName)) {
return false;
}
// The current form has already been populated and the field is not
// an empty credit card field.
const isCreditCardField =
lazy.FormAutofillUtils.isCreditCardField(fieldName);
const isInputAutofilled =
this.activeHandler.getFilledStateByElement(input) ==
lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
const filledRecordGUID = this.activeSection.filledRecordGUID;
if (
!isInputAutofilled &&
filledRecordGUID &&
!(isCreditCardField && this.activeInput.value === "")
) {
return false;
}
// (address only) less than 3 inputs are covered by all saved fields in the storage.
if (
isAddressField &&
this.activeSection.allFieldNames.filter(field =>
lazy.FormAutofillContent.savedFieldNames.has(field)
).length < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
) {
return false;
}
return true;
}
/**
* Convert the search result to autocomplete results
*
* @param {string} searchString - The string to search for
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
* @param {Array<object>} records - autocomplete records
* @returns {AutocompleteResult}
*/
searchResultToAutoCompleteResult(searchString, input, records) {
if (!records) {
return null;
}
const entries = records.records;
const externalEntries = records.externalEntries;
const fieldDetail = this._fieldDetailsManager
._getFormHandler(input)
?.getFieldDetailByElement(input);
if (!fieldDetail) {
return null;
}
const adaptedRecords = this.activeSection.getAdaptedProfiles(entries);
const isSecure = lazy.InsecurePasswordUtils.isFormSecure(
this.activeHandler.form
);
const isInputAutofilled =
this.activeHandler.getFilledStateByElement(input) ==
lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
const allFieldNames = this.activeSection.allFieldNames;
const AutocompleteResult = lazy.FormAutofillUtils.isAddressField(
fieldDetail.fieldName
)
? lazy.AddressResult
: lazy.CreditCardResult;
const acResult = new AutocompleteResult(
searchString,
fieldDetail.fieldName,
allFieldNames,
adaptedRecords,
{ isSecure, isInputAutofilled }
);
acResult.externalEntries.push(
...externalEntries.map(
entry =>
new lazy.GenericAutocompleteItem(
entry.image,
entry.title,
entry.subtitle,
entry.fillMessageName,
entry.fillMessageData
)
)
);
return acResult;
}
}

Просмотреть файл

@ -267,17 +267,8 @@ export class FormAutofillParent extends JSWindowActorParent {
break;
}
case "FormAutofill:GetRecords": {
const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({
formOrigin: this.formOrigin,
scenarioName: data.scenarioName,
hasInput: !!data.searchString?.length,
});
const recordsPromise = FormAutofillParent.getRecords(data);
const [records, externalEntries] = await Promise.all([
recordsPromise,
relayPromise,
]);
return { records, externalEntries };
const records = await FormAutofillParent.getRecords(data);
return { records };
}
case "FormAutofill:OnFormSubmit": {
this.notifyMessageObservers("onFormSubmitted", data);
@ -401,6 +392,43 @@ export class FormAutofillParent extends JSWindowActorParent {
}
}
/**
* Retrieves autocomplete entries for a given search string and data context.
*
* @param {string} searchString
* The search string used to filter autocomplete entries.
* @param {object} options
* @param {string} options.fieldName
* The name of the field for which autocomplete entries are being fetched.
* @param {string} options.scenarioName
* The scenario name used in the autocomplete operation to fetch external entries.
* @returns {Promise<object>} A promise that resolves to an object containing two properties: `records` and `externalEntries`.
* `records` is an array of autofill records from the form's internal data, sorted by `timeLastUsed`.
* `externalEntries` is an array of external autocomplete items fetched based on the scenario.
*/
async searchAutoCompleteEntries(searchString, options) {
const { fieldName, scenarioName } = options;
const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({
formOrigin: this.formOrigin,
scenarioName,
hasInput: !!searchString?.length,
});
const recordsPromise = FormAutofillParent.getRecords({
searchString,
fieldName,
});
const [records, externalEntries] = await Promise.all([
recordsPromise,
relayPromise,
]);
// Sort addresses by timeLastUsed for showing the lastest used address at top.
records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
return { records, externalEntries };
}
/**
* Get the records from profile store and return results back to content
* process. It will decrypt the credit card number and append

Просмотреть файл

@ -9,7 +9,7 @@ Classes = [
'cid': '{895db6c7-dbdf-40ea-9f64-b175033243dc}',
'contract_ids': [
'@mozilla.org/satchel/form-fill-controller;1',
'@mozilla.org/autocomplete/search;1?name=form-history',
'@mozilla.org/autocomplete/search;1?name=form-fill-controller',
],
'type': 'nsFormFillController',
'constructor': 'nsFormFillController::GetSingleton',

Просмотреть файл

@ -522,18 +522,15 @@ nsFormFillController::GetSearchCount(uint32_t* aSearchCount) {
NS_IMETHODIMP
nsFormFillController::GetSearchAt(uint32_t index, nsACString& _retval) {
if (mAutofillInputs.Get(mFocusedInput)) {
MOZ_LOG(sLogger, LogLevel::Debug, ("GetSearchAt: autofill-profiles field"));
nsCOMPtr<nsIAutoCompleteSearch> profileSearch = do_GetService(
"@mozilla.org/autocomplete/search;1?name=autofill-profiles");
if (profileSearch) {
_retval.AssignLiteral("autofill-profiles");
return NS_OK;
}
}
MOZ_LOG(sLogger, LogLevel::Debug,
("GetSearchAt: form-fill-controller field"));
MOZ_LOG(sLogger, LogLevel::Debug, ("GetSearchAt: form-history field"));
_retval.AssignLiteral("form-history");
// The better solution should be AutoCompleteController gets the
// nsIAutoCompleteSearch interface from AutoCompletePopup and invokes the
// StartSearch without going through FormFillController. Currently
// FormFillController acts as the proxy to find the AutoCompletePopup for
// AutoCompleteController.
_retval.AssignLiteral("form-fill-controller");
return NS_OK;
}
@ -669,52 +666,56 @@ nsFormFillController::StartSearch(const nsAString& aSearchString,
nsIAutoCompleteObserver* aListener) {
MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch for %p", mFocusedInput));
nsresult rv;
if (mFocusedInput && mFocusedPopup) {
if (mAutofillInputs.Get(mFocusedInput)) {
MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: formautofill field"));
// If the login manager has indicated it's responsible for this field, let it
// handle the autocomplete. Otherwise, handle with form history.
// This method is sometimes called in unit tests and from XUL without a
// focused node.
if (mFocusedInput && (mPwmgrInputs.Get(mFocusedInput) ||
mFocusedInput->HasBeenTypePassword())) {
MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: login field"));
mLastListener = aListener;
return mFocusedPopup->StartSearch(aSearchString, mFocusedInput, this);
// If the login manager has indicated it's responsible for this field, let
// it handle the autocomplete. Otherwise, handle with form history. This
// method is sometimes called in unit tests and from XUL without a focused
// node.
} else if (mPwmgrInputs.Get(mFocusedInput) ||
mFocusedInput->HasBeenTypePassword()) {
MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: login field"));
// Handle the case where a password field is focused but
// MarkAsLoginManagerField wasn't called because password manager is
// disabled.
if (!mLoginManagerAC) {
mLoginManagerAC =
do_GetService("@mozilla.org/login-manager/autocompletesearch;1");
// Handle the case where a password field is focused but
// MarkAsLoginManagerField wasn't called because password manager is
// disabled.
if (!mLoginManagerAC) {
mLoginManagerAC =
do_GetService("@mozilla.org/login-manager/autocompletesearch;1");
}
if (NS_WARN_IF(!mLoginManagerAC)) {
return NS_ERROR_FAILURE;
}
// XXX aPreviousResult shouldn't ever be a historyResult type, since we're
// not letting satchel manage the field?
mLastListener = aListener;
return mLoginManagerAC->StartSearch(aSearchString, aPreviousResult,
mFocusedInput, this);
}
if (NS_WARN_IF(!mLoginManagerAC)) {
return NS_ERROR_FAILURE;
}
// XXX aPreviousResult shouldn't ever be a historyResult type, since we're
// not letting satchel manage the field?
mLastListener = aListener;
rv = mLoginManagerAC->StartSearch(aSearchString, aPreviousResult,
mFocusedInput, this);
NS_ENSURE_SUCCESS(rv, rv);
} else {
MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: non-login field"));
mLastListener = aListener;
bool addDataList = IsTextControl(mFocusedInput);
if (addDataList) {
MaybeObserveDataListMutations();
}
auto* formHistoryAutoComplete = GetFormHistoryAutoComplete();
NS_ENSURE_TRUE(formHistoryAutoComplete, NS_ERROR_FAILURE);
formHistoryAutoComplete->AutoCompleteSearchAsync(
aSearchParam, aSearchString, mFocusedInput, aPreviousResult,
addDataList, this);
mLastFormHistoryAutoComplete = formHistoryAutoComplete;
}
MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: non-login field"));
mLastListener = aListener;
bool addDataList = IsTextControl(mFocusedInput);
if (addDataList) {
MaybeObserveDataListMutations();
}
auto* formHistoryAutoComplete = GetFormHistoryAutoComplete();
NS_ENSURE_TRUE(formHistoryAutoComplete, NS_ERROR_FAILURE);
formHistoryAutoComplete->AutoCompleteSearchAsync(
aSearchParam, aSearchString, mFocusedInput, aPreviousResult, addDataList,
this);
mLastFormHistoryAutoComplete = formHistoryAutoComplete;
return NS_OK;
}
@ -767,6 +768,10 @@ nsFormFillController::StopSearch() {
mLastFormHistoryAutoComplete = nullptr;
}
if (mFocusedPopup) {
mFocusedPopup->StopSearch();
}
if (mLoginManagerAC) {
mLoginManagerAC->StopSearch();
}

Просмотреть файл

@ -19,7 +19,7 @@ var aaListener = {
function run_test() {
do_test_pending();
let search = Cc[
"@mozilla.org/autocomplete/search;1?name=form-history"
"@mozilla.org/autocomplete/search;1?name=form-fill-controller"
].getService(Ci.nsIAutoCompleteSearch);
search.startSearch("aa", "", null, aaListener);
}