Skip to content

Commit

Permalink
Merge pull request #45 from buttercup/feat/matching_improvements
Browse files Browse the repository at this point in the history
Improved input handling/matching
  • Loading branch information
perry-mitchell authored Mar 30, 2024
2 parents 73e5579 + c85b958 commit cb2c44e
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 75 deletions.
15 changes: 13 additions & 2 deletions source/LocustInputEvent.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions source/LoginTarget.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -163,7 +163,7 @@ export class LoginTarget extends EventEmitter<LoginTargetEvents> {
*/
async fillOTP(otp: string): Promise<void> {
if (this.otpField) {
setInputValue(this.otpField, otp);
await typeIntoInput(this.otpField, otp);
}
}

Expand All @@ -177,7 +177,7 @@ export class LoginTarget extends EventEmitter<LoginTargetEvents> {
*/
async fillPassword(password: string): Promise<void> {
if (this.passwordField) {
setInputValue(this.passwordField, password);
await typeIntoInput(this.passwordField, password);
}
}

Expand All @@ -191,7 +191,7 @@ export class LoginTarget extends EventEmitter<LoginTargetEvents> {
*/
async fillUsername(username: string): Promise<void> {
if (this.usernameField) {
setInputValue(this.usernameField, username);
await typeIntoInput(this.usernameField, username);
}
}

Expand Down
3 changes: 1 addition & 2 deletions source/inputPatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 0 additions & 14 deletions source/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
SUBMIT_BUTTON_QUERIES,
USERNAME_QUERIES
} from "./inputPatterns.js";
import { LocustInputEvent } from "./LocustInputEvent.js";

export interface FetchedForm {
form: HTMLFormElement | HTMLDivElement;
Expand Down Expand Up @@ -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<HTMLFormElement> {
return Array.prototype.slice.call(queryEl.querySelectorAll(FORM_QUERIES.join(",")));
}
Expand Down Expand Up @@ -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<T extends HTMLElement>(elements: Array<T>, type: FormElementScoringType): Array<T> {
const tests = FORM_ELEMENT_SCORING[type];
if (!tests) {
Expand Down
44 changes: 44 additions & 0 deletions source/typing.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, time);
});
}

export async function typeIntoInput(input: HTMLInputElement, value: string): Promise<void> {
// 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);
}
14 changes: 7 additions & 7 deletions test/LoginTarget.spec.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand All @@ -14,27 +14,27 @@ 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) => {
if (info.type === "username") {
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) => {
if (info.type === "username") {
source = info.source;
}
});
setInputValue(this.target.usernameField, "user5644");
await typeIntoInput(this.target.usernameField, "user5644");
expect(source).to.equal("fill");
});

Expand Down Expand Up @@ -84,15 +84,15 @@ 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) => {
if (info.type === "password") {
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");
});

Expand Down
47 changes: 1 addition & 46 deletions test/inputs.spec.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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");
Expand Down
56 changes: 56 additions & 0 deletions test/typing.spec.js
Original file line number Diff line number Diff line change
@@ -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");
});
});
});

0 comments on commit cb2c44e

Please sign in to comment.