Bug 1900009 - Dispatch DOMPossibleUsernameInputAdded event for valid text and email inputs. r=dimi,edgar
Differential Revision: https://phabricator.services.mozilla.com/D214821
This commit is contained in:
@@ -164,7 +164,7 @@ NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLFormElement,
|
||||
void HTMLFormElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
|
||||
if (aEvent->mEventType == u"DOMFormHasPassword"_ns) {
|
||||
mHasPendingPasswordEvent = false;
|
||||
} else if (aEvent->mEventType == u"DOMFormHasPossibleUsername"_ns) {
|
||||
} else if (aEvent->mEventType == u"DOMPossibleUsernameInputAdded"_ns) {
|
||||
mHasPendingPossibleUsernameEvent = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ class HTMLFormElement final : public nsGenericHTMLElement {
|
||||
|
||||
/** Whether we already dispatched a DOMFormHasPassword event or not */
|
||||
bool mHasPendingPasswordEvent = false;
|
||||
/** Whether we already dispatched a DOMFormHasPossibleUsername event or not */
|
||||
/** Whether we already dispatched a DOMPossibleUsernameInputAdded event or not
|
||||
*/
|
||||
bool mHasPendingPossibleUsernameEvent = false;
|
||||
|
||||
// nsIContent
|
||||
|
||||
@@ -4415,7 +4415,7 @@ void HTMLInputElement::MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm) {
|
||||
}
|
||||
|
||||
nsString eventType;
|
||||
Element* target = nullptr;
|
||||
EventTarget* target = nullptr;
|
||||
|
||||
if (mType == FormControlType::InputPassword) {
|
||||
// Don't fire another event if we have a pending event.
|
||||
@@ -4426,28 +4426,40 @@ void HTMLInputElement::MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm) {
|
||||
// TODO(Bug 1864404): Use one event for formless and form inputs.
|
||||
eventType = aForm ? u"DOMFormHasPassword"_ns : u"DOMInputPasswordAdded"_ns;
|
||||
|
||||
target = aForm ? static_cast<Element*>(aForm) : this;
|
||||
|
||||
if (aForm) {
|
||||
target = aForm;
|
||||
aForm->mHasPendingPasswordEvent = true;
|
||||
} else {
|
||||
target = this;
|
||||
}
|
||||
|
||||
} else if (mType == FormControlType::InputEmail ||
|
||||
mType == FormControlType::InputText) {
|
||||
// Don't fire a username event if:
|
||||
// - <input> is not part of a form
|
||||
// - we have a pending event
|
||||
// - username only forms are not supported
|
||||
if (!aForm || aForm->mHasPendingPossibleUsernameEvent ||
|
||||
!StaticPrefs::signon_usernameOnlyForm_enabled()) {
|
||||
// fire event if we have a username field without a form with the
|
||||
// autcomplete value of username
|
||||
|
||||
if (!StaticPrefs::signon_usernameOnlyForm_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventType = u"DOMFormHasPossibleUsername"_ns;
|
||||
target = aForm;
|
||||
|
||||
if (aForm) {
|
||||
if (aForm->mHasPendingPossibleUsernameEvent) {
|
||||
return;
|
||||
}
|
||||
aForm->mHasPendingPossibleUsernameEvent = true;
|
||||
|
||||
target = aForm;
|
||||
} else {
|
||||
nsAutoString autocompleteValue;
|
||||
GetAutocomplete(autocompleteValue);
|
||||
if (!autocompleteValue.EqualsASCII("username")) {
|
||||
return;
|
||||
}
|
||||
target = GetComposedDoc();
|
||||
}
|
||||
eventType = u"DOMPossibleUsernameInputAdded"_ns;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,18 +49,33 @@ export const LoginFormFactory = {
|
||||
let formLike = lazy.FormLikeFactory.createFromForm(aForm);
|
||||
formLike.action = lazy.LoginHelper.getFormActionOrigin(aForm);
|
||||
|
||||
let rootElementsSet = this.getRootElementsWeakSetForDocument(
|
||||
formLike.ownerDocument
|
||||
);
|
||||
rootElementsSet.add(formLike.rootElement);
|
||||
this._addLoginFormToRootElementsSet(formLike);
|
||||
|
||||
return formLike;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a LoginForm object from the HTMLHtmlElement that is the root of the document
|
||||
*
|
||||
* Currently all <input> not in a <form> are one LoginForm but this
|
||||
* shouldn't be relied upon as the heuristics may change to detect multiple
|
||||
* "forms" (e.g. registration and login) on one page with a <form>.
|
||||
*
|
||||
* @param {HTMLHtmlElement} aDocumentRoot
|
||||
* @return {LoginForm}
|
||||
* @throws Error if aDocumentRoot isn't an HTMLHtmlElement
|
||||
*/
|
||||
createFromDocumentRoot(aDocumentRoot) {
|
||||
const formLike = lazy.FormLikeFactory.createFromDocumentRoot(aDocumentRoot);
|
||||
formLike.action = lazy.LoginHelper.getLoginOrigin(aDocumentRoot.baseURI);
|
||||
|
||||
lazy.log.debug(
|
||||
"adding",
|
||||
formLike.rootElement,
|
||||
"to root elements for",
|
||||
formLike.ownerDocument
|
||||
"Created non-form LoginForm for rootElement:",
|
||||
aDocumentRoot
|
||||
);
|
||||
|
||||
this._loginFormsByRootElement.set(formLike.rootElement, formLike);
|
||||
this._addLoginFormToRootElementsSet(formLike);
|
||||
|
||||
return formLike;
|
||||
},
|
||||
|
||||
@@ -113,19 +128,7 @@ export const LoginFormFactory = {
|
||||
aField.ownerDocument.documentElement
|
||||
);
|
||||
|
||||
let rootElementsSet = this.getRootElementsWeakSetForDocument(
|
||||
formLike.ownerDocument
|
||||
);
|
||||
rootElementsSet.add(formLike.rootElement);
|
||||
lazy.log.debug(
|
||||
"adding",
|
||||
formLike.rootElement,
|
||||
"to root elements for",
|
||||
formLike.ownerDocument
|
||||
);
|
||||
|
||||
this._loginFormsByRootElement.set(formLike.rootElement, formLike);
|
||||
|
||||
this._addLoginFormToRootElementsSet(formLike);
|
||||
return formLike;
|
||||
},
|
||||
|
||||
@@ -145,4 +148,19 @@ export const LoginFormFactory = {
|
||||
setForRootElement(aRootElement, aLoginForm) {
|
||||
return this._loginFormsByRootElement.set(aRootElement, aLoginForm);
|
||||
},
|
||||
|
||||
_addLoginFormToRootElementsSet(formLike) {
|
||||
let rootElementsSet = this.getRootElementsWeakSetForDocument(
|
||||
formLike.ownerDocument
|
||||
);
|
||||
rootElementsSet.add(formLike.rootElement);
|
||||
lazy.log.debug(
|
||||
"adding",
|
||||
formLike.rootElement,
|
||||
"to root elements for",
|
||||
formLike.ownerDocument
|
||||
);
|
||||
|
||||
this._loginFormsByRootElement.set(formLike.rootElement, formLike);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -771,21 +771,21 @@ export class LoginFormState {
|
||||
* 2. Only contains one input field whose type is username compatible.
|
||||
* 3. The username compatible input field looks like a username field
|
||||
* or the form itself looks like a sign-in or sign-up form.
|
||||
* Additionally, if an input is formless and its autocomplete attribute is
|
||||
* set to 'username' (this check is done in the DOM to avoid firing excessive events),
|
||||
* we construct a FormLike object using this input and perform the same logic
|
||||
* described above to determine if the new FormLike object is username-only.
|
||||
*
|
||||
* @param {Element} formElement
|
||||
* @param {FormLike} form
|
||||
* the form to check.
|
||||
* @param {Object} recipe=null
|
||||
* A relevant field override recipe to use.
|
||||
* @returns {Element} The username field or null (if the form is not a
|
||||
* username-only form).
|
||||
*/
|
||||
getUsernameFieldFromUsernameOnlyForm(formElement, recipe = null) {
|
||||
if (!HTMLFormElement.isInstance(formElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getUsernameFieldFromUsernameOnlyForm(form, recipe = null) {
|
||||
let candidate = null;
|
||||
for (let element of formElement.elements) {
|
||||
for (let element of form.elements) {
|
||||
// We are looking for a username-only form, so if there is a password
|
||||
// field in the form, this is NOT a username-only form.
|
||||
if (element.hasBeenTypePassword) {
|
||||
@@ -811,10 +811,9 @@ export class LoginFormState {
|
||||
}
|
||||
candidate = element;
|
||||
}
|
||||
|
||||
if (
|
||||
candidate &&
|
||||
this.#isProbablyAUsernameLoginForm(formElement, candidate)
|
||||
this.#isProbablyAUsernameLoginForm(form.rootElement, candidate)
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
@@ -1109,7 +1108,7 @@ export class LoginFormState {
|
||||
}
|
||||
|
||||
usernameField = this.getUsernameFieldFromUsernameOnlyForm(
|
||||
form.rootElement,
|
||||
form,
|
||||
fieldOverrideRecipe
|
||||
);
|
||||
|
||||
@@ -1525,8 +1524,8 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||
lazy.InsecurePasswordUtils.reportInsecurePasswords(formLike);
|
||||
break;
|
||||
}
|
||||
case "DOMFormHasPossibleUsername": {
|
||||
this.#onDOMFormHasPossibleUsername(event);
|
||||
case "DOMPossibleUsernameInputAdded": {
|
||||
this.#onDOMPossibleUsernameInputAdded(event);
|
||||
break;
|
||||
}
|
||||
case "DOMInputPasswordAdded": {
|
||||
@@ -1797,19 +1796,31 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||
this._fetchLoginsFromParentAndFillForm(formLike);
|
||||
}
|
||||
|
||||
#onDOMFormHasPossibleUsername(event) {
|
||||
#onDOMPossibleUsernameInputAdded(event) {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
const isPrimaryPasswordSet = this.#getIsPrimaryPasswordSet();
|
||||
let document = event.target.ownerDocument;
|
||||
|
||||
let document;
|
||||
let formLike;
|
||||
|
||||
if (HTMLFormElement.isInstance(event.target)) {
|
||||
document = event.target.ownerDocument;
|
||||
formLike = lazy.LoginFormFactory.createFromForm(event.target);
|
||||
} else {
|
||||
document = event.target;
|
||||
formLike = lazy.LoginFormFactory.createFromDocumentRoot(
|
||||
document.documentElement
|
||||
);
|
||||
}
|
||||
|
||||
lazy.log(
|
||||
`#onDOMFormHasPossibleUsername: visibilityState: ${document.visibilityState}, isPrimaryPasswordSet: ${isPrimaryPasswordSet}.`
|
||||
`#onDomPossibleUsernameInputAdded: visibilityState: ${document.visibilityState}, isPrimaryPasswordSet: ${isPrimaryPasswordSet}.`
|
||||
);
|
||||
|
||||
// For simplicity, the result of the telemetry is stacked. This means if a
|
||||
// document receives two `DOMFormHasPossibleEvent`, we add one counter to both
|
||||
// document receives two `DOMPossibleUsernameInputAdded`, we add one counter to both
|
||||
// bucket 1 & 2.
|
||||
let docState = this.stateForDocument(document);
|
||||
|
||||
@@ -1823,19 +1834,16 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||
}
|
||||
|
||||
if (document.visibilityState == "visible" || isPrimaryPasswordSet) {
|
||||
this._processDOMFormHasPossibleUsernameEvent(event);
|
||||
this._processDOMPossibleUsernameInputAddedEvent(formLike);
|
||||
} else {
|
||||
// wait until the document becomes visible before handling this event
|
||||
this._deferHandlingEventUntilDocumentVisible(event, document, () => {
|
||||
this._processDOMFormHasPossibleUsernameEvent(event);
|
||||
this._processDOMPossibleUsernameInputAddedEvent(formLike);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_processDOMFormHasPossibleUsernameEvent(event) {
|
||||
let form = event.target;
|
||||
let formLike = lazy.LoginFormFactory.createFromForm(form);
|
||||
|
||||
_processDOMPossibleUsernameInputAddedEvent(formLike) {
|
||||
// If the form contains a passoword field, `getUsernameFieldFromUsernameOnlyForm` returns
|
||||
// null, so we don't trigger autofill for those forms here. In this function,
|
||||
// we only care about username-only forms. For forms contain a password, they'll be handled
|
||||
@@ -1844,8 +1852,12 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||
// We specifically set the recipe to empty here to avoid loading site recipes during page loads.
|
||||
// This is okay because if we end up finding a username-only form that should be ignore by
|
||||
// the site recipe, the form will be skipped while autofilling later.
|
||||
let docState = this.stateForDocument(form.ownerDocument);
|
||||
let usernameField = docState.getUsernameFieldFromUsernameOnlyForm(form, {});
|
||||
let docState = this.stateForDocument(formLike.ownerDocument);
|
||||
let usernameField = docState.getUsernameFieldFromUsernameOnlyForm(
|
||||
formLike,
|
||||
{}
|
||||
);
|
||||
|
||||
if (usernameField) {
|
||||
// Autofill the username-only form.
|
||||
lazy.log("A username-only form is found.");
|
||||
|
||||
@@ -6,7 +6,7 @@ const ids = {
|
||||
INPUT_TYPE: "",
|
||||
};
|
||||
|
||||
function task({ contentIds, expected }) {
|
||||
function task({ contentIds, expected, hasForm = true }) {
|
||||
let resolve;
|
||||
let promise = new Promise(r => {
|
||||
resolve = r;
|
||||
@@ -27,14 +27,17 @@ function task({ contentIds, expected }) {
|
||||
removeEventListener("load", tabLoad, true);
|
||||
|
||||
gDoc = content.document;
|
||||
gDoc.addEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
|
||||
addEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
|
||||
gDoc.addEventListener(
|
||||
"DOMPossibleUsernameInputAdded",
|
||||
unexpectedContentEvent
|
||||
);
|
||||
addEventListener("DOMPossibleUsernameInputAdded", unexpectedContentEvent);
|
||||
gDoc.defaultView.setTimeout(test_inputAdd, 0);
|
||||
}
|
||||
|
||||
function test_inputAdd() {
|
||||
if (expected) {
|
||||
addEventListener("DOMFormHasPossibleUsername", test_inputAddHandler, {
|
||||
addEventListener("DOMPossibleUsernameInputAdded", test_inputAddHandler, {
|
||||
once: true,
|
||||
capture: true,
|
||||
});
|
||||
@@ -45,26 +48,46 @@ function task({ contentIds, expected }) {
|
||||
input.setAttribute("type", contentIds.INPUT_TYPE);
|
||||
input.setAttribute("id", contentIds.INPUT_ID);
|
||||
input.setAttribute("data-test", "unique-attribute");
|
||||
if (hasForm) {
|
||||
gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
|
||||
} else {
|
||||
input.setAttribute("autocomplete", "username");
|
||||
gDoc.querySelector("body").appendChild(input);
|
||||
}
|
||||
}
|
||||
|
||||
function test_inputAddHandler(evt) {
|
||||
if (expected) {
|
||||
evt.stopPropagation();
|
||||
if (hasForm) {
|
||||
Assert.equal(
|
||||
evt.target.id,
|
||||
contentIds.FORM1_ID,
|
||||
evt.type +
|
||||
" event targets correct form element (added possible username element)"
|
||||
evt.type + " event targets correct username element"
|
||||
);
|
||||
} else {
|
||||
Assert.ok(
|
||||
HTMLDocument.isInstance(evt.target),
|
||||
evt.type + " event targets document"
|
||||
);
|
||||
}
|
||||
gDoc.defaultView.setTimeout(test_inputChangeForm, 0);
|
||||
}
|
||||
|
||||
let nextTask;
|
||||
if (hasForm) {
|
||||
nextTask = test_inputChangeForm;
|
||||
} else if (!hasForm && contentIds.INPUT_TYPE !== "text") {
|
||||
nextTask = test_inputChangesType;
|
||||
} else {
|
||||
nextTask = finish;
|
||||
}
|
||||
gDoc.defaultView.setTimeout(nextTask, 0);
|
||||
}
|
||||
|
||||
function test_inputChangeForm() {
|
||||
if (expected) {
|
||||
addEventListener(
|
||||
"DOMFormHasPossibleUsername",
|
||||
"DOMPossibleUsernameInputAdded",
|
||||
test_inputChangeFormHandler,
|
||||
{ once: true, capture: true }
|
||||
);
|
||||
@@ -81,7 +104,7 @@ function task({ contentIds, expected }) {
|
||||
Assert.equal(
|
||||
evt.target.id,
|
||||
contentIds.FORM2_ID,
|
||||
evt.type + " event targets correct form element (changed form)"
|
||||
evt.type + " event targets correct username element"
|
||||
);
|
||||
}
|
||||
// TODO(Bug 1864405): Refactor this test to not expect a DOM event
|
||||
@@ -96,7 +119,7 @@ function task({ contentIds, expected }) {
|
||||
function test_inputChangesType() {
|
||||
if (expected) {
|
||||
addEventListener(
|
||||
"DOMFormHasPossibleUsername",
|
||||
"DOMPossibleUsernameInputAdded",
|
||||
test_inputChangesTypeHandler,
|
||||
{ once: true, capture: true }
|
||||
);
|
||||
@@ -110,19 +133,29 @@ function task({ contentIds, expected }) {
|
||||
function test_inputChangesTypeHandler(evt) {
|
||||
if (expected) {
|
||||
evt.stopPropagation();
|
||||
if (hasForm) {
|
||||
Assert.equal(
|
||||
evt.target.id,
|
||||
contentIds.FORM1_ID,
|
||||
evt.type + " event targets correct form element (changed type)"
|
||||
evt.type + " event targets correct input element (changed type)"
|
||||
);
|
||||
} else {
|
||||
Assert.ok(
|
||||
HTMLDocument.isInstance(evt.target),
|
||||
evt.type + " event targets document"
|
||||
);
|
||||
}
|
||||
}
|
||||
gDoc.defaultView.setTimeout(finish, 0);
|
||||
}
|
||||
|
||||
function finish() {
|
||||
removeEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
|
||||
removeEventListener(
|
||||
"DOMPossibleUsernameInputAdded",
|
||||
unexpectedContentEvent
|
||||
);
|
||||
gDoc.removeEventListener(
|
||||
"DOMFormHasPossibleUsername",
|
||||
"DOMPossibleUsernameInputAdded",
|
||||
unexpectedContentEvent
|
||||
);
|
||||
resolve();
|
||||
@@ -148,20 +181,20 @@ add_task(async function test_disconnectedInputs() {
|
||||
);
|
||||
};
|
||||
|
||||
addEventListener("DOMFormHasPossibleUsername", unexpectedEvent);
|
||||
addEventListener("DOMPossibleUsernameInputAdded", unexpectedEvent);
|
||||
const form = content.document.createElement("form");
|
||||
const textInput = content.document.createElement("input");
|
||||
textInput.setAttribute("type", "text");
|
||||
form.appendChild(textInput);
|
||||
|
||||
// Delay the execution for a bit to allow time for any asynchronously
|
||||
// dispatched 'DOMFormHasPossibleUsername' events to be processed.
|
||||
// dispatched 'DOMPossibleUsernameInputAdded' events to be processed.
|
||||
// This is necessary because such events might not be triggered immediately,
|
||||
// and we want to ensure that if they are dispatched, they are captured
|
||||
// before we remove the event listener.
|
||||
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
removeEventListener("DOMFormHasPossibleUsername", unexpectedEvent);
|
||||
removeEventListener("DOMPossibleUsernameInputAdded", unexpectedEvent);
|
||||
});
|
||||
|
||||
Assert.ok(true, "Test completed");
|
||||
@@ -195,6 +228,30 @@ add_task(async function test_usernameOnlyForm() {
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_formlessUsernameInput() {
|
||||
for (let type of ["text", "email"]) {
|
||||
let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
|
||||
|
||||
ids.INPUT_TYPE = type;
|
||||
let promise = ContentTask.spawn(
|
||||
tab.linkedBrowser,
|
||||
{ contentIds: ids, expected: true, hasForm: false },
|
||||
task
|
||||
);
|
||||
BrowserTestUtils.startLoadingURIString(
|
||||
tab.linkedBrowser,
|
||||
`data:text/html;charset=utf-8,
|
||||
<html><body>
|
||||
<input id="${ids.CHANGE_INPUT_ID}" autocomplete="username">
|
||||
</body></html>`
|
||||
);
|
||||
await promise;
|
||||
|
||||
Assert.ok(true, "Test completed");
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_nonSupportedInputType() {
|
||||
for (let type of ["url", "tel", "number"]) {
|
||||
let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
|
||||
|
||||
@@ -421,10 +421,10 @@ async function checkUnmodifiedFormInFrame(bc, formNum) {
|
||||
return SpecialPowers.spawn(bc, [formNum], formNumF => {
|
||||
let form = this.content.document.getElementById(`form${formNumF}`);
|
||||
ok(form, "Locating form " + formNumF);
|
||||
const elements =
|
||||
form.nodeName === "form" ? form.elements : form.querySelectorAll("input");
|
||||
|
||||
for (var i = 0; i < form.elements.length; i++) {
|
||||
var ele = form.elements[i];
|
||||
|
||||
for (const ele of elements) {
|
||||
// No point in checking form submit/reset buttons.
|
||||
if (ele.type == "submit" || ele.type == "reset") {
|
||||
continue;
|
||||
@@ -466,10 +466,15 @@ async function checkLoginFormInFrameWithElementValues(
|
||||
|
||||
let numToCheck = arguments.length - 1;
|
||||
|
||||
const elements =
|
||||
form.nodeName === "form"
|
||||
? form.elements
|
||||
: form.querySelectorAll("input");
|
||||
|
||||
if (!numToCheck--) {
|
||||
return;
|
||||
}
|
||||
e = form.elements[0];
|
||||
e = elements[0];
|
||||
if (val1F == null) {
|
||||
is(
|
||||
e.value,
|
||||
@@ -488,7 +493,7 @@ async function checkLoginFormInFrameWithElementValues(
|
||||
return;
|
||||
}
|
||||
|
||||
e = form.elements[1];
|
||||
e = elements[1];
|
||||
if (val2F == null) {
|
||||
is(
|
||||
e.value,
|
||||
@@ -506,7 +511,7 @@ async function checkLoginFormInFrameWithElementValues(
|
||||
if (!numToCheck--) {
|
||||
return;
|
||||
}
|
||||
e = form.elements[2];
|
||||
e = elements[2];
|
||||
if (val3F == null) {
|
||||
is(
|
||||
e.value,
|
||||
|
||||
@@ -13,7 +13,8 @@ Test autofill on username-form
|
||||
<script>
|
||||
add_setup(async () => {
|
||||
await setStoredLoginsAsync(
|
||||
[window.location.origin, "https://autofill", null, "user1", "pass1"]
|
||||
[window.location.origin, "https://autofill", null, "user1", "pass1"],
|
||||
[window.location.origin, "http://mochi.test", null, "user2", "pass1"]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -28,10 +29,9 @@ add_task(async function test_autofill_username_only_form() {
|
||||
let win = window.open("about:blank");
|
||||
SimpleTest.registerCleanupFunction(() => win.close());
|
||||
|
||||
// 5 out of the 7 forms should be autofilled
|
||||
// 6 out of the 10 forms should be autofilled
|
||||
await loadFormIntoWindow(window.location.origin, `
|
||||
<!-- no password field, 1 username field -->
|
||||
<form id='form1' action='https://autofill'> 1
|
||||
<!-- no password field, 1 username field --> <form id='form1' action='https://autofill'> 1
|
||||
<input type='text' name='uname' autocomplete='username' value=''>
|
||||
|
||||
<button type='submit'>Submit</button>
|
||||
@@ -85,7 +85,24 @@ add_task(async function test_autofill_username_only_form() {
|
||||
|
||||
<button type='submit'>Submit</button>
|
||||
<button type='reset'> Reset </button>
|
||||
</form>`, win, 5);
|
||||
</form>
|
||||
|
||||
<!-- no form, one text input field (without autocomplete == 'username'), should be ignored -->
|
||||
<div id="form8"> 8
|
||||
<input id='un1' name='shouldnotfill' placeholder=username value= ''>
|
||||
</div>
|
||||
|
||||
<!-- no password field, 1 username field without autocomplete-->
|
||||
<form id='form9' action='https://autofill'> 9
|
||||
<input type="text" id="username" name='uname' value=''>
|
||||
</form>
|
||||
|
||||
<!-- no form, two text input fields with autcomplete == 'username', should be ignored -->
|
||||
<div id='form10'> 10
|
||||
<input name='shouldnotfill' autocomplete=username value = ''>
|
||||
<input name='shouldnotfill' autocomplete=username value = ''>
|
||||
</div>`, win, 6);
|
||||
|
||||
|
||||
await checkLoginFormInFrameWithElementValues(win, 1, "user1");
|
||||
await checkLoginFormInFrameWithElementValues(win, 2, "someuser");
|
||||
@@ -94,7 +111,21 @@ add_task(async function test_autofill_username_only_form() {
|
||||
await checkUnmodifiedFormInFrame(win, 5);
|
||||
await checkUnmodifiedFormInFrame(win, 6);
|
||||
await checkUnmodifiedFormInFrame(win, 7);
|
||||
await checkUnmodifiedFormInFrame(win, 8);
|
||||
await checkLoginFormInFrameWithElementValues(win, 9, "user1");
|
||||
await checkUnmodifiedFormInFrame(win, 10);
|
||||
|
||||
// We must close and reopen the window since multiple formless inputs in one document are grouped together,
|
||||
// which won't trigger autofill.
|
||||
win.close()
|
||||
win = window.open("about:blank");
|
||||
await loadFormIntoWindow(window.location.origin, `
|
||||
<!-- no form, 1 username field with autocomplete, without a value set -->
|
||||
<div id='form1'> 1
|
||||
<input type='text' name='uname' autocomplete='username' value=''>
|
||||
</div>`, win, 1);
|
||||
|
||||
await checkLoginFormInFrameWithElementValues(win, 1, "user2");
|
||||
await resetRecipes();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -102,7 +102,7 @@ const TESTCASES = [
|
||||
{
|
||||
description: "1 username field outside of a <form>",
|
||||
document: `<input id="un1" autocomplete=username>`,
|
||||
returnedFieldIDs: [null, null, null],
|
||||
returnedFieldIDs: ["un1", null, null],
|
||||
},
|
||||
{
|
||||
description: "username with type=user",
|
||||
|
||||
@@ -8,6 +8,10 @@ const { LoginManagerChild } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/LoginManagerChild.sys.mjs"
|
||||
);
|
||||
|
||||
const { FormLikeFactory } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/FormLikeFactory.sys.mjs"
|
||||
);
|
||||
|
||||
// expectation[0] tests cases when a form doesn't have a sign-in keyword.
|
||||
// expectation[1] tests cases when a form has a sign-in keyword.
|
||||
const TESTCASES = [
|
||||
@@ -162,10 +166,11 @@ for (let tc of TESTCASES) {
|
||||
form.setAttribute("name", "login");
|
||||
}
|
||||
|
||||
const formLike = FormLikeFactory.createFromForm(form);
|
||||
const lmc = new LoginManagerChild();
|
||||
const docState = lmc.stateForDocument(form.ownerDocument);
|
||||
const element = docState.getUsernameFieldFromUsernameOnlyForm(
|
||||
form,
|
||||
formLike,
|
||||
testcase.fieldOverrideRecipe
|
||||
);
|
||||
Assert.strictEqual(
|
||||
|
||||
@@ -379,7 +379,7 @@ let JSWINDOWACTORS = {
|
||||
"form-submission-detected": { createActor: false },
|
||||
"before-form-submission": { createActor: false },
|
||||
DOMFormHasPassword: {},
|
||||
DOMFormHasPossibleUsername: {},
|
||||
DOMPossibleUsernameInputAdded: {},
|
||||
DOMInputPasswordAdded: {},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,6 +36,53 @@ export let FormLikeFactory = {
|
||||
return formLike;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a FormLike object from an HTMLHtmlElement that is the root of the document
|
||||
*
|
||||
* Currently all <input> not in a <form> are one LoginForm but this
|
||||
* shouldn't be relied upon as the heuristics may change to detect multiple
|
||||
* "forms" (e.g. registration and login) on one page with a <form>.
|
||||
*
|
||||
* @param {HTMLHtmlElement} aDocumentRoot
|
||||
* @return {FormLike}
|
||||
* @throws Error if aDocumentRoot isn't an HTMLHtmlElement
|
||||
*/
|
||||
createFromDocumentRoot(aDocumentRoot) {
|
||||
if (!HTMLHtmlElement.isInstance(aDocumentRoot)) {
|
||||
throw new Error(
|
||||
"createFromDocumentRoot: aDocumentRoot must be an HTMLHtmlElement"
|
||||
);
|
||||
}
|
||||
|
||||
let formLike = {
|
||||
action: aDocumentRoot.baseURI,
|
||||
autocomplete: "on",
|
||||
ownerDocument: aDocumentRoot.ownerDocument,
|
||||
rootElement: aDocumentRoot,
|
||||
};
|
||||
|
||||
// FormLikes can be created when fields are inserted into the DOM. When
|
||||
// many, many fields are inserted one after the other, we create many
|
||||
// FormLikes, and computing the elements list becomes more and more
|
||||
// expensive. Making the elements list lazy means that it'll only
|
||||
// be computed when it's eventually needed (if ever).
|
||||
ChromeUtils.defineLazyGetter(formLike, "elements", function () {
|
||||
let elements = [];
|
||||
for (let el of aDocumentRoot.querySelectorAll("input, select")) {
|
||||
// Exclude elements inside the rootElement that are already in a <form> as
|
||||
// they will be handled by their own FormLike.
|
||||
if (!el.form) {
|
||||
elements.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
});
|
||||
|
||||
this._addToJSONProperty(formLike);
|
||||
return formLike;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a FormLike object from an <input>/<select> in a document.
|
||||
*
|
||||
@@ -61,40 +108,10 @@ export let FormLikeFactory = {
|
||||
throw new Error("createFromField requires a field in a document");
|
||||
}
|
||||
|
||||
let rootElement = this.findRootForField(aField);
|
||||
if (HTMLFormElement.isInstance(rootElement)) {
|
||||
return this.createFromForm(rootElement);
|
||||
}
|
||||
|
||||
let doc = aField.ownerDocument;
|
||||
|
||||
let formLike = {
|
||||
action: doc.baseURI,
|
||||
autocomplete: "on",
|
||||
ownerDocument: doc,
|
||||
rootElement,
|
||||
};
|
||||
|
||||
// FormLikes can be created when fields are inserted into the DOM. When
|
||||
// many, many fields are inserted one after the other, we create many
|
||||
// FormLikes, and computing the elements list becomes more and more
|
||||
// expensive. Making the elements list lazy means that it'll only
|
||||
// be computed when it's eventually needed (if ever).
|
||||
ChromeUtils.defineLazyGetter(formLike, "elements", function () {
|
||||
let elements = [];
|
||||
for (let el of this.rootElement.querySelectorAll("input, select")) {
|
||||
// Exclude elements inside the rootElement that are already in a <form> as
|
||||
// they will be handled by their own FormLike.
|
||||
if (!el.form) {
|
||||
elements.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
});
|
||||
|
||||
this._addToJSONProperty(formLike);
|
||||
return formLike;
|
||||
const rootElement = this.findRootForField(aField);
|
||||
return HTMLFormElement.isInstance(rootElement)
|
||||
? this.createFromForm(rootElement)
|
||||
: this.createFromDocumentRoot(rootElement);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user