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 66747ba5173..68df1623847 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 @@ -1967,6 +1967,14 @@ describe("CollectAutofillContentService", () => { }); describe("getShadowRoot", () => { + beforeEach(() => { + // eslint-disable-next-line + // @ts-ignore + globalThis.chrome.dom = { + openOrClosedShadowRoot: jest.fn(), + }; + }); + it("returns null if the passed node is not an HTMLElement instance", () => { const textNode = document.createTextNode("Hello, world!"); const shadowRoot = collectAutofillContentService["getShadowRoot"](textNode); @@ -1974,12 +1982,27 @@ describe("CollectAutofillContentService", () => { expect(shadowRoot).toEqual(null); }); - it("returns a value provided by Chrome's openOrClosedShadowRoot API", () => { + it("returns null if the passed node contains children elements", () => { + const element = document.createElement("div"); + element.innerHTML = "

Hello, world!

"; + const shadowRoot = collectAutofillContentService["getShadowRoot"](element); + // eslint-disable-next-line // @ts-ignore - globalThis.chrome.dom = { - openOrClosedShadowRoot: jest.fn(), - }; + expect(chrome.dom.openOrClosedShadowRoot).not.toBeCalled(); + expect(shadowRoot).toEqual(null); + }); + + it("returns an open shadow root if the passed node has a shadowDOM element", () => { + const element = document.createElement("div"); + element.attachShadow({ mode: "open" }); + + const shadowRoot = collectAutofillContentService["getShadowRoot"](element); + + expect(shadowRoot).toBeInstanceOf(ShadowRoot); + }); + + it("returns a value provided by Chrome's openOrClosedShadowRoot API", () => { const element = document.createElement("div"); collectAutofillContentService["getShadowRoot"](element); 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 20c53822bca..26eb79a4b88 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -28,6 +28,14 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private mutationObserver: MutationObserver; private updateAutofillElementsAfterMutationTimeout: NodeJS.Timeout; private readonly updateAfterMutationTimeoutDelay = 1000; + private readonly ignoredInputTypes = new Set([ + "hidden", + "submit", + "reset", + "button", + "image", + "file", + ]); constructor( domElementVisibilityService: DomElementVisibilityService, @@ -339,7 +347,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte tagName: this.getAttributeLowerCase(element, "tagName"), }; - if (element instanceof HTMLSpanElement) { + if (element.tagName.toLowerCase() === "span") { this.cacheAutofillFieldElement(index, element, autofillFieldBase); this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( element, @@ -352,7 +360,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte const elementType = this.getAttributeLowerCase(element, "type"); if (elementType !== "hidden") { autofillFieldLabels = { - "label-tag": this.createAutofillFieldLabelTag(element), + "label-tag": this.createAutofillFieldLabelTag(element as FillableFormFieldElement), "label-data": this.getPropertyOrAttribute(element, "data-label"), "label-aria": this.getPropertyOrAttribute(element, "aria-label"), "label-top": this.createAutofillFieldTopLabel(element), @@ -362,6 +370,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte }; } + const fieldFormElement = (element as ElementWithOpId).form; const autofillField = { ...autofillFieldBase, ...autofillFieldLabels, @@ -373,8 +382,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte disabled: this.getAttributeBoolean(element, "disabled"), readonly: this.getAttributeBoolean(element, "readonly"), selectInfo: - element instanceof HTMLSelectElement ? this.getSelectElementOptions(element) : null, - form: element.form ? this.getPropertyOrAttribute(element.form, "opid") : null, + element.tagName.toLowerCase() === "select" + ? this.getSelectElementOptions(element as HTMLSelectElement) + : null, + form: fieldFormElement ? this.getPropertyOrAttribute(fieldFormElement, "opid") : null, "aria-hidden": this.getAttributeBoolean(element, "aria-hidden", true), "aria-disabled": this.getAttributeBoolean(element, "aria-disabled", true), "aria-haspopup": this.getAttributeBoolean(element, "aria-haspopup", true), @@ -487,7 +498,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte let currentElement: HTMLElement | null = element; while (currentElement && currentElement !== document.documentElement) { - if (currentElement instanceof HTMLLabelElement) { + if (currentElement.tagName.toLowerCase() === "label") { labelElementsSet.add(currentElement); } @@ -562,10 +573,13 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private getAutofillFieldMaxLength(element: FormFieldElement): number | null { - const elementHasMaxLengthProperty = - element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement; + const elementTagName = element.tagName.toLowerCase(); + const elementHasMaxLengthProperty = elementTagName === "input" || elementTagName === "textarea"; const elementMaxLength = - elementHasMaxLengthProperty && element.maxLength > -1 ? element.maxLength : 999; + elementHasMaxLengthProperty && + (element as HTMLInputElement | HTMLTextAreaElement).maxLength > -1 + ? (element as HTMLInputElement | HTMLTextAreaElement).maxLength + : 999; return elementHasMaxLengthProperty ? Math.min(elementMaxLength, 999) : null; } @@ -775,13 +789,13 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private getElementValue(element: FormFieldElement): string { - if (element instanceof HTMLSpanElement) { + if (element.tagName.toLowerCase() === "span") { const spanTextContent = element.textContent || element.innerText; return spanTextContent || ""; } - const elementValue = element.value || ""; - const elementType = String(element.type).toLowerCase(); + const elementValue = (element as FillableFormFieldElement).value || ""; + const elementType = String((element as FillableFormFieldElement).type).toLowerCase(); if ("checked" in element && elementType === "checkbox") { return element.checked ? "✓" : ""; } @@ -832,7 +846,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte const formElements: Node[] = []; const formFieldElements: Node[] = []; this.queryAllTreeWalkerNodes(document.documentElement, (node: Node) => { - if (node instanceof HTMLFormElement) { + if ((node as HTMLFormElement).tagName?.toLowerCase() === "form") { formElements.push(node); return true; } @@ -855,40 +869,49 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private isNodeFormFieldElement(node: Node): boolean { + if (!(node instanceof HTMLElement)) { + return false; + } + + const nodeTagName = node.tagName.toLowerCase(); + const nodeIsSpanElementWithAutofillAttribute = - node instanceof HTMLSpanElement && node.hasAttribute("data-bwautofill"); + nodeTagName === "span" && node.hasAttribute("data-bwautofill"); + if (nodeIsSpanElementWithAutofillAttribute) { + return true; + } - const ignoredInputTypes = new Set(["hidden", "submit", "reset", "button", "image", "file"]); + const nodeHasBwIgnoreAttribute = node.hasAttribute("data-bwignore"); const nodeIsValidInputElement = - node instanceof HTMLInputElement && !ignoredInputTypes.has(node.type); + nodeTagName === "input" && !this.ignoredInputTypes.has((node as HTMLInputElement).type); + if (nodeIsValidInputElement && !nodeHasBwIgnoreAttribute) { + return true; + } - const nodeIsTextAreaOrSelectElement = - node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement; - - const nodeIsNonIgnoredFillableControlElement = - (nodeIsTextAreaOrSelectElement || nodeIsValidInputElement) && - !node.hasAttribute("data-bwignore"); - - return nodeIsSpanElementWithAutofillAttribute || nodeIsNonIgnoredFillableControlElement; + return ["textarea", "select"].includes(nodeTagName) && !nodeHasBwIgnoreAttribute; } /** * Attempts to get the ShadowRoot of the passed node. If support for the * extension based openOrClosedShadowRoot API is available, it will be used. + * Will return null if the node is not an HTMLElement or if the node has + * child nodes. + * * @param {Node} node - * @returns {ShadowRoot | null} - * @private */ private getShadowRoot(node: Node): ShadowRoot | null { - if (!(node instanceof HTMLElement)) { + if (!(node instanceof HTMLElement) || node.childNodes.length !== 0) { return null; } + if (node.shadowRoot) { + return node.shadowRoot; + } if ((chrome as any).dom?.openOrClosedShadowRoot) { return (chrome as any).dom.openOrClosedShadowRoot(node); } - return (node as any).openOrClosedShadowRoot || node.shadowRoot; + return (node as any).openOrClosedShadowRoot; } /** @@ -1031,7 +1054,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte const autofillElementNodes = this.queryAllTreeWalkerNodes( node, - (node: Node) => node instanceof HTMLFormElement || this.isNodeFormFieldElement(node), + (walkerNode: Node) => + (walkerNode as HTMLElement).tagName?.toLowerCase() === "form" || + this.isNodeFormFieldElement(walkerNode), ) as HTMLElement[]; if (autofillElementNodes.length) { @@ -1084,8 +1109,11 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private deleteCachedAutofillElement( element: ElementWithOpId | ElementWithOpId, ) { - if (element instanceof HTMLFormElement && this.autofillFormElements.has(element)) { - this.autofillFormElements.delete(element); + if ( + element.tagName.toLowerCase() === "form" && + this.autofillFormElements.has(element as ElementWithOpId) + ) { + this.autofillFormElements.delete(element as ElementWithOpId); return; }