diff --git a/source/LocustInputEvent.ts b/source/LocustInputEvent.ts index 794b3b3..499ece9 100644 --- a/source/LocustInputEvent.ts +++ b/source/LocustInputEvent.ts @@ -1,11 +1,22 @@ export type InputEventTrigger = "keypress" | "fill"; -export class LocustInputEvent extends Event { +export class LocustInputEvent extends InputEvent { + private _data: string; private _source: InputEventTrigger; - constructor(source: InputEventTrigger, type: string, eventInitDict?: EventInit) { + constructor( + source: InputEventTrigger, + type: string, + data: string, + eventInitDict?: EventInit + ) { super(type, eventInitDict); this._source = source; + this._data = data; + } + + get data(): string { + return this._data; } get source(): InputEventTrigger { diff --git a/source/LoginTarget.ts b/source/LoginTarget.ts index aea6f7a..ccad567 100644 --- a/source/LoginTarget.ts +++ b/source/LoginTarget.ts @@ -1,9 +1,9 @@ import isVisible from "is-visible"; import EventEmitter from "eventemitter3"; import { getSharedObserver as getUnloadObserver } from "./UnloadObserver.js"; -import { setInputValue } from "./inputs.js"; import { LoginTargetFeature } from "./types.js"; import { LocustInputEvent } from "./LocustInputEvent.js"; +import { typeIntoInput } from "./typing.js"; interface ChangeListener { input: HTMLElement; @@ -163,7 +163,7 @@ export class LoginTarget extends EventEmitter { */ async fillOTP(otp: string): Promise { if (this.otpField) { - setInputValue(this.otpField, otp); + await typeIntoInput(this.otpField, otp); } } @@ -177,7 +177,7 @@ export class LoginTarget extends EventEmitter { */ async fillPassword(password: string): Promise { if (this.passwordField) { - setInputValue(this.passwordField, password); + await typeIntoInput(this.passwordField, password); } } @@ -191,7 +191,7 @@ export class LoginTarget extends EventEmitter { */ async fillUsername(username: string): Promise { if (this.usernameField) { - setInputValue(this.usernameField, username); + await typeIntoInput(this.usernameField, username); } } diff --git a/source/inputPatterns.ts b/source/inputPatterns.ts index 63e6667..3c2e882 100644 --- a/source/inputPatterns.ts +++ b/source/inputPatterns.ts @@ -63,8 +63,7 @@ const USERNAMES_OPTIONAL_TEXT = [ "input[name*=login i]", "input[id*=email i]", "input[id*=login i]", - "input[formcontrolname*=user i]", - "input[class*=user i]" + "input[formcontrolname*=user i]" ].reduce( (queries, next) => [ ...queries, diff --git a/source/inputs.ts b/source/inputs.ts index a35d16f..026d1de 100644 --- a/source/inputs.ts +++ b/source/inputs.ts @@ -6,7 +6,6 @@ import { SUBMIT_BUTTON_QUERIES, USERNAME_QUERIES } from "./inputPatterns.js"; -import { LocustInputEvent } from "./LocustInputEvent.js"; export interface FetchedForm { form: HTMLFormElement | HTMLDivElement; @@ -68,11 +67,6 @@ const VISIBILE_SCORE_INCREMENT = 8; type FormElementScoringType = keyof typeof FORM_ELEMENT_SCORING; -const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, - "value" -).set; - function fetchForms(queryEl: Document | HTMLElement = document): Array { return Array.prototype.slice.call(queryEl.querySelectorAll(FORM_QUERIES.join(","))); } @@ -128,14 +122,6 @@ function isInput(el: Element): boolean { return el.tagName?.toLowerCase() === "input"; } -export function setInputValue(input: HTMLInputElement, value: string): void { - nativeInputValueSetter.call(input, value); - const inputEvent = new LocustInputEvent("fill", "input", { bubbles: true }); - input.dispatchEvent(inputEvent); - const changeEvent = new LocustInputEvent("fill", "change", { bubbles: true }); - input.dispatchEvent(changeEvent); -} - export function sortFormElements(elements: Array, type: FormElementScoringType): Array { const tests = FORM_ELEMENT_SCORING[type]; if (!tests) { diff --git a/source/typing.ts b/source/typing.ts new file mode 100644 index 0000000..9e650c9 --- /dev/null +++ b/source/typing.ts @@ -0,0 +1,44 @@ +import { LocustInputEvent } from "./LocustInputEvent.js"; + +const MAX_TYPE_WAIT = 20 +const MIN_TYPE_WAIT = 2; + +const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + "value" +).set; + +async function sleep(time: number): Promise { + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, time); + }); +} + +export async function typeIntoInput(input: HTMLInputElement, value: string): Promise { + // Focus input + const focusEvent = new FocusEvent("focus", { bubbles: true }); + input.dispatchEvent(focusEvent); + // Start typing + const characters = value.split(""); + let newValue = ""; + while (characters.length > 0) { + const char = characters.shift(); + newValue = `${newValue}${char}`; + input.setAttribute("value", newValue); + nativeInputValueSetter.call(input, newValue); + // Input event data takes the single new character + const inputEvent = new LocustInputEvent("fill", "input", char, { bubbles: true }); + input.dispatchEvent(inputEvent); + // Wait + const waitTime = Math.floor(Math.random() * (MAX_TYPE_WAIT - MIN_TYPE_WAIT)) + MIN_TYPE_WAIT; + await sleep(waitTime); + } + // Blur input + const blurEvent = new FocusEvent("blur", { bubbles: true }); + input.dispatchEvent(blurEvent); + // The change event gets all of the new data + const changeEvent = new LocustInputEvent("fill", "change", value, { bubbles: true }); + input.dispatchEvent(changeEvent); +} diff --git a/test/LoginTarget.spec.js b/test/LoginTarget.spec.js index 3dd36e8..8f6fb82 100644 --- a/test/LoginTarget.spec.js +++ b/test/LoginTarget.spec.js @@ -1,7 +1,7 @@ const { expect } = require("chai"); const sinon = require("sinon"); const { LoginTarget } = require("../dist/LoginTarget.js"); -const { setInputValue } = require("../dist/inputs.js"); +const { typeIntoInput } = require("../dist/typing.js"); describe("LoginTarget", function () { beforeEach(function () { @@ -14,7 +14,7 @@ describe("LoginTarget", function () { expect(this.target).to.have.property("once").that.is.a("function"); }); - it("fires events when username inputs are updated", function () { + it("fires events when username inputs are updated", async function () { let currentValue = ""; this.target.usernameField = document.createElement("input"); this.target.on("valueChanged", (info) => { @@ -22,11 +22,11 @@ describe("LoginTarget", function () { currentValue = info.value; } }); - setInputValue(this.target.usernameField, "user5644"); + await typeIntoInput(this.target.usernameField, "user5644"); expect(currentValue).to.equal("user5644"); }); - it("specifies event source as 'fill' when set using the setter method", function () { + it("specifies event source as 'fill' when set using the setter method", async function () { let source = ""; this.target.usernameField = document.createElement("input"); this.target.on("valueChanged", (info) => { @@ -34,7 +34,7 @@ describe("LoginTarget", function () { source = info.source; } }); - setInputValue(this.target.usernameField, "user5644"); + await typeIntoInput(this.target.usernameField, "user5644"); expect(source).to.equal("fill"); }); @@ -84,7 +84,7 @@ describe("LoginTarget", function () { expect(formSubmitted).to.equal(1); }); - it("fires events when password inputs are updated", function () { + it("fires events when password inputs are updated", async function () { let currentValue = ""; this.target.passwordField = document.createElement("input"); this.target.on("valueChanged", (info) => { @@ -92,7 +92,7 @@ describe("LoginTarget", function () { currentValue = info.value; } }); - setInputValue(this.target.passwordField, "pass!3233 5"); + await typeIntoInput(this.target.passwordField, "pass!3233 5"); expect(currentValue).to.equal("pass!3233 5"); }); diff --git a/test/inputs.spec.js b/test/inputs.spec.js index 3dc5132..c7276e5 100644 --- a/test/inputs.spec.js +++ b/test/inputs.spec.js @@ -1,6 +1,6 @@ const { expect } = require("chai"); const sinon = require("sinon"); -const { fetchFormsWithInputs, setInputValue, sortFormElements } = require("../dist/inputs.js"); +const { fetchFormsWithInputs, sortFormElements } = require("../dist/inputs.js"); const { FORM_QUERIES } = require("../dist/inputPatterns.js"); describe("inputs", function () { @@ -49,51 +49,6 @@ describe("inputs", function () { }); }); - describe("setInputValue", function () { - beforeEach(function () { - this.input = document.createElement("input"); - document.body.appendChild(this.input); - }); - - afterEach(function () { - document.body.removeChild(this.input); - }); - - it("sets the input's value", function () { - expect(this.input.value).to.equal(""); - setInputValue(this.input, "new value"); - expect(this.input.value).to.equal("new value"); - }); - - it("fires the input's 'input' event", function () { - return new Promise((resolve) => { - this.input.addEventListener( - "input", - (event) => { - expect(event.target.value).to.equal("123"); - resolve(); - }, - false - ); - setInputValue(this.input, "123"); - }); - }); - - it("fires the input's 'change' event", function () { - return new Promise((resolve) => { - this.input.addEventListener( - "change", - (event) => { - expect(event.target.value).to.equal("456"); - resolve(); - }, - false - ); - setInputValue(this.input, "456"); - }); - }); - }); - describe("sortFormElements", function () { beforeEach(function () { this.username1 = document.createElement("input"); diff --git a/test/typing.spec.js b/test/typing.spec.js new file mode 100644 index 0000000..e010d09 --- /dev/null +++ b/test/typing.spec.js @@ -0,0 +1,56 @@ +const { expect } = require("chai"); +const sinon = require("sinon"); +const { typeIntoInput } = require("../dist/typing.js"); + +describe("typing", function () { + describe("typeIntoInput", function () { + beforeEach(function () { + this.input = document.createElement("input"); + document.body.appendChild(this.input); + }); + + afterEach(function () { + document.body.removeChild(this.input); + }); + + it("sets the input's value", async function () { + expect(this.input.value).to.equal(""); + await typeIntoInput(this.input, "new value"); + expect(this.input.value).to.equal("new value"); + }); + + it("fires the input's 'change' event", async function () { + let entered = ""; + const work = new Promise((resolve) => { + this.input.addEventListener( + "change", + (event) => { + entered = event.target.value; + resolve(); + }, + false + ); + }); + await typeIntoInput(this.input, "123"); + await work; + expect(entered).to.equal("123"); + }); + + it("fires the input's 'input' event", async function () { + let typed = ""; + const work = new Promise((resolve) => { + this.input.addEventListener( + "input", + (event) => { + typed = event.data; + resolve(); + }, + false + ); + }); + await typeIntoInput(this.input, "4"); + await work; + expect(typed).to.equal("4"); + }); + }); +});