Bug 1659217 - Ensure formLike.rootElement correctly points to the nearest HTMLFormElement ancestor, if any, when a password field is inside a ShadowRoot. r=MattN

Differential Revision: https://phabricator.services.mozilla.com/D87335
This commit is contained in:
Bianca Danforth 2020-09-02 01:09:08 +00:00
Родитель b68673dd69
Коммит bbe50878af
12 изменённых файлов: 492 добавлений и 6 удалений

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

@ -46,7 +46,7 @@ this.LoginFormFactory = {
/**
* Maps all DOM content documents in this content process, including those in
* frames, to a WeakSet of LoginForm for the document.
* frames, to a WeakSet of LoginForm.rootElement for the document.
*/
_loginFormRootElementsByDocument: new WeakMap(),
@ -104,8 +104,10 @@ this.LoginFormFactory = {
);
}
if (aField.form) {
return this.createFromForm(aField.form);
let form =
aField.form || FormLikeFactory.closestFormIgnoringShadowRoots(aField);
if (form) {
return this.createFromForm(form);
} else if (aField.hasAttribute("form")) {
log.debug(
"createFromField: field has form attribute but no form: ",

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

@ -0,0 +1,31 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Simple form with username and password fields together in a shadow root with a <form> ancestor -->
<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
<form id="both-fields-together-in-a-shadow-root">
<!-- username and password inputs generated programmatically below -->
<input id="submit" type="submit">
</form>
<script>
const form = document.getElementById("both-fields-together-in-a-shadow-root");
const submitButton = document.getElementById("submit");
const wrapper = document.createElement("span");
wrapper.id = "wrapper-un-and-pw";
const shadow = wrapper.attachShadow({mode: "closed"});
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = field;
inputEle.name = field;
if (field === "password") {
inputEle.type = field;
}
shadow.append(inputEle);
}
submitButton.before(wrapper);
</script>
</body></html>

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

@ -0,0 +1,33 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Simple form with form, username and password fields together in a shadow root -->
<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
<span id="wrapper">
<!-- form and all inputs generated programmatically below -->
</span>
<script>
const wrapper = document.getElementById("wrapper");
const shadow = wrapper.attachShadow({mode: "closed"});
const form = document.createElement("form");
form.id = "form-and-fields-in-a-shadow-root";
const submitButton = document.createElement("input");
submitButton.id = "submit";
submitButton.type = "submit";
shadow.append(form);
form.append(submitButton);
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = field;
inputEle.name = field;
if (field === "password") {
inputEle.type = field;
}
submitButton.before(inputEle);
}
</script>
</body></html>

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

@ -0,0 +1,34 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Simple form with username and password fields together in nested shadow roots -->
<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
<form id="each-field-its-own-shadow">
<span id="outer-wrapper">
<!-- username and password inputs generated programmatically below -->
</span>
<input id="submit" type="submit">
</form>
<script>
const submitButton = document.getElementById("submit");
const innerWrapper = document.createElement("span");
innerWrapper.id = "inner-wrapper";
const innerShadow = innerWrapper.attachShadow({mode: "closed"});
const outerWrapper = document.getElementById("outer-wrapper");
const outerShadow = outerWrapper.attachShadow({mode: "closed"});
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = field;
inputEle.name = field;
if (field === "password") {
inputEle.type = field;
}
innerShadow.append(inputEle);
}
outerShadow.append(innerWrapper);
</script>
</body></html>

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

@ -0,0 +1,37 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Simple form with form, username and password fields together in nested shadow roots -->
<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
<span id="outer-wrapper">
<!-- form and all inputs generated programmatically below -->
</span>
<script>
const outerWrapper = document.getElementById("outer-wrapper");
const innerWrapper = document.createElement("span");
innerWrapper.id = "inner-wrapper";
const innerShadow = innerWrapper.attachShadow({mode: "closed"});
const outerShadow = outerWrapper.attachShadow({mode: "closed"});
const form = document.createElement("form");
form.id = "form-and-fields-in-a-shadow-root";
const submitButton = document.createElement("input");
submitButton.id = "submit";
submitButton.type = "submit";
innerShadow.append(form);
form.append(submitButton);
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = field;
inputEle.name = field;
if (field === "password") {
inputEle.type = field;
}
submitButton.before(inputEle);
}
outerShadow.append(innerWrapper);
</script>
</body></html>

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

@ -0,0 +1,28 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Simple form with username and password fields together in a shadow root -->
<!-- This form is based off of toolkit/components/passwordmgr/test/browser/formless_basic.html -->
<!-- username and password inputs generated programmatically below -->
<input id="submit" type="submit">
<script>
const submitButton = document.getElementById("submit");
const wrapper = document.createElement("span");
wrapper.id = "wrapper-un-and-pw";
const shadow = wrapper.attachShadow({mode: "closed"});
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = field;
inputEle.name = field;
if (field === "password") {
inputEle.type = field;
}
shadow.append(inputEle);
}
submitButton.before(wrapper);
</script>
</body></html>

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

@ -8,7 +8,6 @@
<input id="submit" type="submit">
<script>
const { body } = document;
const submitButton = document.getElementById("submit");
const fields = ["username", "password"];
for (let field of fields) {

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

@ -0,0 +1,30 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Simple form with form, username and password fields together in a shadow root -->
<!-- This form is based off of toolkit/components/passwordmgr/test/browser/formless_basic.html -->
<span id="wrapper">
</span>
<!-- username, password and submit inputs generated programmatically below -->
<script>
const wrapper = document.getElementById("wrapper");
const shadow = wrapper.attachShadow({mode: "closed"});
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = field;
inputEle.name = field;
if (field === "password") {
inputEle.type = field;
}
shadow.append(inputEle);
}
const submitButton = document.createElement("input");
submitButton.id = "submit";
submitButton.type = "submit";
shadow.append(submitButton);
</script>
</body></html>

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

@ -21,9 +21,16 @@ support-files =
../browser/form_same_origin_action.html
auth2/authenticate.sjs
file_history_back.html
form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html
form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html
form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html
form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html
form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html
formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html
formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
multiple_forms_shadow_DOM_all_known_variants.html
pwmgr_common.js
pwmgr_common_parent.js
../authenticate.sjs
@ -172,6 +179,9 @@ skip-if = toolkit == 'android' && debug # bug 1397615
[test_formless_submit_navigation_negative.html]
skip-if = toolkit == 'android' && debug # bug 1397615
|| xorigin # Hangs
[test_formLike_rootElement_with_Shadow_DOM.html]
scheme = https
skip-if = xorigin # Hangs
[test_input_events.html]
[test_input_events_for_identical_values.html]
[test_LoginManagerContent_passwordEditedOrGenerated.html]

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

@ -0,0 +1,111 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Page with multiple forms containing the following Shadow DOM variants: -->
<!-- Case 1: Each field (username and password) in its own shadow root -->
<!-- Case 2: Both fields (username and password) together in a shadow root with a form ancestor -->
<!-- Case 3: Form and fields (username and password) together in a shadow root -->
<span id="outer-wrapper">
</span>
<script>
const outerWrapper = document.getElementById("outer-wrapper");
const outerShadow = outerWrapper.attachShadow({mode: "closed"});
function makeFormlessOuterForm(scenario) {
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = `${field}-${scenario}`;
inputEle.name = `${field}-${scenario}`;
if (field === "password") {
inputEle.type = field;
}
outerShadow.append(inputEle);
}
const submitButton = document.createElement("input");
submitButton.id = `submit-${scenario}`;
submitButton.type = "submit";
outerShadow.append(submitButton);
}
function makeFormEachFieldInItsOwnShadowRoot(scenario) {
const form = document.createElement("form");
form.id = scenario;
const submitButton = document.createElement("input");
submitButton.id = `submit-${scenario}`;
submitButton.type = "submit";
form.append(submitButton);
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = `${field}-${scenario}`;
inputEle.name = `${field}-${scenario}`;
if (field === "password") {
inputEle.type = field;
}
const wrapper = document.createElement("span");
wrapper.id = `wrapper-${field}-${scenario}`;
const shadow = wrapper.attachShadow({mode: "closed"});
shadow.append(inputEle);
submitButton.before(wrapper);
}
outerShadow.append(form);
}
function makeFormBothFieldsTogetherInAShadowRoot(scenario) {
const form = document.createElement("form");
form.id = scenario;
const submitButton = document.createElement("input");
submitButton.id = `submit-${scenario}`;
submitButton.type = "submit";
form.append(submitButton);
const wrapper = document.createElement("span");
wrapper.id = `wrapper-${scenario}`;
const shadow = wrapper.attachShadow({mode: "closed"});
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = `${field}-${scenario}`;
inputEle.name = `${field}-${scenario}`;
if (field === "password") {
inputEle.type = field;
}
shadow.append(inputEle);
}
submitButton.before(wrapper);
outerShadow.append(form);
}
function makeFormFormAndFieldsTogetherInAShadowRoot(scenario) {
const wrapper = document.createElement("span");
wrapper.id = `wrapper-${scenario}`;
const shadow = wrapper.attachShadow({mode: "closed"});
const form = document.createElement("form");
form.id = scenario;
shadow.append(form);
const submitButton = document.createElement("input");
submitButton.id = `submit-${scenario}`;
submitButton.type = "submit";
form.append(submitButton);
const fields = ["username", "password"];
for (let field of fields) {
const inputEle = document.createElement("input");
inputEle.id = field;
inputEle.name = field;
if (field === "password") {
inputEle.type = field;
}
submitButton.before(inputEle);
}
outerShadow.append(wrapper);
}
makeFormlessOuterForm("formless-case-2");
makeFormEachFieldInItsOwnShadowRoot("form-case-1");
makeFormBothFieldsTogetherInAShadowRoot("form-case-2");
makeFormFormAndFieldsTogetherInAShadowRoot("form-case-3");
</script>
</body></html>

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

@ -0,0 +1,149 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test that FormLike.rootElement points to right element when the page has Shadow DOM</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="pwmgr_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
</head>
<body>
<iframe></iframe>
<script type="application/javascript">
const { LoginFormFactory } = SpecialPowers.Cu.import("resource://gre/modules/LoginFormFactory.jsm", {});
add_task(async function setup() {
const readyPromise = registerRunTests();
info("Waiting for setup and page load");
await readyPromise;
// assert that there are no logins
const allLogins = await LoginManager.getAllLogins();
is(allLogins.length, 0, "There are no logins");
});
const IFRAME = document.querySelector("iframe");
const TESTCASES = [
// Check that the Shadow DOM version of form_basic.html works
{
name: "test_form_each_field_in_its_own_shadow_root",
filename: "form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#wrapper-password", "form"]],
},
{
name: "test_form_both_fields_together_in_a_shadow_root",
filename: "form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#wrapper-un-and-pw", "form"]],
},
{
name: "test_form_form_and_fields_together_in_a_shadow_root",
filename: "form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#wrapper", "form"]],
},
// Check that the Shadow DOM version of formless_basic.html works
{
name: "test_formless_each_field_in_its_own_shadow_root",
filename: "formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#wrapper-password", "html"]],
},
{
name: "test_formless_both_fields_together_in_a_shadow_root",
filename: "formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#wrapper-un-and-pw", "html"]],
},
{
name: "test_formless_form_and_fields_together_in_a_shadow_root.html",
filename: "formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#wrapper", "html"]],
},
// Check that the nested Shadow DOM version of form_basic.html works
{
name: "test_form_nested_each_field_in_its_own_shadow_root",
filename: "form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#wrapper-password", "form"]],
outerHostElementSelector: "span#outer-wrapper-password",
},
{
name: "test_form_nested_both_fields_together_in_a_shadow_root",
filename: "form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#inner-wrapper", "form"]],
outerHostElementSelector: "span#outer-wrapper",
},
{
name: "test_form_nested_form_and_fields_together_in_a_shadow_root",
filename: "form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html",
hostAndRootElementSelectorTuples: [["span#inner-wrapper", "form"]],
outerHostElementSelector: "span#outer-wrapper",
},
{
name: "test_multiple_forms_shadow_DOM_all_known_variants",
filename: "multiple_forms_shadow_DOM_all_known_variants.html",
hostAndRootElementSelectorTuples: [
["span#outer-wrapper", "html"],
["span#wrapper-password-form-case-1", "form#form-case-1"],
["span#wrapper-form-case-2", "form#form-case-2"],
["span#wrapper-form-case-3", "form#form-case-3"],
],
outerHostElementSelector: "span#outer-wrapper",
}
];
async function testForm(testcase) {
const iframeLoaded = new Promise(resolve => {
IFRAME.addEventListener(
"load",
function(e) {
resolve(true);
},
{ once: true }
);
});
// This could complete before the page finishes loading.
const numForms = testcase.hostAndRootElementSelectorTuples.length;
const formsProcessed = promiseFormsProcessed(numForms);
IFRAME.src = testcase.filename;
info("Waiting for test page to load in the iframe");
await iframeLoaded;
info(`Wait for ${numForms} form(s) to be processed.`);
await formsProcessed;
const iframeDoc = SpecialPowers.wrap(IFRAME.contentWindow).document;
for (let [hostElementSelector, rootElementSelector] of testcase.hostAndRootElementSelectorTuples) {
info("Get the expected rootElement from the document");
let hostElement = iframeDoc.querySelector(hostElementSelector);
let outerShadowRoot = null;
if (!hostElement) {
// Nested Shadow DOM testcase
const outerHostElement = iframeDoc.querySelector(testcase.outerHostElementSelector);
outerShadowRoot = outerHostElement.openOrClosedShadowRoot;
hostElement = outerShadowRoot.querySelector(hostElementSelector);
}
const shadowRoot = hostElement.openOrClosedShadowRoot;
let expectedRootElement = iframeDoc.querySelector(rootElementSelector);
if (!expectedRootElement) {
// The form itself is inside a ShadowRoot and/or there is a ShadowRoot in between the field and form
expectedRootElement =
shadowRoot.querySelector(rootElementSelector) ||
outerShadowRoot.querySelector(rootElementSelector);
}
ok(LoginFormFactory.getRootElementsWeakSetForDocument(iframeDoc).has(expectedRootElement), "Ensure formLike.rootElement has the expected value");
}
}
for (let testcase of TESTCASES) {
const taskName = testcase.name;
const tmp = {
async [taskName]() {
await testForm(testcase);
}
}
add_task(tmp[taskName]);
}
</script>
</body>
</html>

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

@ -105,6 +105,27 @@ let FormLikeFactory = {
return formLike;
},
/**
* Find the closest <form> if any when aField is inside a ShadowRoot.
*
* @param {HTMLInputElement} aField - a password or username field in a document
* @return {HTMLFormElement|null}
*/
closestFormIgnoringShadowRoots(aField) {
let form = aField.closest("form");
let current = aField;
while (!form) {
let shadowRoot = current.getRootNode();
if (ChromeUtils.getClassName(shadowRoot) !== "ShadowRoot") {
break;
}
let host = shadowRoot.host;
form = host.closest("form");
current = host;
}
return form;
},
/**
* Determine the Element that encapsulates the related fields. For example, if
* a page contains a login form and a checkout form which are "submitted"
@ -116,8 +137,9 @@ let FormLikeFactory = {
* @return {HTMLElement} - the root element surrounding related fields
*/
findRootForField(aField) {
if (aField.form) {
return aField.form;
let form = aField.form || this.closestFormIgnoringShadowRoots(aField);
if (form) {
return form;
}
return aField.ownerDocument.documentElement;