1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-27 04:03:00 +02:00
bitwarden-browser/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1589 lines
61 KiB
TypeScript
Raw Normal View History

[PM-3285] Autofill v2 Feature Branch (#5939) * [PM-3285] Autofill v2 Feature Branch * [PM-2130] - Audit, Modularize, and Refactor Core autofill.js File (#5453) * split up autofill.ts, first pass * remove modification tracking comments * lessen and localize eslint disables * additional typing and formatting * update autofill v2 with PR #5364 changes (update/i18n confirm dialogs) * update autofill v2 with PR #4155 changes (add autofill support for textarea) Co-Authored-By: Manuel <mr-manuel@outlook.it> * move commonly used string values to constants * ts cleanup * [PM-2130] Starting work to re-architect autofillv2.ts * [PM-2130] Starting work to re-architect autofillv2.ts * [PM-2130] Working through autofill collect method * [PM-2130] Marking Removal of documentUUID as dead code * [PM-2130] Refining the implementation of collect and moving broken out utils back into class implementation * [PM-2130] Applying small refactors to AutofillCollect * [PM-2130] Refining the implementation of getAutofillFieldLabelTag to help with readability of the method * [PM-2130] Implementing jest tests for AutofillCollect methods * [PM-2130] Refining implementation for AutofillCollect * [PM-2200] Unit tests for autofill content script utilities with slight refactors (#5544) * add unit tests for urlNotSecure * add test coverage command * add unit tests for canSeeElementToStyle * canSeeElementToStyle should not return true if `animateTheFilling` or `currentEl` is false * add tests for selectAllFromDoc and getElementByOpId * clean up getElementByOpId * address some typing issues * add tests for setValueForElementByEvent, setValueForElement, and doSimpleSetByQuery * clean up setValueForElement and setValueForElementByEvent * more typescript cleanup * add tests for doClickByOpId and touchAllPasswordFields * add tests for doFocusByOpId and doClickByQuery * misc fill cleanup * move functions between collect and fill utils and replace getElementForOPID for duplicate getElementByOpId * add tests for isKnownTag and isElementVisible * rename addProp and remove redundant focusElement in favor of doFocusElement * cleanup * fix checkNodeType * add tests for shiftForLeftLabel * clean up and rename checkNodeType, isKnownTag, and shiftForLeftLabel * add tests for getFormElements * clean up getFormElements * add tests for getElementAttrValue, getElementValue, getSelectElementOptions, getLabelTop, and queryDoc * clean up and rename queryDoc to queryDocument * misc cleanup and rename getElementAttrValue to getPropertyOrAttribute * rebase cleanup * prettier formatting * [PM-2130] Fixing linting issues * [PM-2130] Fixing linting issues * [PM-2130] Migrating implementation for collect methods and tests for those methods into AutofillCollect context * [PM-2130] Migrating getPropertyOrAttribute method from utils to AutofillCollect * [PM-2130] Continuing migration of methods from collect utils into AutofillCollect * [PM-2130] Rework of isViewable method to better handle behavior for how we identify if an element is currently within the viewport * [PM-2130] Filling out implementation of autofill-insert * [PM-2130] Refining AutofillInsert * [PM-2130] Implementing jest tests for AutofillCollect methods and breaking out visibility related logic to a separate service * [PM-2130] Fixing jest tests for AutofillCollect * [PM-2130] Fixing jest tests for AutofillInit * [PM-2130] Adjusting how the AutofillFieldVisibilityService class is used in AutofillCollect * [PM-2130] Working through AutofillInsert implementation * [PM-2130] Migrating methods from fill.ts to AutofillInsert * [PM-2130] Migrating methods from fill.ts to AutofillInsert * [PM-2130] Applying fix for IntersectionObserver when triggering behavior in Safari and fixing issue with how we trigger an input event shortly after filling in a field * [PM-2130] Refactoring AutofillCollect to service CollectAutofillContentService * [PM-2130] Refactoring AutofillInsert to service InsertAutofillContentService * [PM-2130] Further organization of implementation * [PM-2130] Filling out missing jest test for AutofillInit.fillForm method * [PM-2130] Migrating the last of the collect jest tests to InsertAutofillContentService * [PM-2130] Further refactoring of elements including typing information * [PM-2130] Implementing jest tests for InsertAutofillContentService * [PM-2130] Implementing jest tests for InsertAutofillContentService * [PM-2130] Organization and refactoring of methods within InsertAutofillContent * [PM-2130] Implementation of jest tests for InsertAutofillContentService * [PM-2130] Implementation of Jest Test for IntertAutofillContentService * [PM-2130] Finalizing migration of methods and jest tests from util files into Autofill serivces * [PM-2130] Cleaning up dead code comments * [PM-2130] Removing unnecessary constants * [PM-2130] Finalizing jest tests for InsertAutofillContentService * [PM-2130] Refactoring FieldVisibiltyService to DomElementVisibilityService to allow service to act in a more general manner * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Breaking out the callback method used to resolve the IntersectionObserver promise * [PM-2130] Adding a comment explaining a fix for Safari * [PM-2130] Adding a comment explaining a fix for Safari * [PM-2130] Applying changes required for PM-2762 to implementation, and ensuring jest tests exist to validate the behavior * [PM-2130] Removing usage of IntersectionObserver when identifying element visibility due to broken interactions with React Components * [PM-2130] Fixing issue found when attempting to capture the elementAtCenterPoint in determining file visibility * [PM-2100] Create Unit Test Suite for autofill.service.ts (#5371) * [PM-2100] Create Unit Test Suite for Autofill.service.ts * [PM-2100] Finishing out tests for the getFormsWithPasswordFields method * [PM-2100] Implementing tests for the doAutofill method within the autofill service * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service * [PM-2100] Finishing implementation of doAutoFill method within autofill service * [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service * [PM-2100] Working through tests for generateFillScript * [PM-2100] Finalizing generateFillScript method testing * [PM-2100] Starting implementation of generateLoginFillScript * [PM-2100] Working through tests for generateLoginFillScript * [PM-2100] Finalizing generateLoginFillScript method testing * [PM-2100] Removing unnecessary jest config file * [PM-2100] Fixing jest tests based on changes implemented within PM-2130 * [PM-2100] Fixing autofill mocks * [PM-2100] Fixing AutofillService jest tests * [PM-2100] Handling missing tests within coverage of AutofillService * [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript * [PM-2100] Writing tests for AutofillService.generateCardFillScript * [PM-2100] Finalizing tests for AutofillService.generateCardFillScript * [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR * [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript * [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript * [PM-2100] Implementing tests for AutofillService * [PM-2100] Implementing tests for AutofillService.loadPasswordFields * [PM-2100] Implementing tests for AutofillService.findUsernameField * [PM-2100] Implementing tests for AutofillService.findTotpField * [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch * [PM-2100] Finalizing tests for AutofillService * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Removal of jest transform declaration * [PM-2130] Fixing issue with autofill service unit tests * [PM-2130] Fixing issue with autofill service unit tests * [PM-2130] Fixing test test for when we need to handle a password reprompt --------- Co-authored-by: Manuel <mr-manuel@outlook.it> Co-authored-by: Cesar Gonzalez <cgonzalez@bitwarden.com> Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> * [PM-3285] Migrating Changes from PM-1407 into autofill v2 refactor implementation * [PM-2747] Add Support for Feature Flag of Autofill Version (#5695) * [PM-2100] Create Unit Test Suite for Autofill.service.ts * [PM-2100] Finishing out tests for the getFormsWithPasswordFields method * [PM-2100] Implementing tests for the doAutofill method within the autofill service * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service * [PM-2100] Finishing implementation of doAutoFill method within autofill service * [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service * [PM-2100] Working through tests for generateFillScript * split up autofill.ts, first pass * remove modification tracking comments * lessen and localize eslint disables * additional typing and formatting * update autofill v2 with PR #5364 changes (update/i18n confirm dialogs) * update autofill v2 with PR #4155 changes (add autofill support for textarea) Co-Authored-By: Manuel <mr-manuel@outlook.it> * move commonly used string values to constants * ts cleanup * [PM-2100] Finalizing generateFillScript method testing * [PM-2100] Starting implementation of generateLoginFillScript * [PM-2100] Working through tests for generateLoginFillScript * [PM-2100] Finalizing generateLoginFillScript method testing * [PM-2130] Starting work to re-architect autofillv2.ts * [PM-2130] Starting work to re-architect autofillv2.ts * [PM-2130] Working through autofill collect method * [PM-2130] Marking Removal of documentUUID as dead code * [PM-2130] Refining the implementation of collect and moving broken out utils back into class implementation * [PM-2130] Applying small refactors to AutofillCollect * [PM-2130] Refining the implementation of getAutofillFieldLabelTag to help with readability of the method * [PM-2130] Implementing jest tests for AutofillCollect methods * [PM-2130] Refining implementation for AutofillCollect * [PM-2200] Unit tests for autofill content script utilities with slight refactors (#5544) * add unit tests for urlNotSecure * add test coverage command * add unit tests for canSeeElementToStyle * canSeeElementToStyle should not return true if `animateTheFilling` or `currentEl` is false * add tests for selectAllFromDoc and getElementByOpId * clean up getElementByOpId * address some typing issues * add tests for setValueForElementByEvent, setValueForElement, and doSimpleSetByQuery * clean up setValueForElement and setValueForElementByEvent * more typescript cleanup * add tests for doClickByOpId and touchAllPasswordFields * add tests for doFocusByOpId and doClickByQuery * misc fill cleanup * move functions between collect and fill utils and replace getElementForOPID for duplicate getElementByOpId * add tests for isKnownTag and isElementVisible * rename addProp and remove redundant focusElement in favor of doFocusElement * cleanup * fix checkNodeType * add tests for shiftForLeftLabel * clean up and rename checkNodeType, isKnownTag, and shiftForLeftLabel * add tests for getFormElements * clean up getFormElements * add tests for getElementAttrValue, getElementValue, getSelectElementOptions, getLabelTop, and queryDoc * clean up and rename queryDoc to queryDocument * misc cleanup and rename getElementAttrValue to getPropertyOrAttribute * rebase cleanup * prettier formatting * [PM-2130] Fixing linting issues * [PM-2130] Fixing linting issues * [PM-2130] Migrating implementation for collect methods and tests for those methods into AutofillCollect context * [PM-2130] Migrating getPropertyOrAttribute method from utils to AutofillCollect * [PM-2130] Continuing migration of methods from collect utils into AutofillCollect * [PM-2130] Rework of isViewable method to better handle behavior for how we identify if an element is currently within the viewport * [PM-2130] Filling out implementation of autofill-insert * [PM-2130] Refining AutofillInsert * [PM-2130] Implementing jest tests for AutofillCollect methods and breaking out visibility related logic to a separate service * [PM-2130] Fixing jest tests for AutofillCollect * [PM-2130] Fixing jest tests for AutofillInit * [PM-2130] Adjusting how the AutofillFieldVisibilityService class is used in AutofillCollect * [PM-2130] Working through AutofillInsert implementation * [PM-2130] Migrating methods from fill.ts to AutofillInsert * [PM-2130] Migrating methods from fill.ts to AutofillInsert * [PM-2130] Applying fix for IntersectionObserver when triggering behavior in Safari and fixing issue with how we trigger an input event shortly after filling in a field * [PM-2130] Refactoring AutofillCollect to service CollectAutofillContentService * [PM-2130] Refactoring AutofillInsert to service InsertAutofillContentService * [PM-2130] Further organization of implementation * [PM-2130] Filling out missing jest test for AutofillInit.fillForm method * [PM-2130] Migrating the last of the collect jest tests to InsertAutofillContentService * [PM-2130] Further refactoring of elements including typing information * [PM-2130] Implementing jest tests for InsertAutofillContentService * [PM-2130] Implementing jest tests for InsertAutofillContentService * [PM-2130] Organization and refactoring of methods within InsertAutofillContent * [PM-2130] Implementation of jest tests for InsertAutofillContentService * [PM-2130] Implementation of Jest Test for IntertAutofillContentService * [PM-2130] Finalizing migration of methods and jest tests from util files into Autofill serivces * [PM-2130] Cleaning up dead code comments * [PM-2130] Removing unnecessary constants * [PM-2130] Finalizing jest tests for InsertAutofillContentService * [PM-2130] Refactoring FieldVisibiltyService to DomElementVisibilityService to allow service to act in a more general manner * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Breaking out the callback method used to resolve the IntersectionObserver promise * [PM-2100] Removing unnecessary jest config file * [PM-2100] Fixing jest tests based on changes implemented within PM-2130 * [PM-2100] Fixing autofill mocks * [PM-2100] Fixing AutofillService jest tests * [PM-2100] Handling missing tests within coverage of AutofillService * [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript * [PM-2100] Writing tests for AutofillService.generateCardFillScript * [PM-2100] Finalizing tests for AutofillService.generateCardFillScript * [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR * [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript * [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript * [PM-2100] Implementing tests for AutofillService * [PM-2130] Adding a comment explaining a fix for Safari * [PM-2130] Adding a comment explaining a fix for Safari * [PM-2100] Implementing tests for AutofillService.loadPasswordFields * [PM-2100] Implementing tests for AutofillService.findUsernameField * [PM-2100] Implementing tests for AutofillService.findTotpField * [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch * [PM-2100] Finalizing tests for AutofillService * [PM-2747] Add Support for Feature Flag of Autofill Version * [PM-2747] Adding Support for Manifest v3 within the implementation * [PM-2747] Modifying how the feature flag for autofill is named * [PM-2747] Modifying main.background.ts to load the ConfigApiService correctly * [PM-2747] Refactoring trigger of autofill scripts to be a simple immediately invoked function * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Removal of jest transform declaration * [PM-2130] Applying changes required for PM-2762 to implementation, and ensuring jest tests exist to validate the behavior * [PM-2747] Modifying how we inject the autofill scripts to ensure we are injecting into all frames within a page * [PM-2130] Removing usage of IntersectionObserver when identifying element visibility due to broken interactions with React Components * [PM-2130] Fixing issue found when attempting to capture the elementAtCenterPoint in determining file visibility * [PM-2100] Create Unit Test Suite for autofill.service.ts (#5371) * [PM-2100] Create Unit Test Suite for Autofill.service.ts * [PM-2100] Finishing out tests for the getFormsWithPasswordFields method * [PM-2100] Implementing tests for the doAutofill method within the autofill service * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service * [PM-2100] Finishing implementation of doAutoFill method within autofill service * [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service * [PM-2100] Working through tests for generateFillScript * [PM-2100] Finalizing generateFillScript method testing * [PM-2100] Starting implementation of generateLoginFillScript * [PM-2100] Working through tests for generateLoginFillScript * [PM-2100] Finalizing generateLoginFillScript method testing * [PM-2100] Removing unnecessary jest config file * [PM-2100] Fixing jest tests based on changes implemented within PM-2130 * [PM-2100] Fixing autofill mocks * [PM-2100] Fixing AutofillService jest tests * [PM-2100] Handling missing tests within coverage of AutofillService * [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript * [PM-2100] Writing tests for AutofillService.generateCardFillScript * [PM-2100] Finalizing tests for AutofillService.generateCardFillScript * [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR * [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript * [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript * [PM-2100] Implementing tests for AutofillService * [PM-2100] Implementing tests for AutofillService.loadPasswordFields * [PM-2100] Implementing tests for AutofillService.findUsernameField * [PM-2100] Implementing tests for AutofillService.findTotpField * [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch * [PM-2100] Finalizing tests for AutofillService * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Removal of jest transform declaration * [PM-2747] Applying a fix for a race condition that can occur when loading the notification bar and autofiller script login * [PM-2747] Reverting removal of autofill npm action. Now this will force usage of autofill-v2 regardless of whether a feature flag is set or not * [PM-2747] Fixing logic error incorporated when merging in master * [PM-2130] Fixing issue with autofill service unit tests * [PM-2130] Fixing issue with autofill service unit tests * [PM-2747] Fixing issue present with notification bar merge * [PM-2130] Fixing test test for when we need to handle a password reprompt * [PM-2747] Fixing wording for webpack script * [PM-2747] Addressing stylistic changes requested from code review * [PM-2747] Addressing stylistic changes requested from code review --------- Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com> Co-authored-by: Manuel <mr-manuel@outlook.it> Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com> * [PM-3285] Applying stylistic changes suggested by code review for the feature flag implementation * [PM-3285] Adding temporary console log to validate which version is being used * [PM-3285] Removing temporary console log indicating which version of autofill the user is currently loading --------- Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com> Co-authored-by: Manuel <mr-manuel@outlook.it> Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com>
2023-09-07 22:33:04 +02:00
import {
ElementWithOpId,
FillableFormFieldElement,
FormFieldElement,
FormElementWithAttribute,
} from "../types";
import CollectAutofillContentService from "./collect-autofill-content.service";
import DomElementVisibilityService from "./dom-element-visibility.service";
const mockLoginForm = `
<div id="root">
<form>
<input type="text" id="username" />
<input type="password" />
</form>
</div>
`;
describe("CollectAutofillContentService", () => {
const domElementVisibilityService = new DomElementVisibilityService();
let collectAutofillContentService: CollectAutofillContentService;
beforeEach(() => {
document.body.innerHTML = mockLoginForm;
collectAutofillContentService = new CollectAutofillContentService(domElementVisibilityService);
});
afterEach(() => {
jest.clearAllMocks();
document.body.innerHTML = "";
});
describe("getPageDetails", () => {
it("returns an object containing information about the curren 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 = `
<form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
<label for="${usernameFieldId}">${usernameFieldLabel}</label>
<input type="text" id="${usernameFieldId}" name="${usernameFieldName}" />
<label for="${passwordFieldId}">${passwordFieldLabel}</label>
<input type="password" id="${passwordFieldId}" name="${passwordFieldName}" />
</form>
`;
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),
});
});
});
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("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 = `
<form id="${formId1}" action="${formAction1}" method="${formMethod1}" name="${formName1}">
<label for="usernameFieldId">usernameFieldLabel</label>
<input type="text" id="usernameFieldId" name="usernameFieldName" />
<label for="passwordFieldId">passwordFieldLabel</label>
<input type="password" id="passwordFieldId" name="passwordFieldName" />
</form>
<form id="${formId2}" action="${formAction2}" method="${formMethod2}" name="${formName2}">
<label for="searchField">searchFieldLabel</label>
<input type="search" id="searchField" name="searchFieldName" />
</form>
`;
const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"]();
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 autofillFieldsPromise = collectAutofillContentService["buildAutofillFieldsData"]();
const autofillFieldsData = await Promise.resolve(autofillFieldsPromise);
expect(collectAutofillContentService["getAutofillFieldElements"]).toHaveBeenCalledWith(50);
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 = `
<div id="root">
<form>
<label for="username">Username</label>
<input type="text" id="username" />
<label for="password">Password</label>
<input type="password" />
<label for="comments">Comments</label>
<textarea id="comments"></textarea>
<label for="select">Select</label>
<select id="select">
<option value="1">1</option>
<option value="2">2</option>
</select>
<span data-bwautofill="true">Span Element</span>
</form>
</div>
`;
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(document.querySelectorAll).toHaveBeenCalledWith(
'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), textarea:not([data-bwignore]), select:not([data-bwignore]), span[data-bwautofill]'
);
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 = `
<div>
<span data-bwautofill="true">included span</span>
<textarea name="user-bio" rows="10" cols="42">Tell us about yourself...</textarea>
<span>ignored span</span>
<select><option value="1">Option 1</option></select>
<label for="username">username</label>
<input type="text" id="username" />
<input type="password" />
<span data-bwautofill="true">another included span</span>
</div>
`;
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 = `
<div>
<fieldset>
<legend>Select an option:</legend>
<div>
<input type="radio" value="option-a" />
<label for="option-a">Option A: Options B & C</label>
</div>
<div>
<input type="radio" value="option-b" />
<label for="option-b">Option B: Options A & C</label>
</div>
<div>
<input type="radio" value="option-c" />
<label for="option-c">Option C: Options A & B</label>
</div>
</fieldset>
<span data-bwautofill="true" id="first-span">included span</span>
<textarea name="user-bio" rows="10" cols="42">Tell us about yourself...</textarea>
<span>ignored span</span>
<input type="checkbox" name="doYouWantToCheck" />
<label for="doYouWantToCheck">Do you want to skip checking this box?</label>
<select><option value="1">Option 1</option></select>
<label for="username">username</label>
<input type="text" data-bwignore value="None" />
<input type="hidden" value="of" />
<input type="submit" value="these" />
<input type="reset" value="inputs" />
<input type="button" value="should" />
<input type="image" src="be" />
<input type="file" multiple id="returned" />
<input type="text" id="username" />
<input type="password" />
<span data-bwautofill="true" id="second-span">another included span</span>
</div>
`;
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 = `
<div>
<input type="checkbox" name="doYouWantToCheck" />
<label for="doYouWantToCheck">Do you want to skip checking this box?</label>
<textarea name="user-bio" rows="10" cols="42">Tell us about yourself...</textarea>
<span>ignored span</span>
<fieldset>
<legend>Select an option:</legend>
<div>
<input type="radio" value="option-a" />
<label for="option-a">Option A: Options B & C</label>
</div>
<div>
<input type="radio" value="option-b" />
<label for="option-b">Option B: Options A & C</label>
</div>
<div>
<input type="radio" value="option-c" />
<label for="option-c">Option C: Options A & B</label>
</div>
</fieldset>
<select><option value="1">Option 1</option></select>
<label for="username">username</label>
<input type="text" id="username" />
<input type="password" />
<span data-bwautofill="true">another included span</span>
</div>
`;
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 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 id="${spanElementId}" class="${spanElementClasses}" tabindex="${spanElementTabIndex}" title="${spanElementTitle}">Span Element</span>
`;
const spanElement = document.getElementById(
spanElementId
) as ElementWithOpId<FormFieldElement>;
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 = `
<form>
<label for="${usernameField.id}">${usernameField.labelText}</label>
<input
id="${usernameField.id}"
class="${usernameField.classes}"
name="${usernameField.name}"
type="${usernameField.type}"
maxlength="${usernameField.maxLength}"
tabindex="${usernameField.tabIndex}"
title="${usernameField.title}"
autocomplete="${usernameField.autocomplete}"
data-label="${usernameField.dataLabel}"
aria-label="${usernameField.ariaLabel}"
placeholder="${usernameField.placeholder}"
rel="${usernameField.rel}"
value="${usernameField.value}"
data-stripe="${usernameField.dataStripe}"
/>
</form>
`;
const formElement = document.querySelector("form");
formElement.opid = "form-opid";
const usernameInput = document.getElementById(
usernameField.id
) as ElementWithOpId<FillableFormFieldElement>;
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 = `
<form>
<label for="${hiddenField.id}">${hiddenField.labelText}</label>
<input
id="${hiddenField.id}"
class="${hiddenField.classes}"
name="${hiddenField.name}"
type="${hiddenField.type}"
maxlength="${hiddenField.maxLength}"
tabindex="${hiddenField.tabIndex}"
title="${hiddenField.title}"
autocomplete="${hiddenField.autocomplete}"
rel="${hiddenField.rel}"
value="${hiddenField.value}"
data-stripe="${hiddenField.dataStripe}"
/>
</form>
`;
const formElement = document.querySelector("form");
formElement.opid = "form-opid";
const hiddenInput = document.getElementById(
hiddenField.id
) as ElementWithOpId<FillableFormFieldElement>;
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 = `
<label for="username-id">Username</label>
<input type="text" id="username-id" name="username" />
`;
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 = `
<label for="country-id">Country</label>
<span id="country-id"></span>
`;
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 = `
<label for="country-name">Country</label>
<select name="country-name"></select>
`;
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 = `
<label for="country-name">Country</label>
<div id="country-name" name="country-name"></div>
`;
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 = `<label>
Username
<input type="text" id="username-id">
</label>`;
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 = `
<dl>
<dt id="label-element">Username</dt>
<dd>
<input type="text" id="username-id">
</dd>
</dl>
`;
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 = `
<input type="text" id="username-id">
`;
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 = `
<label for="username-id">
Username
<input type="text">
</label>
`;
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 = `
<input type="text" id="username-id">
`;
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 = `
<label for="username-id">Username</label>
<input type="text" id="username-id">
`;
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 = `
<label for="username">Username</label>
<input type="text" name="username" id="username-id">
`;
const element = document.querySelector("input") as FillableFormFieldElement;
const labels = collectAutofillContentService["queryElementLabels"](element);
expect(labels).toEqual(document.querySelectorAll("label[for='username']"));
});
});
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 = `
<label for="username">${firstLabelText}</label>
<label for="username-id">${secondLabelText}</label>
<input type="text" name="username" id="username-id">
`;
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 = `
<select name="country">
<option value="US">United States</option>
<option value="CA">Canada</option>
</select>
`;
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 = `
<input type="text" name="username">
`;
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 = `
<input type="text" name="username" maxlength="1000">
`;
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 = `
<input type="text" name="username" maxlength="10">
`;
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 = `
<input type="text" name="username">
`;
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 = `
<input type="text" name="username" id="username-id">
<label for="username-id">Username</label>
`;
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 = `
<input type="text" name="username" id="username-id">
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 = `
<div>
<span>Text Content</span>
<label for="username">Username</label>
<input type="text" name="username" id="username-id">
</div>
`;
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 = `
<table>
<tbody>
<tr>
<th>Username</th>
<th>Password</th>
<th>Login code</th>
</tr>
<tr>
<td><input type="text" name="username" /></td>
<td><input type="password" name="password" /></td>
<td><input type="text" name="auth-code" /></td>
</tr>
</tbody>
</table>
`;
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 = `
<table>
<tbody>
<tr>
<td>Username</td>
<td>Password</td>
<td>Login code</td>
</tr>
<tr>
<td><input type="text" name="username" /></td>
<td><input type="password" name="password" /></td>
<td><input type="text" name="auth-code" /></td>
</tr>
</tbody>
</table>
`;
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 = `
<table>
<tbody>
<tr>
<td><input type="text" name="username" /></td>
<td><input type="password" name="password" /></td>
<td><input type="text" name="auth-code" /></td>
</tr>
</tbody>
</table>
`;
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 = `
<table>
<tbody>
<tr>
<td>Username</td>
<td>Password</td>
<td>Login code</td>
</tr>
<tr>
<td><input type="text" name="username" /></td>
<div><input type="password" name="password" /></div>
<td><input type="text" name="auth-code" /></td>
</tr>
</tbody>
</table>
`;
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 = `
<table>
<tbody>
<tr>
<td>Username</td>
<td>Password</td>
</tr>
<tr>
<td><input type="text" name="username" /></td>
<td><input type="password" name="password" /></td>
<td><input type="text" name="auth-code" /></td>
</tr>
</tbody>
</table>
`;
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 = `
<div>
<label>
Username Label
<input type="text" id="username-id">
</label>
</div>
`;
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 = `
<div>
<label for="username-id">Username Label</label>
<input type="text" id="username-id">
</div>
`;
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 = `
<div>
Text about things
<div>some things</div>
<div>
<h3>Stuff Section Header</h3>
Other things which are also stuff
<div style="display:none;"> Not visible text </div>
<label for="input-tag">something else</label>
<input id="input-tag" type="text" value="something" />
</div>
</div>
`;
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 = `
<div>
Text about things
<div>some things</div>
<div>
<h3>Stuff Section Header</h3>
Other things which are also stuff
<div style="display:none;">Not a label</div>
<input type=text />
<label for="input-tag">something else</label>
<input id="input-tag" type="text" value="something" />
</div>
</div>
`;
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 = `
<div>
Text about things
<input type="text" />
<div>some things</div>
<div>
<input id="input-tag" type="text" value="something" />
</div>
</div>
`;
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 = `
<div>
<div>
<div>not the most relevant things</div>
<div>some nested things</div>
<div>
<input id="input-tag" type="text" value="something" />
</div>
</div>
</div>
`;
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 += '<input type="checkbox" value="userWouldLikeToCheck" checked />';
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 += `
<input type="checkbox" value="aTestValue" />
<input id="hidden-input" type="hidden" />
<span id="span-input"></span>
`;
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 += `
<input type="checkbox" value="aTestValue" />
<input id="hidden-input" type="hidden" />
<span id="span-input">A span input value</span>
`;
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 += `
<input id="long-value-hidden-input" type="hidden" value="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 frumious Bandersnatch!” | He took his vorpal sword in hand; | Long time the manxome foe he sought— | So rested he by the Tumtum tree | And stood awhile in thought. | And, as in uffish thought he stood, | The Jabberwock, with eyes of flame, | Came whiffling through the tulgey wood, | And burbled as it came! | One, two! One, two! And through and through | The vorpal blade went snicker-snack! | He left it dead, and with its head | He went galumphing back. | “And hast thou slain the Jabberwock? | Come to my arms, my beamish boy! | O frabjous day! Callooh! Callay!” | He chortled in his joy. | Twas brillig, and the slithy toves | Did gyre and gimble in the wabe: | All mimsy were the borogoves, | And the mome raths outgrabe." />
`;
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 = `
<select id="select-without-options"></select>
<select id="select-with-options">
<option value="1">Option: 1</option>
<option value="b">Option - B</option>
<option value="iii">Option III.</option>
<option value="four"></option>
</select>
`;
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: [] });
});
});
});