1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-21 11:35:34 +01:00

[PM-5670] Autofill not triggering correctly when DOM mutates element with nested form fields (#7518)

This commit is contained in:
Cesar Gonzalez 2024-01-17 10:21:55 -06:00 committed by GitHub
parent 1c8ab3900c
commit c85b43371a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 71 additions and 38 deletions

View File

@ -7,12 +7,24 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { OverlayCipherData } from "../background/abstractions/overlay.background"; import { OverlayCipherData } from "../background/abstractions/overlay.background";
import AutofillField from "../models/autofill-field"; import AutofillField from "../models/autofill-field";
import AutofillForm from "../models/autofill-form";
import AutofillPageDetails from "../models/autofill-page-details"; import AutofillPageDetails from "../models/autofill-page-details";
import AutofillScript, { FillScript } from "../models/autofill-script"; import AutofillScript, { FillScript } from "../models/autofill-script";
import { InitAutofillOverlayButtonMessage } from "../overlay/abstractions/autofill-overlay-button"; import { InitAutofillOverlayButtonMessage } from "../overlay/abstractions/autofill-overlay-button";
import { InitAutofillOverlayListMessage } from "../overlay/abstractions/autofill-overlay-list"; import { InitAutofillOverlayListMessage } from "../overlay/abstractions/autofill-overlay-list";
import { GenerateFillScriptOptions, PageDetail } from "../services/abstractions/autofill.service"; import { GenerateFillScriptOptions, PageDetail } from "../services/abstractions/autofill.service";
function createAutofillFormMock(customFields = {}): AutofillForm {
return {
opid: "default-form-opid",
htmlID: "default-htmlID",
htmlAction: "default-htmlAction",
htmlMethod: "default-htmlMethod",
htmlName: "default-htmlName",
...customFields,
};
}
function createAutofillFieldMock(customFields = {}): AutofillField { function createAutofillFieldMock(customFields = {}): AutofillField {
return { return {
opid: "default-input-field-opid", opid: "default-input-field-opid",
@ -258,6 +270,7 @@ function createPortSpyMock(name: string) {
} }
export { export {
createAutofillFormMock,
createAutofillFieldMock, createAutofillFieldMock,
createPageDetailMock, createPageDetailMock,
createAutofillPageDetailsMock, createAutofillPageDetailsMock,

View File

@ -1,5 +1,6 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { createAutofillFieldMock, createAutofillFormMock } from "../jest/autofill-mocks";
import AutofillField from "../models/autofill-field"; import AutofillField from "../models/autofill-field";
import AutofillForm from "../models/autofill-form"; import AutofillForm from "../models/autofill-form";
import { import {
@ -2079,6 +2080,42 @@ describe("CollectAutofillContentService", () => {
); );
}); });
it("removes cached autofill elements that are nested within a removed node", () => {
const form = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
const usernameInput = document.createElement("input") as ElementWithOpId<FormFieldElement>;
usernameInput.setAttribute("type", "text");
usernameInput.setAttribute("name", "username");
form.appendChild(usernameInput);
document.body.appendChild(form);
const removedNodes = document.querySelectorAll("form");
const autofillForm: AutofillForm = createAutofillFormMock({});
const autofillField: AutofillField = createAutofillFieldMock({});
collectAutofillContentService["autofillFormElements"] = new Map([[form, autofillForm]]);
collectAutofillContentService["autofillFieldElements"] = new Map([
[usernameInput, autofillField],
]);
collectAutofillContentService["domRecentlyMutated"] = false;
collectAutofillContentService["noFieldsFound"] = true;
collectAutofillContentService["currentLocationHref"] = window.location.href;
collectAutofillContentService["handleMutationObserverMutation"]([
{
type: "childList",
addedNodes: null,
attributeName: null,
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: null,
removedNodes: removedNodes,
target: document.body,
},
]);
expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0);
expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0);
});
it("will handle updating the autofill element if any attribute mutations are encountered", () => { it("will handle updating the autofill element if any attribute mutations are encountered", () => {
const mutationRecord: MutationRecord = { const mutationRecord: MutationRecord = {
type: "attributes", type: "attributes",
@ -2389,6 +2426,12 @@ describe("CollectAutofillContentService", () => {
}; };
const updatedAttributes = ["action", "name", "id", "method"]; const updatedAttributes = ["action", "name", "id", "method"];
beforeEach(() => {
collectAutofillContentService["autofillFormElements"] = new Map([
[formElement, autofillForm],
]);
});
updatedAttributes.forEach((attribute) => { updatedAttributes.forEach((attribute) => {
it(`will update the ${attribute} value for the form element`, () => { it(`will update the ${attribute} value for the form element`, () => {
jest.spyOn(collectAutofillContentService["autofillFormElements"], "set"); jest.spyOn(collectAutofillContentService["autofillFormElements"], "set");
@ -2454,6 +2497,12 @@ describe("CollectAutofillContentService", () => {
"data-stripe", "data-stripe",
]; ];
beforeEach(() => {
collectAutofillContentService["autofillFieldElements"] = new Map([
[fieldElement, autofillField],
]);
});
updatedAttributes.forEach((attribute) => { updatedAttributes.forEach((attribute) => {
it(`will update the ${attribute} value for the field element`, async () => { it(`will update the ${attribute} value for the field element`, async () => {
jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set"); jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set");
@ -2471,26 +2520,6 @@ describe("CollectAutofillContentService", () => {
}); });
}); });
it("will check the dom element's visibility if the `style` or `class` attribute has updated ", async () => {
jest.spyOn(
collectAutofillContentService["domElementVisibilityService"],
"isFormFieldViewable",
);
const attributes = ["class", "style"];
for (const attribute of attributes) {
await collectAutofillContentService["updateAutofillFieldElementData"](
attribute,
fieldElement,
autofillField,
);
expect(
collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable,
).toBeCalledWith(fieldElement);
}
});
it("will not update an attribute value if it is not present in the updateActions object", async () => { it("will not update an attribute value if it is not present in the updateActions object", async () => {
jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set"); jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set");

View File

@ -1029,19 +1029,14 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
continue; continue;
} }
if (node instanceof HTMLFormElement || this.isNodeFormFieldElement(node)) { const autofillElementNodes = this.queryAllTreeWalkerNodes(
isElementMutated = true;
mutatedElements.push(node);
continue;
}
const childNodes = this.queryAllTreeWalkerNodes(
node, node,
(node: Node) => node instanceof HTMLFormElement || this.isNodeFormFieldElement(node), (node: Node) => node instanceof HTMLFormElement || this.isNodeFormFieldElement(node),
) as HTMLElement[]; ) as HTMLElement[];
if (childNodes.length) {
if (autofillElementNodes.length) {
isElementMutated = true; isElementMutated = true;
mutatedElements.push(...childNodes); mutatedElements.push(...autofillElementNodes);
} }
} }
@ -1182,7 +1177,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
} }
updateActions[attributeName](); updateActions[attributeName]();
this.autofillFormElements.set(element, dataTarget); if (this.autofillFormElements.has(element)) {
this.autofillFormElements.set(element, dataTarget);
}
} }
/** /**
@ -1233,15 +1230,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
updateActions[attributeName](); updateActions[attributeName]();
const visibilityAttributesSet = new Set(["class", "style"]); if (this.autofillFieldElements.has(element)) {
if ( this.autofillFieldElements.set(element, dataTarget);
visibilityAttributesSet.has(attributeName) &&
!dataTarget.htmlClass?.includes("com-bitwarden-browser-animated-fill")
) {
dataTarget.viewable = await this.domElementVisibilityService.isFormFieldViewable(element);
} }
this.autofillFieldElements.set(element, dataTarget);
} }
/** /**