From dafb251cacfcaed0a7a44e6c8ce4d96737ef2bf9 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Thu, 7 Dec 2023 16:23:42 -0600 Subject: [PATCH] [PM-4923] Form elements that fade into view contain incorrectly cached page details (#6953) * [PM-4923] Form Elements that Fade into View Contain Incorrectly Cached Page Details * [PM-4923] Form Elements that Fade into View Contain Incorrectly Cached Page Details * [PM-4923] Running prettier on implementation --- .../collect-autofill-content.service.spec.ts | 77 +++++++++++++++++++ .../collect-autofill-content.service.ts | 54 ++++++++++--- 2 files changed, 120 insertions(+), 11 deletions(-) diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index 8bb252d1c57..e300b65acc2 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -2051,6 +2051,83 @@ describe("CollectAutofillContentService", () => { collectAutofillContentService["handleAutofillElementAttributeMutation"], ).not.toBeCalled(); }); + + it("will setup the overlay listeners on mutated elements", async () => { + jest.useFakeTimers(); + const form = document.createElement("form"); + document.body.appendChild(form); + const addedNodes = document.querySelectorAll("form"); + const removedNodes = document.querySelectorAll("li"); + const mutationRecord: MutationRecord = { + type: "childList", + addedNodes: addedNodes, + attributeName: null, + attributeNamespace: null, + nextSibling: null, + oldValue: null, + previousSibling: null, + removedNodes: removedNodes, + target: document.body, + }; + collectAutofillContentService["domRecentlyMutated"] = false; + collectAutofillContentService["noFieldsFound"] = true; + collectAutofillContentService["currentLocationHref"] = window.location.href; + jest.spyOn(collectAutofillContentService as any, "setupOverlayListenersOnMutatedElements"); + + collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]); + jest.runAllTimers(); + + expect(collectAutofillContentService["setupOverlayListenersOnMutatedElements"]).toBeCalled(); + }); + }); + + describe("setupOverlayListenersOnMutatedElements", () => { + it("skips building the autofill field item if the node is not a form field element", () => { + const divElement = document.createElement("div"); + const nodes = [divElement]; + jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldItem"); + + collectAutofillContentService["setupOverlayListenersOnMutatedElements"](nodes); + + expect(collectAutofillContentService["buildAutofillFieldItem"]).not.toBeCalled(); + }); + + it("skips building the autofill field item if the node is already a field element", () => { + const inputElement = document.createElement("input") as ElementWithOpId; + inputElement.setAttribute("type", "password"); + const nodes = [inputElement]; + collectAutofillContentService["autofillFieldElements"].set(inputElement, { + opid: "1234", + } as AutofillField); + jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldItem"); + + collectAutofillContentService["setupOverlayListenersOnMutatedElements"](nodes); + + expect(collectAutofillContentService["buildAutofillFieldItem"]).not.toBeCalled(); + }); + + it("builds the autofill field item to ensure the overlay listeners are set", () => { + document.body.innerHTML = ` +
+ + +
+ `; + + const inputElement = document.getElementById( + "username-id", + ) as ElementWithOpId; + inputElement.setAttribute("type", "password"); + const nodes = [inputElement]; + jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldItem"); + + collectAutofillContentService["setupOverlayListenersOnMutatedElements"](nodes); + + expect(collectAutofillContentService["buildAutofillFieldItem"]).toBeCalledWith( + inputElement, + -1, + ); + }); }); describe("deleteCachedAutofillElement", () => { diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 66a699618ee..d675a37921d 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -303,7 +303,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte element.opid = `__${index}`; const existingAutofillField = this.autofillFieldElements.get(element); - if (existingAutofillField) { + if (index >= 0 && existingAutofillField) { existingAutofillField.opid = element.opid; existingAutofillField.elementNumber = index; this.autofillFieldElements.set(element, existingAutofillField); @@ -325,7 +325,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte }; if (element instanceof HTMLSpanElement) { - this.autofillFieldElements.set(element, autofillFieldBase); + this.cacheAutofillFieldElement(index, element, autofillFieldBase); this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( element, autofillFieldBase, @@ -366,11 +366,31 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte "data-stripe": this.getPropertyOrAttribute(element, "data-stripe"), }; - this.autofillFieldElements.set(element, autofillField); + this.cacheAutofillFieldElement(index, element, autofillField); this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField(element, autofillField); return autofillField; }; + /** + * Caches the autofill field element and its data. + * Will not cache the element if the index is less than 0. + * + * @param index - The index of the autofill field element + * @param element - The autofill field element to cache + * @param autofillFieldData - The autofill field data to cache + */ + private cacheAutofillFieldElement( + index: number, + element: ElementWithOpId, + autofillFieldData: AutofillField, + ) { + if (index < 0) { + return; + } + + this.autofillFieldElements.set(element, autofillFieldData); + } + /** * Identifies the autocomplete attribute associated with an element and returns * the value of the attribute if it is not set to "off". @@ -987,7 +1007,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } let isElementMutated = false; - const mutatedElements = []; + const mutatedElements: Node[] = []; for (let index = 0; index < nodes.length; index++) { const node = nodes[index]; if (!(node instanceof HTMLElement)) { @@ -1010,17 +1030,31 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } } - for (let elementIndex = 0; elementIndex < mutatedElements.length; elementIndex++) { - const node = mutatedElements[elementIndex]; - if (isRemovingNodes) { + if (isRemovingNodes) { + for (let elementIndex = 0; elementIndex < mutatedElements.length; elementIndex++) { + const node = mutatedElements[elementIndex]; this.deleteCachedAutofillElement( node as ElementWithOpId | ElementWithOpId, ); - continue; } + } else if (this.autofillOverlayContentService) { + setTimeout(() => this.setupOverlayListenersOnMutatedElements(mutatedElements), 1000); + } + return isElementMutated; + } + + /** + * Sets up the overlay listeners on the passed mutated elements. This ensures + * that the overlay can appear on elements that are injected into the DOM after + * the initial page load. + * + * @param mutatedElements - HTML elements that have been mutated + */ + private setupOverlayListenersOnMutatedElements(mutatedElements: Node[]) { + for (let elementIndex = 0; elementIndex < mutatedElements.length; elementIndex++) { + const node = mutatedElements[elementIndex]; if ( - this.autofillOverlayContentService && this.isNodeFormFieldElement(node) && !this.autofillFieldElements.get(node as ElementWithOpId) ) { @@ -1029,8 +1063,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte this.buildAutofillFieldItem(node as ElementWithOpId, -1); } } - - return isElementMutated; } /**