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) {
|
void HTMLFormElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
|
||||||
if (aEvent->mEventType == u"DOMFormHasPassword"_ns) {
|
if (aEvent->mEventType == u"DOMFormHasPassword"_ns) {
|
||||||
mHasPendingPasswordEvent = false;
|
mHasPendingPasswordEvent = false;
|
||||||
} else if (aEvent->mEventType == u"DOMFormHasPossibleUsername"_ns) {
|
} else if (aEvent->mEventType == u"DOMPossibleUsernameInputAdded"_ns) {
|
||||||
mHasPendingPossibleUsernameEvent = false;
|
mHasPendingPossibleUsernameEvent = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ class HTMLFormElement final : public nsGenericHTMLElement {
|
|||||||
|
|
||||||
/** Whether we already dispatched a DOMFormHasPassword event or not */
|
/** Whether we already dispatched a DOMFormHasPassword event or not */
|
||||||
bool mHasPendingPasswordEvent = false;
|
bool mHasPendingPasswordEvent = false;
|
||||||
/** Whether we already dispatched a DOMFormHasPossibleUsername event or not */
|
/** Whether we already dispatched a DOMPossibleUsernameInputAdded event or not
|
||||||
|
*/
|
||||||
bool mHasPendingPossibleUsernameEvent = false;
|
bool mHasPendingPossibleUsernameEvent = false;
|
||||||
|
|
||||||
// nsIContent
|
// nsIContent
|
||||||
|
|||||||
@@ -4415,7 +4415,7 @@ void HTMLInputElement::MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nsString eventType;
|
nsString eventType;
|
||||||
Element* target = nullptr;
|
EventTarget* target = nullptr;
|
||||||
|
|
||||||
if (mType == FormControlType::InputPassword) {
|
if (mType == FormControlType::InputPassword) {
|
||||||
// Don't fire another event if we have a pending event.
|
// 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.
|
// TODO(Bug 1864404): Use one event for formless and form inputs.
|
||||||
eventType = aForm ? u"DOMFormHasPassword"_ns : u"DOMInputPasswordAdded"_ns;
|
eventType = aForm ? u"DOMFormHasPassword"_ns : u"DOMInputPasswordAdded"_ns;
|
||||||
|
|
||||||
target = aForm ? static_cast<Element*>(aForm) : this;
|
|
||||||
|
|
||||||
if (aForm) {
|
if (aForm) {
|
||||||
|
target = aForm;
|
||||||
aForm->mHasPendingPasswordEvent = true;
|
aForm->mHasPendingPasswordEvent = true;
|
||||||
|
} else {
|
||||||
|
target = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (mType == FormControlType::InputEmail ||
|
} else if (mType == FormControlType::InputEmail ||
|
||||||
mType == FormControlType::InputText) {
|
mType == FormControlType::InputText) {
|
||||||
// Don't fire a username event if:
|
// Don't fire a username event if:
|
||||||
// - <input> is not part of a form
|
|
||||||
// - we have a pending event
|
// - we have a pending event
|
||||||
// - username only forms are not supported
|
// - username only forms are not supported
|
||||||
if (!aForm || aForm->mHasPendingPossibleUsernameEvent ||
|
// fire event if we have a username field without a form with the
|
||||||
!StaticPrefs::signon_usernameOnlyForm_enabled()) {
|
// autcomplete value of username
|
||||||
|
|
||||||
|
if (!StaticPrefs::signon_usernameOnlyForm_enabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventType = u"DOMFormHasPossibleUsername"_ns;
|
if (aForm) {
|
||||||
target = aForm;
|
if (aForm->mHasPendingPossibleUsernameEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
aForm->mHasPendingPossibleUsernameEvent = true;
|
aForm->mHasPendingPossibleUsernameEvent = true;
|
||||||
|
target = aForm;
|
||||||
|
} else {
|
||||||
|
nsAutoString autocompleteValue;
|
||||||
|
GetAutocomplete(autocompleteValue);
|
||||||
|
if (!autocompleteValue.EqualsASCII("username")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target = GetComposedDoc();
|
||||||
|
}
|
||||||
|
eventType = u"DOMPossibleUsernameInputAdded"_ns;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,18 +49,33 @@ export const LoginFormFactory = {
|
|||||||
let formLike = lazy.FormLikeFactory.createFromForm(aForm);
|
let formLike = lazy.FormLikeFactory.createFromForm(aForm);
|
||||||
formLike.action = lazy.LoginHelper.getFormActionOrigin(aForm);
|
formLike.action = lazy.LoginHelper.getFormActionOrigin(aForm);
|
||||||
|
|
||||||
let rootElementsSet = this.getRootElementsWeakSetForDocument(
|
this._addLoginFormToRootElementsSet(formLike);
|
||||||
formLike.ownerDocument
|
|
||||||
);
|
return formLike;
|
||||||
rootElementsSet.add(formLike.rootElement);
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
lazy.log.debug(
|
||||||
"adding",
|
"Created non-form LoginForm for rootElement:",
|
||||||
formLike.rootElement,
|
aDocumentRoot
|
||||||
"to root elements for",
|
|
||||||
formLike.ownerDocument
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this._loginFormsByRootElement.set(formLike.rootElement, formLike);
|
this._addLoginFormToRootElementsSet(formLike);
|
||||||
|
|
||||||
return formLike;
|
return formLike;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -113,19 +128,7 @@ export const LoginFormFactory = {
|
|||||||
aField.ownerDocument.documentElement
|
aField.ownerDocument.documentElement
|
||||||
);
|
);
|
||||||
|
|
||||||
let rootElementsSet = this.getRootElementsWeakSetForDocument(
|
this._addLoginFormToRootElementsSet(formLike);
|
||||||
formLike.ownerDocument
|
|
||||||
);
|
|
||||||
rootElementsSet.add(formLike.rootElement);
|
|
||||||
lazy.log.debug(
|
|
||||||
"adding",
|
|
||||||
formLike.rootElement,
|
|
||||||
"to root elements for",
|
|
||||||
formLike.ownerDocument
|
|
||||||
);
|
|
||||||
|
|
||||||
this._loginFormsByRootElement.set(formLike.rootElement, formLike);
|
|
||||||
|
|
||||||
return formLike;
|
return formLike;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -145,4 +148,19 @@ export const LoginFormFactory = {
|
|||||||
setForRootElement(aRootElement, aLoginForm) {
|
setForRootElement(aRootElement, aLoginForm) {
|
||||||
return this._loginFormsByRootElement.set(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.
|
* 2. Only contains one input field whose type is username compatible.
|
||||||
* 3. The username compatible input field looks like a username field
|
* 3. The username compatible input field looks like a username field
|
||||||
* or the form itself looks like a sign-in or sign-up form.
|
* 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.
|
* the form to check.
|
||||||
* @param {Object} recipe=null
|
* @param {Object} recipe=null
|
||||||
* A relevant field override recipe to use.
|
* A relevant field override recipe to use.
|
||||||
* @returns {Element} The username field or null (if the form is not a
|
* @returns {Element} The username field or null (if the form is not a
|
||||||
* username-only form).
|
* username-only form).
|
||||||
*/
|
*/
|
||||||
getUsernameFieldFromUsernameOnlyForm(formElement, recipe = null) {
|
getUsernameFieldFromUsernameOnlyForm(form, recipe = null) {
|
||||||
if (!HTMLFormElement.isInstance(formElement)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let candidate = 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
|
// 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.
|
// field in the form, this is NOT a username-only form.
|
||||||
if (element.hasBeenTypePassword) {
|
if (element.hasBeenTypePassword) {
|
||||||
@@ -811,10 +811,9 @@ export class LoginFormState {
|
|||||||
}
|
}
|
||||||
candidate = element;
|
candidate = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
candidate &&
|
candidate &&
|
||||||
this.#isProbablyAUsernameLoginForm(formElement, candidate)
|
this.#isProbablyAUsernameLoginForm(form.rootElement, candidate)
|
||||||
) {
|
) {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
@@ -1109,7 +1108,7 @@ export class LoginFormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
usernameField = this.getUsernameFieldFromUsernameOnlyForm(
|
usernameField = this.getUsernameFieldFromUsernameOnlyForm(
|
||||||
form.rootElement,
|
form,
|
||||||
fieldOverrideRecipe
|
fieldOverrideRecipe
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1525,8 +1524,8 @@ export class LoginManagerChild extends JSWindowActorChild {
|
|||||||
lazy.InsecurePasswordUtils.reportInsecurePasswords(formLike);
|
lazy.InsecurePasswordUtils.reportInsecurePasswords(formLike);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "DOMFormHasPossibleUsername": {
|
case "DOMPossibleUsernameInputAdded": {
|
||||||
this.#onDOMFormHasPossibleUsername(event);
|
this.#onDOMPossibleUsernameInputAdded(event);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "DOMInputPasswordAdded": {
|
case "DOMInputPasswordAdded": {
|
||||||
@@ -1797,19 +1796,31 @@ export class LoginManagerChild extends JSWindowActorChild {
|
|||||||
this._fetchLoginsFromParentAndFillForm(formLike);
|
this._fetchLoginsFromParentAndFillForm(formLike);
|
||||||
}
|
}
|
||||||
|
|
||||||
#onDOMFormHasPossibleUsername(event) {
|
#onDOMPossibleUsernameInputAdded(event) {
|
||||||
if (!event.isTrusted) {
|
if (!event.isTrusted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isPrimaryPasswordSet = this.#getIsPrimaryPasswordSet();
|
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(
|
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
|
// 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.
|
// bucket 1 & 2.
|
||||||
let docState = this.stateForDocument(document);
|
let docState = this.stateForDocument(document);
|
||||||
|
|
||||||
@@ -1823,19 +1834,16 @@ export class LoginManagerChild extends JSWindowActorChild {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (document.visibilityState == "visible" || isPrimaryPasswordSet) {
|
if (document.visibilityState == "visible" || isPrimaryPasswordSet) {
|
||||||
this._processDOMFormHasPossibleUsernameEvent(event);
|
this._processDOMPossibleUsernameInputAddedEvent(formLike);
|
||||||
} else {
|
} else {
|
||||||
// wait until the document becomes visible before handling this event
|
// wait until the document becomes visible before handling this event
|
||||||
this._deferHandlingEventUntilDocumentVisible(event, document, () => {
|
this._deferHandlingEventUntilDocumentVisible(event, document, () => {
|
||||||
this._processDOMFormHasPossibleUsernameEvent(event);
|
this._processDOMPossibleUsernameInputAddedEvent(formLike);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_processDOMFormHasPossibleUsernameEvent(event) {
|
_processDOMPossibleUsernameInputAddedEvent(formLike) {
|
||||||
let form = event.target;
|
|
||||||
let formLike = lazy.LoginFormFactory.createFromForm(form);
|
|
||||||
|
|
||||||
// If the form contains a passoword field, `getUsernameFieldFromUsernameOnlyForm` returns
|
// If the form contains a passoword field, `getUsernameFieldFromUsernameOnlyForm` returns
|
||||||
// null, so we don't trigger autofill for those forms here. In this function,
|
// 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
|
// 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.
|
// 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
|
// 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.
|
// the site recipe, the form will be skipped while autofilling later.
|
||||||
let docState = this.stateForDocument(form.ownerDocument);
|
let docState = this.stateForDocument(formLike.ownerDocument);
|
||||||
let usernameField = docState.getUsernameFieldFromUsernameOnlyForm(form, {});
|
let usernameField = docState.getUsernameFieldFromUsernameOnlyForm(
|
||||||
|
formLike,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
if (usernameField) {
|
if (usernameField) {
|
||||||
// Autofill the username-only form.
|
// Autofill the username-only form.
|
||||||
lazy.log("A username-only form is found.");
|
lazy.log("A username-only form is found.");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const ids = {
|
|||||||
INPUT_TYPE: "",
|
INPUT_TYPE: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
function task({ contentIds, expected }) {
|
function task({ contentIds, expected, hasForm = true }) {
|
||||||
let resolve;
|
let resolve;
|
||||||
let promise = new Promise(r => {
|
let promise = new Promise(r => {
|
||||||
resolve = r;
|
resolve = r;
|
||||||
@@ -27,14 +27,17 @@ function task({ contentIds, expected }) {
|
|||||||
removeEventListener("load", tabLoad, true);
|
removeEventListener("load", tabLoad, true);
|
||||||
|
|
||||||
gDoc = content.document;
|
gDoc = content.document;
|
||||||
gDoc.addEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
|
gDoc.addEventListener(
|
||||||
addEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
|
"DOMPossibleUsernameInputAdded",
|
||||||
|
unexpectedContentEvent
|
||||||
|
);
|
||||||
|
addEventListener("DOMPossibleUsernameInputAdded", unexpectedContentEvent);
|
||||||
gDoc.defaultView.setTimeout(test_inputAdd, 0);
|
gDoc.defaultView.setTimeout(test_inputAdd, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_inputAdd() {
|
function test_inputAdd() {
|
||||||
if (expected) {
|
if (expected) {
|
||||||
addEventListener("DOMFormHasPossibleUsername", test_inputAddHandler, {
|
addEventListener("DOMPossibleUsernameInputAdded", test_inputAddHandler, {
|
||||||
once: true,
|
once: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
@@ -45,26 +48,46 @@ function task({ contentIds, expected }) {
|
|||||||
input.setAttribute("type", contentIds.INPUT_TYPE);
|
input.setAttribute("type", contentIds.INPUT_TYPE);
|
||||||
input.setAttribute("id", contentIds.INPUT_ID);
|
input.setAttribute("id", contentIds.INPUT_ID);
|
||||||
input.setAttribute("data-test", "unique-attribute");
|
input.setAttribute("data-test", "unique-attribute");
|
||||||
|
if (hasForm) {
|
||||||
gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
|
gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
|
||||||
|
} else {
|
||||||
|
input.setAttribute("autocomplete", "username");
|
||||||
|
gDoc.querySelector("body").appendChild(input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_inputAddHandler(evt) {
|
function test_inputAddHandler(evt) {
|
||||||
if (expected) {
|
if (expected) {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
if (hasForm) {
|
||||||
Assert.equal(
|
Assert.equal(
|
||||||
evt.target.id,
|
evt.target.id,
|
||||||
contentIds.FORM1_ID,
|
contentIds.FORM1_ID,
|
||||||
evt.type +
|
evt.type + " event targets correct username element"
|
||||||
" event targets correct form element (added possible 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() {
|
function test_inputChangeForm() {
|
||||||
if (expected) {
|
if (expected) {
|
||||||
addEventListener(
|
addEventListener(
|
||||||
"DOMFormHasPossibleUsername",
|
"DOMPossibleUsernameInputAdded",
|
||||||
test_inputChangeFormHandler,
|
test_inputChangeFormHandler,
|
||||||
{ once: true, capture: true }
|
{ once: true, capture: true }
|
||||||
);
|
);
|
||||||
@@ -81,7 +104,7 @@ function task({ contentIds, expected }) {
|
|||||||
Assert.equal(
|
Assert.equal(
|
||||||
evt.target.id,
|
evt.target.id,
|
||||||
contentIds.FORM2_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
|
// TODO(Bug 1864405): Refactor this test to not expect a DOM event
|
||||||
@@ -96,7 +119,7 @@ function task({ contentIds, expected }) {
|
|||||||
function test_inputChangesType() {
|
function test_inputChangesType() {
|
||||||
if (expected) {
|
if (expected) {
|
||||||
addEventListener(
|
addEventListener(
|
||||||
"DOMFormHasPossibleUsername",
|
"DOMPossibleUsernameInputAdded",
|
||||||
test_inputChangesTypeHandler,
|
test_inputChangesTypeHandler,
|
||||||
{ once: true, capture: true }
|
{ once: true, capture: true }
|
||||||
);
|
);
|
||||||
@@ -110,19 +133,29 @@ function task({ contentIds, expected }) {
|
|||||||
function test_inputChangesTypeHandler(evt) {
|
function test_inputChangesTypeHandler(evt) {
|
||||||
if (expected) {
|
if (expected) {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
if (hasForm) {
|
||||||
Assert.equal(
|
Assert.equal(
|
||||||
evt.target.id,
|
evt.target.id,
|
||||||
contentIds.FORM1_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);
|
gDoc.defaultView.setTimeout(finish, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
removeEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
|
removeEventListener(
|
||||||
|
"DOMPossibleUsernameInputAdded",
|
||||||
|
unexpectedContentEvent
|
||||||
|
);
|
||||||
gDoc.removeEventListener(
|
gDoc.removeEventListener(
|
||||||
"DOMFormHasPossibleUsername",
|
"DOMPossibleUsernameInputAdded",
|
||||||
unexpectedContentEvent
|
unexpectedContentEvent
|
||||||
);
|
);
|
||||||
resolve();
|
resolve();
|
||||||
@@ -148,20 +181,20 @@ add_task(async function test_disconnectedInputs() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
addEventListener("DOMFormHasPossibleUsername", unexpectedEvent);
|
addEventListener("DOMPossibleUsernameInputAdded", unexpectedEvent);
|
||||||
const form = content.document.createElement("form");
|
const form = content.document.createElement("form");
|
||||||
const textInput = content.document.createElement("input");
|
const textInput = content.document.createElement("input");
|
||||||
textInput.setAttribute("type", "text");
|
textInput.setAttribute("type", "text");
|
||||||
form.appendChild(textInput);
|
form.appendChild(textInput);
|
||||||
|
|
||||||
// Delay the execution for a bit to allow time for any asynchronously
|
// 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,
|
// This is necessary because such events might not be triggered immediately,
|
||||||
// and we want to ensure that if they are dispatched, they are captured
|
// and we want to ensure that if they are dispatched, they are captured
|
||||||
// before we remove the event listener.
|
// before we remove the event listener.
|
||||||
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
removeEventListener("DOMFormHasPossibleUsername", unexpectedEvent);
|
removeEventListener("DOMPossibleUsernameInputAdded", unexpectedEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ok(true, "Test completed");
|
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() {
|
add_task(async function test_nonSupportedInputType() {
|
||||||
for (let type of ["url", "tel", "number"]) {
|
for (let type of ["url", "tel", "number"]) {
|
||||||
let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
|
let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
|
||||||
|
|||||||
@@ -421,10 +421,10 @@ async function checkUnmodifiedFormInFrame(bc, formNum) {
|
|||||||
return SpecialPowers.spawn(bc, [formNum], formNumF => {
|
return SpecialPowers.spawn(bc, [formNum], formNumF => {
|
||||||
let form = this.content.document.getElementById(`form${formNumF}`);
|
let form = this.content.document.getElementById(`form${formNumF}`);
|
||||||
ok(form, "Locating 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++) {
|
for (const ele of elements) {
|
||||||
var ele = form.elements[i];
|
|
||||||
|
|
||||||
// No point in checking form submit/reset buttons.
|
// No point in checking form submit/reset buttons.
|
||||||
if (ele.type == "submit" || ele.type == "reset") {
|
if (ele.type == "submit" || ele.type == "reset") {
|
||||||
continue;
|
continue;
|
||||||
@@ -466,10 +466,15 @@ async function checkLoginFormInFrameWithElementValues(
|
|||||||
|
|
||||||
let numToCheck = arguments.length - 1;
|
let numToCheck = arguments.length - 1;
|
||||||
|
|
||||||
|
const elements =
|
||||||
|
form.nodeName === "form"
|
||||||
|
? form.elements
|
||||||
|
: form.querySelectorAll("input");
|
||||||
|
|
||||||
if (!numToCheck--) {
|
if (!numToCheck--) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e = form.elements[0];
|
e = elements[0];
|
||||||
if (val1F == null) {
|
if (val1F == null) {
|
||||||
is(
|
is(
|
||||||
e.value,
|
e.value,
|
||||||
@@ -488,7 +493,7 @@ async function checkLoginFormInFrameWithElementValues(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e = form.elements[1];
|
e = elements[1];
|
||||||
if (val2F == null) {
|
if (val2F == null) {
|
||||||
is(
|
is(
|
||||||
e.value,
|
e.value,
|
||||||
@@ -506,7 +511,7 @@ async function checkLoginFormInFrameWithElementValues(
|
|||||||
if (!numToCheck--) {
|
if (!numToCheck--) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e = form.elements[2];
|
e = elements[2];
|
||||||
if (val3F == null) {
|
if (val3F == null) {
|
||||||
is(
|
is(
|
||||||
e.value,
|
e.value,
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ Test autofill on username-form
|
|||||||
<script>
|
<script>
|
||||||
add_setup(async () => {
|
add_setup(async () => {
|
||||||
await setStoredLoginsAsync(
|
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");
|
let win = window.open("about:blank");
|
||||||
SimpleTest.registerCleanupFunction(() => win.close());
|
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, `
|
await loadFormIntoWindow(window.location.origin, `
|
||||||
<!-- no password field, 1 username field -->
|
<!-- no password field, 1 username field --> <form id='form1' action='https://autofill'> 1
|
||||||
<form id='form1' action='https://autofill'> 1
|
|
||||||
<input type='text' name='uname' autocomplete='username' value=''>
|
<input type='text' name='uname' autocomplete='username' value=''>
|
||||||
|
|
||||||
<button type='submit'>Submit</button>
|
<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='submit'>Submit</button>
|
||||||
<button type='reset'> Reset </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, 1, "user1");
|
||||||
await checkLoginFormInFrameWithElementValues(win, 2, "someuser");
|
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, 5);
|
||||||
await checkUnmodifiedFormInFrame(win, 6);
|
await checkUnmodifiedFormInFrame(win, 6);
|
||||||
await checkUnmodifiedFormInFrame(win, 7);
|
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();
|
await resetRecipes();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const TESTCASES = [
|
|||||||
{
|
{
|
||||||
description: "1 username field outside of a <form>",
|
description: "1 username field outside of a <form>",
|
||||||
document: `<input id="un1" autocomplete=username>`,
|
document: `<input id="un1" autocomplete=username>`,
|
||||||
returnedFieldIDs: [null, null, null],
|
returnedFieldIDs: ["un1", null, null],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "username with type=user",
|
description: "username with type=user",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ const { LoginManagerChild } = ChromeUtils.importESModule(
|
|||||||
"resource://gre/modules/LoginManagerChild.sys.mjs"
|
"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[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.
|
// expectation[1] tests cases when a form has a sign-in keyword.
|
||||||
const TESTCASES = [
|
const TESTCASES = [
|
||||||
@@ -162,10 +166,11 @@ for (let tc of TESTCASES) {
|
|||||||
form.setAttribute("name", "login");
|
form.setAttribute("name", "login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formLike = FormLikeFactory.createFromForm(form);
|
||||||
const lmc = new LoginManagerChild();
|
const lmc = new LoginManagerChild();
|
||||||
const docState = lmc.stateForDocument(form.ownerDocument);
|
const docState = lmc.stateForDocument(form.ownerDocument);
|
||||||
const element = docState.getUsernameFieldFromUsernameOnlyForm(
|
const element = docState.getUsernameFieldFromUsernameOnlyForm(
|
||||||
form,
|
formLike,
|
||||||
testcase.fieldOverrideRecipe
|
testcase.fieldOverrideRecipe
|
||||||
);
|
);
|
||||||
Assert.strictEqual(
|
Assert.strictEqual(
|
||||||
|
|||||||
@@ -379,7 +379,7 @@ let JSWINDOWACTORS = {
|
|||||||
"form-submission-detected": { createActor: false },
|
"form-submission-detected": { createActor: false },
|
||||||
"before-form-submission": { createActor: false },
|
"before-form-submission": { createActor: false },
|
||||||
DOMFormHasPassword: {},
|
DOMFormHasPassword: {},
|
||||||
DOMFormHasPossibleUsername: {},
|
DOMPossibleUsernameInputAdded: {},
|
||||||
DOMInputPasswordAdded: {},
|
DOMInputPasswordAdded: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,53 @@ export let FormLikeFactory = {
|
|||||||
return formLike;
|
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.
|
* 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");
|
throw new Error("createFromField requires a field in a document");
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootElement = this.findRootForField(aField);
|
const rootElement = this.findRootForField(aField);
|
||||||
if (HTMLFormElement.isInstance(rootElement)) {
|
return HTMLFormElement.isInstance(rootElement)
|
||||||
return this.createFromForm(rootElement);
|
? this.createFromForm(rootElement)
|
||||||
}
|
: this.createFromDocumentRoot(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;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user