import { mock } from "jest-mock-extended"; import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement, FormElementWithAttribute, } from "../types"; import AutofillOverlayContentService from "./autofill-overlay-content.service"; import CollectAutofillContentService from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; const mockLoginForm = `
`; describe("CollectAutofillContentService", () => { const domElementVisibilityService = new DomElementVisibilityService(); const autofillOverlayContentService = new AutofillOverlayContentService(); let collectAutofillContentService: CollectAutofillContentService; beforeEach(() => { document.body.innerHTML = mockLoginForm; collectAutofillContentService = new CollectAutofillContentService( domElementVisibilityService, autofillOverlayContentService, ); }); afterEach(() => { jest.clearAllMocks(); document.body.innerHTML = ""; }); describe("getPageDetails", () => { beforeEach(() => { jest .spyOn(collectAutofillContentService as any, "setupMutationObserver") .mockImplementationOnce(() => { collectAutofillContentService["mutationObserver"] = mock(); }); }); it("sets up the mutation observer the first time getPageDetails is called", async () => { await collectAutofillContentService.getPageDetails(); await collectAutofillContentService.getPageDetails(); expect(collectAutofillContentService["setupMutationObserver"]).toHaveBeenCalledTimes(1); }); it("returns an object with empty forms and fields if no fields were found on a previous iteration", async () => { collectAutofillContentService["domRecentlyMutated"] = false; collectAutofillContentService["noFieldsFound"] = true; jest.spyOn(collectAutofillContentService as any, "getFormattedPageDetails"); jest.spyOn(collectAutofillContentService as any, "queryAutofillFormAndFieldElements"); jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData"); jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData"); await collectAutofillContentService.getPageDetails(); expect(collectAutofillContentService["getFormattedPageDetails"]).toHaveBeenCalledWith({}, []); expect( collectAutofillContentService["queryAutofillFormAndFieldElements"], ).not.toHaveBeenCalled(); expect(collectAutofillContentService["buildAutofillFormsData"]).not.toHaveBeenCalled(); expect(collectAutofillContentService["buildAutofillFieldsData"]).not.toHaveBeenCalled(); }); it("returns an object with cached form and field data values", async () => { const formId = "validFormId"; const formAction = "https://example.com/"; const formMethod = "post"; const formName = "validFormName"; const usernameFieldId = "usernameField"; const usernameFieldName = "username"; const usernameFieldLabel = "User Name"; const passwordFieldId = "passwordField"; const passwordFieldName = "password"; const passwordFieldLabel = "Password"; document.body.innerHTML = `
`; const formElement = document.getElementById(formId) as ElementWithOpId; const autofillForm: AutofillForm = { opid: "__form__0", htmlAction: formAction, htmlName: formName, htmlID: formId, htmlMethod: formMethod, }; const fieldElement = document.getElementById( usernameFieldId, ) as ElementWithOpId; const autofillField: AutofillField = { opid: "__0", elementNumber: 0, maxLength: 999, viewable: true, htmlID: usernameFieldId, htmlName: usernameFieldName, htmlClass: null, tabindex: null, title: "", tagName: "input", "label-tag": usernameFieldLabel, "label-data": null, "label-aria": null, "label-top": null, "label-right": passwordFieldLabel, "label-left": usernameFieldLabel, placeholder: "", rel: null, type: "text", value: "", checked: false, autoCompleteType: "", disabled: false, readonly: false, selectInfo: null, form: "__form__0", "aria-hidden": false, "aria-disabled": false, "aria-haspopup": false, "data-stripe": null, }; collectAutofillContentService["domRecentlyMutated"] = false; collectAutofillContentService["autofillFormElements"] = new Map([ [formElement, autofillForm], ]); collectAutofillContentService["autofillFieldElements"] = new Map([ [fieldElement, autofillField], ]); jest.spyOn(collectAutofillContentService as any, "getFormattedPageDetails"); jest.spyOn(collectAutofillContentService as any, "getFormattedAutofillFormsData"); jest.spyOn(collectAutofillContentService as any, "getFormattedAutofillFieldsData"); jest.spyOn(collectAutofillContentService as any, "queryAutofillFormAndFieldElements"); jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData"); jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData"); await collectAutofillContentService.getPageDetails(); expect(collectAutofillContentService["getFormattedPageDetails"]).toHaveBeenCalled(); expect(collectAutofillContentService["getFormattedAutofillFormsData"]).toHaveBeenCalled(); expect(collectAutofillContentService["getFormattedAutofillFieldsData"]).toHaveBeenCalled(); expect( collectAutofillContentService["queryAutofillFormAndFieldElements"], ).not.toHaveBeenCalled(); expect(collectAutofillContentService["buildAutofillFormsData"]).not.toHaveBeenCalled(); expect(collectAutofillContentService["buildAutofillFieldsData"]).not.toHaveBeenCalled(); }); it("returns an object containing information about the current page as well as autofill data for the forms and fields of the page", async () => { const documentTitle = "Test Page"; const formId = "validFormId"; const formAction = "https://example.com/"; const formMethod = "post"; const formName = "validFormName"; const usernameFieldId = "usernameField"; const usernameFieldName = "username"; const usernameFieldLabel = "User Name"; const passwordFieldId = "passwordField"; const passwordFieldName = "password"; const passwordFieldLabel = "Password"; document.title = documentTitle; document.body.innerHTML = `
`; jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData"); jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData"); jest .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") .mockResolvedValue(true); const pageDetails = await collectAutofillContentService.getPageDetails(); expect(collectAutofillContentService["buildAutofillFormsData"]).toHaveBeenCalled(); expect(collectAutofillContentService["buildAutofillFieldsData"]).toHaveBeenCalled(); expect(pageDetails).toStrictEqual({ title: documentTitle, url: window.location.href, documentUrl: document.location.href, forms: { __form__0: { opid: "__form__0", htmlAction: formAction, htmlName: formName, htmlID: formId, htmlMethod: formMethod, }, }, fields: [ { opid: "__0", elementNumber: 0, maxLength: 999, viewable: true, htmlID: usernameFieldId, htmlName: usernameFieldName, htmlClass: null, tabindex: null, title: "", tagName: "input", "label-tag": usernameFieldLabel, "label-data": null, "label-aria": null, "label-top": null, "label-right": passwordFieldLabel, "label-left": usernameFieldLabel, placeholder: "", rel: null, type: "text", value: "", checked: false, autoCompleteType: "", disabled: false, readonly: false, selectInfo: null, form: "__form__0", "aria-hidden": false, "aria-disabled": false, "aria-haspopup": false, "data-stripe": null, }, { opid: "__1", elementNumber: 1, maxLength: 999, viewable: true, htmlID: passwordFieldId, htmlName: passwordFieldName, htmlClass: null, tabindex: null, title: "", tagName: "input", "label-tag": passwordFieldLabel, "label-data": null, "label-aria": null, "label-top": null, "label-right": "", "label-left": passwordFieldLabel, placeholder: "", rel: null, type: "password", value: "", checked: false, autoCompleteType: "", disabled: false, readonly: false, selectInfo: null, form: "__form__0", "aria-hidden": false, "aria-disabled": false, "aria-haspopup": false, "data-stripe": null, }, ], collectedTimestamp: expect.any(Number), }); }); it("sets the noFieldsFond property to true if the page has no forms or fields", async function () { document.body.innerHTML = ""; collectAutofillContentService["noFieldsFound"] = false; jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData"); jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData"); await collectAutofillContentService.getPageDetails(); expect(collectAutofillContentService["buildAutofillFormsData"]).toHaveBeenCalled(); expect(collectAutofillContentService["buildAutofillFieldsData"]).toHaveBeenCalled(); expect(collectAutofillContentService["noFieldsFound"]).toBe(true); }); }); describe("getAutofillFieldElementByOpid", () => { it("returns the element with the opid property value matching the passed value", () => { const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; const passwordInput = document.querySelector( 'input[type="password"]', ) as FormElementWithAttribute; textInput.opid = "__0"; passwordInput.opid = "__1"; const textInputWithOpid = collectAutofillContentService.getAutofillFieldElementByOpid("__0"); const passwordInputWithOpid = collectAutofillContentService.getAutofillFieldElementByOpid("__1"); expect(textInputWithOpid).toEqual(textInput); expect(textInputWithOpid).not.toEqual(passwordInput); expect(passwordInputWithOpid).toEqual(passwordInput); }); it("returns the first of the element with an `opid` value matching the passed value and emits a console warning if multiple fields contain the same `opid`", () => { const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; const passwordInput = document.querySelector( 'input[type="password"]', ) as FormElementWithAttribute; jest.spyOn(console, "warn").mockImplementationOnce(jest.fn()); textInput.opid = "__1"; passwordInput.opid = "__1"; const elementWithOpid0 = collectAutofillContentService.getAutofillFieldElementByOpid("__0"); const elementWithOpid1 = collectAutofillContentService.getAutofillFieldElementByOpid("__1"); expect(elementWithOpid0).toEqual(textInput); expect(elementWithOpid1).toEqual(textInput); expect(elementWithOpid1).not.toEqual(passwordInput); // eslint-disable-next-line no-console expect(console.warn).toHaveBeenCalledWith("More than one element found with opid __1"); }); it("returns the element at the index position (parsed from passed opid) of all AutofillField elements when the passed opid value cannot be found", () => { const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; const passwordInput = document.querySelector( 'input[type="password"]', ) as FormElementWithAttribute; textInput.opid = undefined; passwordInput.opid = "__1"; const elementWithOpid0 = collectAutofillContentService.getAutofillFieldElementByOpid("__0"); const elementWithOpid2 = collectAutofillContentService.getAutofillFieldElementByOpid("__2"); expect(textInput.opid).toBeUndefined(); expect(elementWithOpid0).toEqual(textInput); expect(elementWithOpid0).not.toEqual(passwordInput); expect(elementWithOpid2).toBeNull(); }); it("returns null if no element can be found", () => { const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; textInput.opid = "__0"; const foundElementWithOpid = collectAutofillContentService.getAutofillFieldElementByOpid("__999"); expect(foundElementWithOpid).toBeNull(); }); }); describe("buildAutofillFormsData", () => { it("will not attempt to gather data from a cached form element", () => { const documentTitle = "Test Page"; const formId = "validFormId"; const formAction = "https://example.com/"; const formMethod = "post"; const formName = "validFormName"; document.title = documentTitle; document.body.innerHTML = `
`; const formElement = document.getElementById(formId) as ElementWithOpId; const existingAutofillForm: AutofillForm = { opid: "__form__0", htmlAction: formAction, htmlName: formName, htmlID: formId, htmlMethod: formMethod, }; collectAutofillContentService["autofillFormElements"] = new Map([ [formElement, existingAutofillForm], ]); const formElements = Array.from(document.querySelectorAll("form")); jest.spyOn(collectAutofillContentService as any, "getFormActionAttribute"); const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"]( formElements as Node[], ); expect(collectAutofillContentService["getFormActionAttribute"]).not.toHaveBeenCalled(); expect(autofillFormsData).toStrictEqual({ __form__0: existingAutofillForm }); }); it("returns an object of AutofillForm objects with the form id as a key", () => { const documentTitle = "Test Page"; const formId1 = "validFormId"; const formAction1 = "https://example.com/"; const formMethod1 = "post"; const formName1 = "validFormName"; const formId2 = "validFormId2"; const formAction2 = "https://example2.com/"; const formMethod2 = "get"; const formName2 = "validFormName2"; document.title = documentTitle; document.body.innerHTML = `
`; const { formElements } = collectAutofillContentService["queryAutofillFormAndFieldElements"](); const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"](formElements); expect(autofillFormsData).toStrictEqual({ __form__0: { opid: "__form__0", htmlAction: formAction1, htmlName: formName1, htmlID: formId1, htmlMethod: formMethod1, }, __form__1: { opid: "__form__1", htmlAction: formAction2, htmlName: formName2, htmlID: formId2, htmlMethod: formMethod2, }, }); }); }); describe("buildAutofillFieldsData", () => { it("returns a promise containing an array of AutofillField objects", async () => { jest.spyOn(collectAutofillContentService as any, "getAutofillFieldElements"); jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldItem"); jest .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") .mockResolvedValue(true); const { formFieldElements } = collectAutofillContentService["queryAutofillFormAndFieldElements"](); const autofillFieldsPromise = collectAutofillContentService["buildAutofillFieldsData"]( formFieldElements as FormFieldElement[], ); const autofillFieldsData = await Promise.resolve(autofillFieldsPromise); expect(collectAutofillContentService["getAutofillFieldElements"]).toHaveBeenCalledWith( 100, formFieldElements, ); expect(collectAutofillContentService["buildAutofillFieldItem"]).toHaveBeenCalledTimes(2); expect(autofillFieldsPromise).toBeInstanceOf(Promise); expect(autofillFieldsData).toStrictEqual([ { "aria-disabled": false, "aria-haspopup": false, "aria-hidden": false, autoCompleteType: "", checked: false, "data-stripe": null, disabled: false, elementNumber: 0, form: null, htmlClass: null, htmlID: "username", htmlName: "", "label-aria": null, "label-data": null, "label-left": "", "label-right": "", "label-tag": "", "label-top": null, maxLength: 999, opid: "__0", placeholder: "", readonly: false, rel: null, selectInfo: null, tabindex: null, tagName: "input", title: "", type: "text", value: "", viewable: true, }, { "aria-disabled": false, "aria-haspopup": false, "aria-hidden": false, autoCompleteType: "", checked: false, "data-stripe": null, disabled: false, elementNumber: 1, form: null, htmlClass: null, htmlID: "", htmlName: "", "label-aria": null, "label-data": null, "label-left": "", "label-right": "", "label-tag": "", "label-top": null, maxLength: 999, opid: "__1", placeholder: "", readonly: false, rel: null, selectInfo: null, tabindex: null, tagName: "input", title: "", type: "password", value: "", viewable: true, }, ]); }); }); describe("getAutofillFieldElements", () => { it("returns all form elements from the targeted document if no limit is set", () => { document.body.innerHTML = `
Span Element
`; const usernameInput = document.getElementById("username"); const passwordInput = document.querySelector('input[type="password"]'); const commentsTextarea = document.getElementById("comments"); const selectElement = document.getElementById("select"); const spanElement = document.querySelector('span[data-bwautofill="true"]'); jest.spyOn(document, "querySelectorAll"); jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); const formElements: FormFieldElement[] = collectAutofillContentService["getAutofillFieldElements"](); expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled(); expect(formElements).toEqual([ usernameInput, passwordInput, commentsTextarea, selectElement, spanElement, ]); }); it("returns up to 2 (passed as `limit`) form elements from the targeted document with more than 2 form elements", () => { document.body.innerHTML = `
included span ignored span another included span
`; const spanElement = document.querySelector("span[data-bwautofill='true']"); const textAreaInput = document.querySelector("textarea"); jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); const formElements: FormFieldElement[] = collectAutofillContentService["getAutofillFieldElements"](2); expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( 1, spanElement, "type", ); expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( 2, textAreaInput, "type", ); expect(formElements).toEqual([spanElement, textAreaInput]); }); it("returns form elements from the targeted document, ignoring input types `hidden`, `submit`, `reset`, `button`, `image`, `file`, and inputs tagged with `data-bwignore`, while giving lower order priority to `checkbox` and `radio` inputs if the returned list is truncated by `limit", () => { document.body.innerHTML = `
Select an option:
included span ignored span another included span
`; const inputRadioA = document.querySelector('input[type="radio"][value="option-a"]'); const inputRadioB = document.querySelector('input[type="radio"][value="option-b"]'); const inputRadioC = document.querySelector('input[type="radio"][value="option-c"]'); const firstSpan = document.getElementById("first-span"); const textAreaInput = document.querySelector("textarea"); const checkboxInput = document.querySelector('input[type="checkbox"]'); const selectElement = document.querySelector("select"); const usernameInput = document.getElementById("username"); const passwordInput = document.querySelector('input[type="password"]'); const secondSpan = document.getElementById("second-span"); const formElements: FormFieldElement[] = collectAutofillContentService["getAutofillFieldElements"](); expect(formElements).toEqual([ inputRadioA, inputRadioB, inputRadioC, firstSpan, textAreaInput, checkboxInput, selectElement, usernameInput, passwordInput, secondSpan, ]); }); it("returns form elements from the targeted document while giving lower order priority to `checkbox` and `radio` inputs if the returned list is truncated by `limit`", () => { document.body.innerHTML = `
ignored span
Select an option:
another included span
`; const textAreaInput = document.querySelector("textarea"); const selectElement = document.querySelector("select"); const usernameInput = document.getElementById("username"); const passwordInput = document.querySelector('input[type="password"]'); const includedSpan = document.querySelector('span[data-bwautofill="true"]'); const checkboxInput = document.querySelector('input[type="checkbox"]'); const inputRadioA = document.querySelector('input[type="radio"][value="option-a"]'); const inputRadioB = document.querySelector('input[type="radio"][value="option-b"]'); const truncatedFormElements: FormFieldElement[] = collectAutofillContentService["getAutofillFieldElements"](8); expect(truncatedFormElements).toEqual([ textAreaInput, selectElement, usernameInput, passwordInput, includedSpan, checkboxInput, inputRadioA, inputRadioB, ]); }); }); describe("buildAutofillFieldItem", () => { it("returns an existing autofill field item if it exists", async () => { const index = 0; const usernameField = { labelText: "Username", id: "username-id", classes: "username input classes", name: "username", type: "text", maxLength: 42, tabIndex: 0, title: "Username Input Title", autocomplete: "username-autocomplete", dataLabel: "username-data-label", ariaLabel: "username-aria-label", placeholder: "username-placeholder", rel: "username-rel", value: "username-value", dataStripe: "data-stripe", }; document.body.innerHTML = `
`; document.body.innerHTML = `
`; const existingFieldData: AutofillField = { elementNumber: index, htmlClass: usernameField.classes, htmlID: usernameField.id, htmlName: usernameField.name, maxLength: usernameField.maxLength, opid: `__${index}`, tabindex: String(usernameField.tabIndex), tagName: "input", title: usernameField.title, viewable: true, }; const usernameInput = document.getElementById( usernameField.id, ) as ElementWithOpId; usernameInput.opid = "__0"; collectAutofillContentService["autofillFieldElements"].set(usernameInput, existingFieldData); jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength"); jest .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") .mockResolvedValue(true); jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); jest.spyOn(collectAutofillContentService as any, "getElementValue"); const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"]( usernameInput, 0, ); expect(collectAutofillContentService["getAutofillFieldMaxLength"]).not.toHaveBeenCalled(); expect( collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable, ).not.toHaveBeenCalled(); expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled(); expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled(); expect(autofillFieldItem).toEqual(existingFieldData); }); it("returns the AutofillField base data values without the field labels or input values if the passed element is a span element", async () => { const index = 0; const spanElementId = "span-element"; const spanElementClasses = "span element classes"; const spanElementTabIndex = 0; const spanElementTitle = "Span Element Title"; document.body.innerHTML = ` Span Element `; const spanElement = document.getElementById( spanElementId, ) as ElementWithOpId; jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength"); jest .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") .mockResolvedValue(true); jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); jest.spyOn(collectAutofillContentService as any, "getElementValue"); const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"]( spanElement, index, ); expect(collectAutofillContentService["getAutofillFieldMaxLength"]).toHaveBeenCalledWith( spanElement, ); expect( collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable, ).toHaveBeenCalledWith(spanElement); expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( 1, spanElement, "id", ); expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( 2, spanElement, "name", ); expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( 3, spanElement, "class", ); expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( 4, spanElement, "tabindex", ); expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( 5, spanElement, "title", ); expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( 6, spanElement, "tagName", ); expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled(); expect(autofillFieldItem).toEqual({ elementNumber: index, htmlClass: spanElementClasses, htmlID: spanElementId, htmlName: null, maxLength: null, opid: `__${index}`, tabindex: String(spanElementTabIndex), tagName: spanElement.tagName.toLowerCase(), title: spanElementTitle, viewable: true, }); }); it("returns the AutofillField base data, label data, and input element data", async () => { const index = 0; const usernameField = { labelText: "Username", id: "username-id", classes: "username input classes", name: "username", type: "text", maxLength: 42, tabIndex: 0, title: "Username Input Title", autocomplete: "username-autocomplete", dataLabel: "username-data-label", ariaLabel: "username-aria-label", placeholder: "username-placeholder", rel: "username-rel", value: "username-value", dataStripe: "data-stripe", }; document.body.innerHTML = `
`; const formElement = document.querySelector("form"); formElement.opid = "form-opid"; const usernameInput = document.getElementById( usernameField.id, ) as ElementWithOpId; jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength"); jest .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") .mockResolvedValue(true); jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); jest.spyOn(collectAutofillContentService as any, "getElementValue"); const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"]( usernameInput, index, ); expect(autofillFieldItem).toEqual({ "aria-disabled": false, "aria-haspopup": false, "aria-hidden": false, autoCompleteType: usernameField.autocomplete, checked: false, "data-stripe": usernameField.dataStripe, disabled: false, elementNumber: index, form: formElement.opid, htmlClass: usernameField.classes, htmlID: usernameField.id, htmlName: usernameField.name, "label-aria": usernameField.ariaLabel, "label-data": usernameField.dataLabel, "label-left": usernameField.labelText, "label-right": "", "label-tag": usernameField.labelText, "label-top": null, maxLength: usernameField.maxLength, opid: `__${index}`, placeholder: usernameField.placeholder, readonly: false, rel: usernameField.rel, selectInfo: null, tabindex: String(usernameField.tabIndex), tagName: usernameInput.tagName.toLowerCase(), title: usernameField.title, type: usernameField.type, value: usernameField.value, viewable: true, }); }); it("returns the AutofillField base data and input element data, but not the label data if the input element is of type `hidden`", async () => { const index = 0; const hiddenField = { labelText: "Hidden Field", id: "hidden-id", classes: "hidden input classes", name: "hidden", type: "hidden", maxLength: 42, tabIndex: 0, title: "Hidden Input Title", autocomplete: "off", rel: "hidden-rel", value: "hidden-value", dataStripe: "data-stripe", }; document.body.innerHTML = `
`; const formElement = document.querySelector("form"); formElement.opid = "form-opid"; const hiddenInput = document.getElementById( hiddenField.id, ) as ElementWithOpId; jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength"); jest .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") .mockResolvedValue(true); jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); jest.spyOn(collectAutofillContentService as any, "getElementValue"); const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"]( hiddenInput, index, ); expect(autofillFieldItem).toEqual({ "aria-disabled": false, "aria-haspopup": false, "aria-hidden": false, autoCompleteType: null, checked: false, "data-stripe": hiddenField.dataStripe, disabled: false, elementNumber: index, form: formElement.opid, htmlClass: hiddenField.classes, htmlID: hiddenField.id, htmlName: hiddenField.name, maxLength: hiddenField.maxLength, opid: `__${index}`, readonly: false, rel: hiddenField.rel, selectInfo: null, tabindex: String(hiddenField.tabIndex), tagName: hiddenInput.tagName.toLowerCase(), title: hiddenField.title, type: hiddenField.type, value: hiddenField.value, viewable: true, }); }); }); describe("createAutofillFieldLabelTag", () => { beforeEach(() => { jest.spyOn(collectAutofillContentService as any, "createLabelElementsTag"); jest.spyOn(document, "querySelectorAll"); }); it("returns the label tag early if the passed element contains any labels", () => { document.body.innerHTML = ` `; const element = document.querySelector("#username-id") as FillableFormFieldElement; const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( new Set(element.labels), ); expect(document.querySelectorAll).not.toHaveBeenCalled(); expect(labelTag).toEqual("Username"); }); it("queries all labels associated with the element's id", () => { document.body.innerHTML = ` `; const element = document.querySelector("#country-id") as FillableFormFieldElement; const elementLabel = document.querySelector("label[for='country-id']"); const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); expect(document.querySelectorAll).toHaveBeenCalledWith(`label[for="${element.id}"]`); expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( new Set([elementLabel]), ); expect(labelTag).toEqual("Country"); }); it("queries all labels associated with the element's name", () => { document.body.innerHTML = ` `; const element = document.querySelector("select") as FillableFormFieldElement; const elementLabel = document.querySelector("label[for='country-name']"); const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); expect(document.querySelectorAll).not.toHaveBeenCalledWith(`label[for="${element.id}"]`); expect(document.querySelectorAll).toHaveBeenCalledWith(`label[for="${element.name}"]`); expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( new Set([elementLabel]), ); expect(labelTag).toEqual("Country"); }); it("will not add duplicate labels that are found to the label tag", () => { document.body.innerHTML = `
`; const element = document.querySelector("#country-name") as FillableFormFieldElement; element.name = "country-name"; const elementLabel = document.querySelector("label[for='country-name']"); const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); expect(document.querySelectorAll).toHaveBeenCalledWith( `label[for="${element.id}"], label[for="${element.name}"]`, ); expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( new Set([elementLabel]), ); expect(labelTag).toEqual("Country"); }); it("will attempt to identify the label of an element from its parent element", () => { document.body.innerHTML = ``; const element = document.querySelector("#username-id") as FillableFormFieldElement; const elementLabel = element.parentElement; const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( new Set([elementLabel]), ); expect(labelTag).toEqual("Username"); }); it("will attempt to identify the label of an element from a `dt` element associated with the element's parent", () => { document.body.innerHTML = `
Username
`; const element = document.querySelector("#username-id") as FillableFormFieldElement; const elementLabel = document.querySelector("#label-element"); const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( new Set([elementLabel]), ); expect(labelTag).toEqual("Username"); }); it("will return an empty string value if no labels can be found for an element", () => { document.body.innerHTML = ` `; const element = document.querySelector("#username-id") as FillableFormFieldElement; const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); expect(labelTag).toEqual(""); }); }); describe("queryElementLabels", () => { it("returns null if the passed element has no id or name", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const labels = collectAutofillContentService["queryElementLabels"](element); expect(labels).toBeNull(); }); it("returns an empty NodeList if the passed element has no label", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const labels = collectAutofillContentService["queryElementLabels"](element); expect(labels).toEqual(document.querySelectorAll("label")); }); it("returns the label of an element associated with its ID value", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const labels = collectAutofillContentService["queryElementLabels"](element); expect(labels).toEqual(document.querySelectorAll("label[for='username-id']")); }); it("returns the label of an element associated with its name value", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const labels = collectAutofillContentService["queryElementLabels"](element); expect(labels).toEqual(document.querySelectorAll("label[for='username']")); }); it("removes any new lines generated for the query selector", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const labels = collectAutofillContentService["queryElementLabels"](element); expect(labels).toEqual(document.querySelectorAll("label[for='username-id']")); }); }); describe("createLabelElementsTag", () => { it("returns a string containing all the labels associated with a given input element", () => { const firstLabelText = "Username by name"; const secondLabelText = "Username by ID"; document.body.innerHTML = ` `; const labels = document.querySelectorAll("label"); jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText"); const labelTag = collectAutofillContentService["createLabelElementsTag"](new Set(labels)); expect( collectAutofillContentService["trimAndRemoveNonPrintableText"], ).toHaveBeenNthCalledWith(1, firstLabelText); expect( collectAutofillContentService["trimAndRemoveNonPrintableText"], ).toHaveBeenNthCalledWith(2, secondLabelText); expect(labelTag).toEqual(`${firstLabelText}${secondLabelText}`); }); }); describe("getAutofillFieldMaxLength", () => { it("returns null if the passed FormFieldElement is not an element type that has a max length property", () => { document.body.innerHTML = ` `; const element = document.querySelector("select") as FillableFormFieldElement; const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element); expect(maxLength).toBeNull(); }); it("returns a value of 999 if the passed FormFieldElement has no set maxLength value", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element); expect(maxLength).toEqual(999); }); it("returns a value of 999 if the passed FormFieldElement has a maxLength value higher than 999", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element); expect(maxLength).toEqual(999); }); it("returns the maxLength property of a passed FormFieldElement", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element); expect(maxLength).toEqual(10); }); }); describe("createAutofillFieldRightLabel", () => { it("returns an empty string if no siblings are found", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element); expect(labelTag).toEqual(""); }); it("returns the text content of the element's next sibling element", () => { document.body.innerHTML = ` `; const element = document.querySelector("input") as FillableFormFieldElement; const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element); expect(labelTag).toEqual("Username"); }); it("returns the text content of the element's next sibling textNode", () => { document.body.innerHTML = ` Username `; const element = document.querySelector("input") as FillableFormFieldElement; const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element); expect(labelTag).toEqual("Username"); }); }); describe("createAutofillFieldLeftLabel", () => { it("returns a string value of the text content associated with the previous siblings of the passed element", () => { document.body.innerHTML = `
Text Content
`; const element = document.querySelector("input") as FillableFormFieldElement; const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element); expect(labelTag).toEqual("Text ContentUsername"); }); }); describe("createAutofillFieldTopLabel", () => { it("returns the table column header value for the passed table element", () => { document.body.innerHTML = `
Username Password Login code
`; const targetTableCellInput = document.querySelector( 'input[name="password"]', ) as HTMLInputElement; const targetTableCellLabel = collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); expect(targetTableCellLabel).toEqual("Password"); }); it("will attempt to return the value for the previous sibling row as the label if a `th` cell is not found", () => { document.body.innerHTML = `
Username Password Login code
`; const targetTableCellInput = document.querySelector( 'input[name="auth-code"]', ) as HTMLInputElement; const targetTableCellLabel = collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); expect(targetTableCellLabel).toEqual("Login code"); }); it("returns null for the passed table element it's parent row has no previous sibling row", () => { document.body.innerHTML = `
`; const targetTableCellInput = document.querySelector( 'input[name="password"]', ) as HTMLInputElement; const targetTableCellLabel = collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); expect(targetTableCellLabel).toEqual(null); }); it("returns null if the input element is not structured within a `td` element", () => { document.body.innerHTML = `
Username Password Login code
`; const targetTableCellInput = document.querySelector( 'input[name="password"]', ) as HTMLInputElement; const targetTableCellLabel = collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); expect(targetTableCellLabel).toEqual(null); }); it("returns null if the index of the `td` element is larger than the length of cells in the sibling row", () => { document.body.innerHTML = `
Username Password
`; const targetTableCellInput = document.querySelector( 'input[name="auth-code"]', ) as HTMLInputElement; const targetTableCellLabel = collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); expect(targetTableCellLabel).toEqual(null); }); }); describe("isNewSectionElement", () => { const validElementTags = [ "html", "body", "button", "form", "head", "iframe", "input", "option", "script", "select", "table", "textarea", ]; const invalidElementTags = ["div", "span"]; describe("given a transitional element", () => { validElementTags.forEach((tag) => { const element = document.createElement(tag); it(`returns true if the element tag is a ${tag}`, () => { expect(collectAutofillContentService["isNewSectionElement"](element)).toEqual(true); }); }); }); describe("given an non-transitional element", () => { invalidElementTags.forEach((tag) => { const element = document.createElement(tag); it(`returns false if the element tag is a ${tag}`, () => { expect(collectAutofillContentService["isNewSectionElement"](element)).toEqual(false); }); }); }); it(`returns true if the provided element is falsy`, () => { expect(collectAutofillContentService["isNewSectionElement"](undefined)).toEqual(true); }); }); describe("getTextContentFromElement", () => { it("returns the node value for a text node", () => { document.body.innerHTML = `
`; const element = document.querySelector("#username-id"); const textNode = element.previousSibling; const parsedTextContent = collectAutofillContentService["trimAndRemoveNonPrintableText"]( textNode.nodeValue, ); jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText"); const textContent = collectAutofillContentService["getTextContentFromElement"](textNode); expect(textNode.nodeType).toEqual(Node.TEXT_NODE); expect(collectAutofillContentService["trimAndRemoveNonPrintableText"]).toHaveBeenCalledWith( textNode.nodeValue, ); expect(textContent).toEqual(parsedTextContent); }); it("returns the text content for an element node", () => { document.body.innerHTML = `
`; const element = document.querySelector('label[for="username-id"]'); jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText"); const textContent = collectAutofillContentService["getTextContentFromElement"](element); expect(element.nodeType).toEqual(Node.ELEMENT_NODE); expect(collectAutofillContentService["trimAndRemoveNonPrintableText"]).toHaveBeenCalledWith( element.textContent, ); expect(textContent).toEqual(element.textContent); }); }); describe("trimAndRemoveNonPrintableText", () => { it("returns an empty string if no text content is passed", () => { const textContent = collectAutofillContentService["trimAndRemoveNonPrintableText"](undefined); expect(textContent).toEqual(""); }); it("returns a trimmed string with all non-printable text removed", () => { const nonParsedText = `Hello!\nThis is a \t test string.\x0B\x08`; const parsedText = collectAutofillContentService["trimAndRemoveNonPrintableText"](nonParsedText); expect(parsedText).toEqual("Hello! This is a test string."); }); }); describe("recursivelyGetTextFromPreviousSiblings", () => { it("should find text adjacent to the target element likely to be a label", () => { document.body.innerHTML = `
Text about things
some things

Stuff Section Header

Other things which are also stuff
Not visible text
`; const textInput = document.querySelector("#input-tag") as FormElementWithAttribute; const elementList: string[] = collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); expect(elementList).toEqual([ "something else", "Not visible text", "Other things which are also stuff", "Stuff Section Header", ]); }); it("should stop looking at siblings for label values when a 'new section' element is seen", () => { document.body.innerHTML = `
Text about things
some things

Stuff Section Header

Other things which are also stuff
Not a label
`; const textInput = document.querySelector("#input-tag") as FormElementWithAttribute; const elementList: string[] = collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); expect(elementList).toEqual(["something else"]); }); it("should keep looking for labels in parents when there are no siblings of the target element", () => { document.body.innerHTML = `
Text about things
some things
`; const textInput = document.querySelector("#input-tag") as FormElementWithAttribute; const elementList: string[] = collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); expect(elementList).toEqual(["some things"]); }); it("should find label in parent sibling last child if no other label candidates have been encountered and there are no text nodes along the way", () => { document.body.innerHTML = `
not the most relevant things
some nested things
`; const textInput = document.querySelector("#input-tag") as FormElementWithAttribute; const elementList: string[] = collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); expect(elementList).toEqual(["some nested things"]); }); it("should exit early if the target element has no parent element/node", () => { const textInput = document.querySelector("html") as HTMLHtmlElement; const elementList: string[] = collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); expect(elementList).toEqual([]); }); }); describe("getPropertyOrAttribute", () => { it("returns the value of the named property of the target element if the property exists within the element", () => { document.body.innerHTML += ''; const textInput = document.querySelector("#username") as HTMLInputElement; textInput.setAttribute("value", "jsmith"); const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement; jest.spyOn(textInput, "getAttribute"); jest.spyOn(checkboxInput, "getAttribute"); const textInputValue = collectAutofillContentService["getPropertyOrAttribute"]( textInput, "value", ); const textInputId = collectAutofillContentService["getPropertyOrAttribute"](textInput, "id"); const textInputBaseURI = collectAutofillContentService["getPropertyOrAttribute"]( textInput, "baseURI", ); const textInputAutofocus = collectAutofillContentService["getPropertyOrAttribute"]( textInput, "autofocus", ); const checkboxInputChecked = collectAutofillContentService["getPropertyOrAttribute"]( checkboxInput, "checked", ); expect(textInput.getAttribute).not.toHaveBeenCalled(); expect(checkboxInput.getAttribute).not.toHaveBeenCalled(); expect(textInputValue).toEqual("jsmith"); expect(textInputId).toEqual("username"); expect(textInputBaseURI).toEqual("http://localhost/"); expect(textInputAutofocus).toEqual(false); expect(checkboxInputChecked).toEqual(true); }); it("returns the value of the named attribute of the element if it does not exist as a property within the element", () => { const textInput = document.querySelector("#username") as HTMLInputElement; textInput.setAttribute("data-unique-attribute", "unique-value"); jest.spyOn(textInput, "getAttribute"); const textInputUniqueAttribute = collectAutofillContentService["getPropertyOrAttribute"]( textInput, "data-unique-attribute", ); expect(textInputUniqueAttribute).toEqual("unique-value"); expect(textInput.getAttribute).toHaveBeenCalledWith("data-unique-attribute"); }); it("returns a null value if the element does not contain the passed attribute name as either a property or attribute value", () => { const textInput = document.querySelector("#username") as HTMLInputElement; jest.spyOn(textInput, "getAttribute"); const textInputNonExistentAttribute = collectAutofillContentService["getPropertyOrAttribute"]( textInput, "non-existent-attribute", ); expect(textInputNonExistentAttribute).toEqual(null); expect(textInput.getAttribute).toHaveBeenCalledWith("non-existent-attribute"); }); }); describe("getElementValue", () => { it("returns an empty string of passed input elements whose value is not set", () => { document.body.innerHTML += ` `; const textInput = document.querySelector("#username") as HTMLInputElement; const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement; const hiddenInput = document.querySelector("#hidden-input") as HTMLInputElement; const spanInput = document.querySelector("#span-input") as HTMLInputElement; const textInputValue = collectAutofillContentService["getElementValue"](textInput); const checkboxInputValue = collectAutofillContentService["getElementValue"](checkboxInput); const hiddenInputValue = collectAutofillContentService["getElementValue"](hiddenInput); const spanInputValue = collectAutofillContentService["getElementValue"](spanInput); expect(textInputValue).toEqual(""); expect(checkboxInputValue).toEqual(""); expect(hiddenInputValue).toEqual(""); expect(spanInputValue).toEqual(""); }); it("returns the value of the passed input element", () => { document.body.innerHTML += ` A span input value `; const textInput = document.querySelector("#username") as HTMLInputElement; textInput.value = "jsmith"; const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement; checkboxInput.checked = true; const hiddenInput = document.querySelector("#hidden-input") as HTMLInputElement; hiddenInput.value = "aHiddenInputValue"; const spanInput = document.querySelector("#span-input") as HTMLInputElement; const textInputValue = collectAutofillContentService["getElementValue"](textInput); const checkboxInputValue = collectAutofillContentService["getElementValue"](checkboxInput); const hiddenInputValue = collectAutofillContentService["getElementValue"](hiddenInput); const spanInputValue = collectAutofillContentService["getElementValue"](spanInput); expect(textInputValue).toEqual("jsmith"); expect(checkboxInputValue).toEqual("✓"); expect(hiddenInputValue).toEqual("aHiddenInputValue"); expect(spanInputValue).toEqual("A span input value"); }); it("return the truncated value of the passed hidden input type if the value length exceeds 256 characters", () => { document.body.innerHTML += ` `; const longValueHiddenInput = document.querySelector( "#long-value-hidden-input", ) as HTMLInputElement; const longHiddenValue = collectAutofillContentService["getElementValue"](longValueHiddenInput); expect(longHiddenValue).toEqual( "’Twas brillig, and the slithy toves | Did gyre and gimble in the wabe: | All mimsy were the borogoves, | And the mome raths outgrabe. | “Beware the Jabberwock, my son! | The jaws that bite, the claws that catch! | Beware the Jubjub bird, and shun | The f...SNIPPED", ); }); }); describe("getSelectElementOptions", () => { it("returns the inner text and values of each `option` within the passed `select`", () => { document.body.innerHTML = ` `; const selectWithOptions = document.querySelector("#select-with-options") as HTMLSelectElement; const selectWithoutOptions = document.querySelector( "#select-without-options", ) as HTMLSelectElement; const selectWithOptionsOptions = collectAutofillContentService["getSelectElementOptions"](selectWithOptions); const selectWithoutOptionsOptions = collectAutofillContentService["getSelectElementOptions"](selectWithoutOptions); expect(selectWithOptionsOptions).toEqual({ options: [ ["option1", "1"], ["optionb", "b"], ["optioniii", "iii"], [null, "four"], ], }); expect(selectWithoutOptionsOptions).toEqual({ options: [] }); }); }); describe("getShadowRoot", () => { it("returns null if the passed node is not an HTMLElement instance", () => { const textNode = document.createTextNode("Hello, world!"); const shadowRoot = collectAutofillContentService["getShadowRoot"](textNode); expect(shadowRoot).toEqual(null); }); it("returns a value provided by Chrome's openOrClosedShadowRoot API", () => { // eslint-disable-next-line // @ts-ignore globalThis.chrome.dom = { openOrClosedShadowRoot: jest.fn(), }; const element = document.createElement("div"); collectAutofillContentService["getShadowRoot"](element); // eslint-disable-next-line // @ts-ignore expect(chrome.dom.openOrClosedShadowRoot).toBeCalled(); }); }); describe("buildTreeWalkerNodesQueryResults", () => { it("will recursively call itself if a shadowDOM element is found and will observe the element for mutations", () => { collectAutofillContentService["mutationObserver"] = mock({ observe: jest.fn(), }); jest.spyOn(collectAutofillContentService as any, "buildTreeWalkerNodesQueryResults"); const shadowRoot = document.createElement("div"); jest .spyOn(collectAutofillContentService as any, "getShadowRoot") .mockReturnValueOnce(shadowRoot); const callbackFilter = jest.fn(); collectAutofillContentService["buildTreeWalkerNodesQueryResults"]( document.body, [], callbackFilter, true, ); expect(collectAutofillContentService["buildTreeWalkerNodesQueryResults"]).toBeCalledTimes(2); expect(collectAutofillContentService["mutationObserver"].observe).toBeCalled(); }); it("will not observe the shadowDOM element if required to skip", () => { collectAutofillContentService["mutationObserver"] = mock({ observe: jest.fn(), }); const shadowRoot = document.createElement("div"); jest .spyOn(collectAutofillContentService as any, "getShadowRoot") .mockReturnValueOnce(shadowRoot); const callbackFilter = jest.fn(); collectAutofillContentService["buildTreeWalkerNodesQueryResults"]( document.body, [], callbackFilter, false, ); expect(collectAutofillContentService["mutationObserver"].observe).not.toBeCalled(); }); }); describe("setupMutationObserver", () => { it("sets up a mutation observer and observes the document element", () => { jest.spyOn(MutationObserver.prototype, "observe"); collectAutofillContentService["setupMutationObserver"](); expect(collectAutofillContentService["mutationObserver"]).toBeInstanceOf(MutationObserver); expect(collectAutofillContentService["mutationObserver"].observe).toBeCalled(); }); }); describe("handleMutationObserverMutation", () => { it("will set the domRecentlyMutated value to true and the noFieldsFound value to false if a form or field node has been added ", () => { 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, "isAutofillElementNodeMutated"); collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]); expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true); expect(collectAutofillContentService["noFieldsFound"]).toEqual(false); expect(collectAutofillContentService["isAutofillElementNodeMutated"]).toBeCalledWith( removedNodes, true, ); expect(collectAutofillContentService["isAutofillElementNodeMutated"]).toBeCalledWith( addedNodes, ); }); it("will handle updating the autofill element if any attribute mutations are encountered", () => { const mutationRecord: MutationRecord = { type: "attributes", addedNodes: null, attributeName: "value", attributeNamespace: null, nextSibling: null, oldValue: null, previousSibling: null, removedNodes: null, target: document.body, }; collectAutofillContentService["domRecentlyMutated"] = false; collectAutofillContentService["noFieldsFound"] = true; collectAutofillContentService["currentLocationHref"] = window.location.href; jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated"); jest.spyOn(collectAutofillContentService as any, "handleAutofillElementAttributeMutation"); collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]); expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(false); expect(collectAutofillContentService["noFieldsFound"]).toEqual(true); expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled(); expect(collectAutofillContentService["handleAutofillElementAttributeMutation"]).toBeCalled(); }); it("will handle window location mutations", () => { const mutationRecord: MutationRecord = { type: "attributes", addedNodes: null, attributeName: "value", attributeNamespace: null, nextSibling: null, oldValue: null, previousSibling: null, removedNodes: null, target: document.body, }; collectAutofillContentService["currentLocationHref"] = "https://someotherurl.com"; jest.spyOn(collectAutofillContentService as any, "handleWindowLocationMutation"); jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated"); jest.spyOn(collectAutofillContentService as any, "handleAutofillElementAttributeMutation"); collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]); expect(collectAutofillContentService["handleWindowLocationMutation"]).toBeCalled(); expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled(); expect( collectAutofillContentService["handleAutofillElementAttributeMutation"], ).not.toBeCalled(); }); }); describe("deleteCachedAutofillElement", () => { it("removes the autofill form element from the map of elements", () => { const formElement = document.createElement("form") as ElementWithOpId; const autofillForm: AutofillForm = { opid: "1234", htmlName: "formEl", htmlID: "formEl-id", htmlAction: "https://example.com", htmlMethod: "POST", }; collectAutofillContentService["autofillFormElements"] = new Map([ [formElement, autofillForm], ]); collectAutofillContentService["deleteCachedAutofillElement"](formElement); expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0); }); it("removes the autofill field element form the map of elements", () => { const fieldElement = document.createElement("input") as ElementWithOpId; const autofillField: AutofillField = { elementNumber: 0, htmlClass: "", tabindex: "", title: "", viewable: false, opid: "1234", htmlName: "username", htmlID: "username-id", htmlType: "text", htmlAutocomplete: "username", htmlAutofocus: false, htmlDisabled: false, htmlMaxLength: 999, htmlReadonly: false, htmlRequired: false, htmlValue: "jsmith", }; collectAutofillContentService["autofillFieldElements"] = new Map([ [fieldElement, autofillField], ]); collectAutofillContentService["deleteCachedAutofillElement"](fieldElement); expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0); }); }); describe("handleWindowLocationMutation", () => { it("will set the current location to the global location href, set the dom recently mutated flag and the no fields found flag, clear out the autofill form and field maps, and update the autofill elements after mutation", () => { collectAutofillContentService["currentLocationHref"] = "https://example.com/login"; collectAutofillContentService["domRecentlyMutated"] = false; collectAutofillContentService["noFieldsFound"] = true; jest.spyOn(collectAutofillContentService as any, "updateAutofillElementsAfterMutation"); collectAutofillContentService["handleWindowLocationMutation"](); expect(collectAutofillContentService["currentLocationHref"]).toEqual(window.location.href); expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true); expect(collectAutofillContentService["noFieldsFound"]).toEqual(false); expect(collectAutofillContentService["updateAutofillElementsAfterMutation"]).toBeCalled(); expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0); expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0); }); }); describe("handleAutofillElementAttributeMutation", () => { it("returns early if the target node is not an HTMLElement instance", () => { const mutationRecord: MutationRecord = { type: "attributes", addedNodes: null, attributeName: "value", attributeNamespace: null, nextSibling: null, oldValue: null, previousSibling: null, removedNodes: null, target: document.createTextNode("Hello, world!"), }; jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated"); collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord); expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled(); }); it("will update the autofill form element data if the target node can be found in the autofillFormElements map", () => { const targetNode = document.createElement("form") as ElementWithOpId; targetNode.setAttribute("name", "username"); targetNode.setAttribute("value", "jsmith"); const autofillForm: AutofillForm = { opid: "1234", htmlName: "formEl", htmlID: "formEl-id", htmlAction: "https://example.com", htmlMethod: "POST", }; const mutationRecord: MutationRecord = { type: "attributes", addedNodes: null, attributeName: "id", attributeNamespace: null, nextSibling: null, oldValue: null, previousSibling: null, removedNodes: null, target: targetNode, }; collectAutofillContentService["autofillFormElements"] = new Map([[targetNode, autofillForm]]); jest.spyOn(collectAutofillContentService as any, "updateAutofillFormElementData"); collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord); expect(collectAutofillContentService["updateAutofillFormElementData"]).toBeCalledWith( mutationRecord.attributeName, mutationRecord.target, autofillForm, ); }); it("will update the autofill field element data if the target node can be found in the autofillFieldElements map", () => { const targetNode = document.createElement("input") as ElementWithOpId; targetNode.setAttribute("name", "username"); targetNode.setAttribute("value", "jsmith"); const autofillField: AutofillField = { elementNumber: 0, htmlClass: "", tabindex: "", title: "", viewable: false, opid: "1234", htmlName: "username", htmlID: "username-id", htmlType: "text", htmlAutocomplete: "username", htmlAutofocus: false, htmlDisabled: false, htmlMaxLength: 999, htmlReadonly: false, htmlRequired: false, htmlValue: "jsmith", }; const mutationRecord: MutationRecord = { type: "attributes", addedNodes: null, attributeName: "id", attributeNamespace: null, nextSibling: null, oldValue: null, previousSibling: null, removedNodes: null, target: targetNode, }; collectAutofillContentService["autofillFieldElements"] = new Map([ [targetNode, autofillField], ]); jest.spyOn(collectAutofillContentService as any, "updateAutofillFieldElementData"); collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord); expect(collectAutofillContentService["updateAutofillFieldElementData"]).toBeCalledWith( mutationRecord.attributeName, mutationRecord.target, autofillField, ); }); }); describe("updateAutofillFormElementData", () => { const formElement = document.createElement("form") as ElementWithOpId; const autofillForm: AutofillForm = { opid: "1234", htmlName: "formEl", htmlID: "formEl-id", htmlAction: "https://example.com", htmlMethod: "POST", }; const updatedAttributes = ["action", "name", "id", "method"]; updatedAttributes.forEach((attribute) => { it(`will update the ${attribute} value for the form element`, () => { jest.spyOn(collectAutofillContentService["autofillFormElements"], "set"); collectAutofillContentService["updateAutofillFormElementData"]( attribute, formElement, autofillForm, ); expect(collectAutofillContentService["autofillFormElements"].set).toBeCalledWith( formElement, autofillForm, ); }); }); it("will not update an attribute value if it is not present in the updateActions object", () => { jest.spyOn(collectAutofillContentService["autofillFormElements"], "set"); collectAutofillContentService["updateAutofillFormElementData"]( "aria-label", formElement, autofillForm, ); expect(collectAutofillContentService["autofillFormElements"].set).not.toBeCalled(); }); }); describe("updateAutofillFieldElementData", () => { const fieldElement = document.createElement("input") as ElementWithOpId; const autofillField: AutofillField = { htmlClass: "value", htmlID: "", htmlName: "", opid: "", tabindex: "", title: "", viewable: false, elementNumber: 0, }; const updatedAttributes = [ "maxlength", "name", "id", "type", "autocomplete", "class", "tabindex", "title", "value", "rel", "tagname", "checked", "disabled", "readonly", "data-label", "aria-label", "aria-hidden", "aria-disabled", "aria-haspopup", "data-stripe", ]; updatedAttributes.forEach((attribute) => { it(`will update the ${attribute} value for the field element`, async () => { jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set"); await collectAutofillContentService["updateAutofillFieldElementData"]( attribute, fieldElement, autofillField, ); expect(collectAutofillContentService["autofillFieldElements"].set).toBeCalledWith( fieldElement, autofillField, ); }); }); 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 () => { jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set"); await collectAutofillContentService["updateAutofillFieldElementData"]( "random-attribute", fieldElement, autofillField, ); expect(collectAutofillContentService["autofillFieldElements"].set).not.toBeCalled(); }); }); });