Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions frontend/__tests__/test/events/data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ vi.mock("../../../src/ts/test/test-stats", () => ({
import {
logTestEvent,
getAllTestEvents,
getInputEvents,
getInputEventsPerWord,
getEventsPerWord,
cleanupData,
resetTestEvents,
__testing,
Expand Down Expand Up @@ -245,56 +244,58 @@ describe("data.ts", () => {
});
});

describe("getInputEvents", () => {
it("returns only input events", () => {
logTestEvent("keydown", 1010, keyDown());
logTestEvent("input", 1020, inputData());
logTestEvent("timer", 1030, timerData("start", 0));
logTestEvent("input", 1040, inputData({ charIndex: 1 }));

const inputs = getInputEvents();
expect(inputs).toHaveLength(2);
expect(inputs.every((e) => e.type === "input")).toBe(true);
});
});

describe("getInputEventsPerWord", () => {
describe("getEventsPerWord", () => {
it("groups input events by wordIndex", () => {
logTestEvent("input", 1010, inputData({ wordIndex: 0, charIndex: 0 }));
logTestEvent("input", 1020, inputData({ wordIndex: 0, charIndex: 1 }));
logTestEvent("input", 1030, inputData({ wordIndex: 1, charIndex: 0 }));

const perWord = getInputEventsPerWord();
const perWord = getEventsPerWord();
expect(perWord.get(0)).toHaveLength(2);
expect(perWord.get(1)).toHaveLength(1);
});

it("includes composition events alongside input events", () => {
logTestEvent("input", 1010, inputData({ wordIndex: 0, charIndex: 0 }));
logTestEvent("composition", 1020, { event: "start", wordIndex: 0 });
logTestEvent("keydown", 1025, keyDown());
logTestEvent("composition", 1030, {
event: "update",
data: "a",
wordIndex: 0,
});

const perWord = getEventsPerWord();
expect(perWord.get(0)).toHaveLength(3);
});

it("does not shift delete at charIndex 0 if wordIndex is 0", () => {
logTestEvent("input", 1010, {
charIndex: 0,
wordIndex: 0,
inputType: "deleteContentBackward",
} as InputEventData);

const perWord = getInputEventsPerWord();
const perWord = getEventsPerWord();
expect(perWord.get(0)).toHaveLength(1);
});

it("respects testMsLimit", () => {
logTestEvent("input", 1010, inputData({ wordIndex: 0, charIndex: 0 }));
logTestEvent("input", 1100, inputData({ wordIndex: 0, charIndex: 1 }));

const perWord = getInputEventsPerWord(undefined, 50);
const perWord = getEventsPerWord(undefined, 50);
expect(perWord.get(0)).toHaveLength(1);
});

it("respects startMs", () => {
logTestEvent("input", 1010, inputData({ wordIndex: 0, charIndex: 0 }));
logTestEvent("input", 1100, inputData({ wordIndex: 0, charIndex: 1 }));

const perWord = getInputEventsPerWord(50);
const perWord = getEventsPerWord(50);
expect(perWord.get(0)).toHaveLength(1);
expect(perWord.get(0)?.[0]?.data.charIndex).toBe(1);
const first = perWord.get(0)?.[0];
expect(first?.type === "input" && first.data.charIndex).toBe(1);
});
});

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/ts/input/listeners/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ inputEl.addEventListener("compositionstart", (event) => {

logTestEvent("composition", now, {
event: "start",
wordIndex: TestState.activeWordIndex,
});
});

Expand All @@ -50,6 +51,7 @@ inputEl.addEventListener("compositionupdate", (event) => {
logTestEvent("composition", now, {
event: "update",
data: event.data,
wordIndex: TestState.activeWordIndex,
});
});

Expand All @@ -75,5 +77,6 @@ inputEl.addEventListener("compositionend", async (event) => {
logTestEvent("composition", now, {
event: "end",
data: event.data,
wordIndex: TestState.activeWordIndex,
});
});
15 changes: 4 additions & 11 deletions frontend/src/ts/test/events/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
KeydownEventData,
KeyupEvent,
KeyupEventData,
InputEventNoMs,
TestEventData,
TestEventNoMs,
TestEventType,
Expand Down Expand Up @@ -317,27 +316,21 @@ export function resetTestEvents(): void {
noCodeIndex = 0;
}

export function getInputEvents(): InputEventNoMs[] {
return getAllTestEvents().filter(
(event): event is InputEventNoMs => event.type === "input",
);
}

export function getPressedKeys(): Map<
Keycode | "NoCode" | `NoCode${number}`,
{ timestamp: number }
> {
return pressedKeys;
}

export function getInputEventsPerWord(
export function getEventsPerWord(
startMs?: number,
testMsLimit?: number,
): Map<number, InputEventNoMs[]> {
let eventsPerWordIndex: Map<number, InputEventNoMs[]> = new Map();
): Map<number, TestEventNoMs[]> {
let eventsPerWordIndex: Map<number, TestEventNoMs[]> = new Map();
const events = getAllTestEvents();
for (const event of events) {
if (event.type !== "input") {
if (!("wordIndex" in event.data)) {
continue;
}

Expand Down
30 changes: 11 additions & 19 deletions frontend/src/ts/test/events/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Config } from "../../config/store";
import { Keycode } from "../../constants/keys";
import { InputEventNoMs } from "./types";
import { InputEventNoMs, TestEventNoMs } from "./types";

export const keysToTrack = new Set<Keycode | "NoCode">([
"NumpadMultiply",
Expand Down Expand Up @@ -120,34 +120,26 @@ export function applyInputEvent(input: string, event: InputEventNoMs): string {
return input;
}

/**
* Reads input from the DOM snapshots captured on each event (inputValue),
* falling back to op-based derivation for events without a snapshot.
* Use this whenever you need the actual current/past input state.
*
* Walks backward to find the latest event with a captured inputValue, then
* replays any subsequent events forward — O(1) when the last event has a
* snapshot (the common case), O(n) worst case.
*/
export function getInputFromDom(events: InputEventNoMs[]): string {
const lastEvent = events[events.length - 1];
export function getInputFromDom(events: TestEventNoMs[]): string {
const lastInputEvent = events.findLast((e) => e.type === "input");

if (lastEvent === undefined) {
if (lastInputEvent === undefined) {
let input = "";
for (const event of events) {
if (event.type !== "input") continue;
input = applyInputEvent(input, event);
}
return input;
}

const inputValue = lastEvent.data.inputValue;
const inputValue = lastInputEvent.data.inputValue;

if (
lastEvent.data.inputType === "insertText" &&
lastEvent.data.data === " " &&
lastEvent.data.lastWord &&
lastEvent.data.commitsWord &&
!lastEvent.data.correct
lastInputEvent.data.inputType === "insertText" &&
lastInputEvent.data.data === " " &&
lastInputEvent.data.lastWord &&
lastInputEvent.data.commitsWord &&
!lastInputEvent.data.correct
) {
// if this is an incorrect word commit on the last word, we dont want to count it at all
return inputValue.trimEnd();
Expand Down
22 changes: 12 additions & 10 deletions frontend/src/ts/test/events/stats.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
getAllTestEvents,
getInputEvents,
getInputEventsPerWord,
getEventsPerWord,
getPressedKeys,
logTestEvent,
} from "./data";
Expand All @@ -12,7 +11,7 @@ import { getInputFromDom } from "./helpers";
import { activeWordIndex, bailedOut, koreanStatus } from "../test-state";
import { calculateWpm } from "../../utils/numbers";
import { mean, roundTo2 } from "@monkeytype/util/numbers";
import { InputEventNoMs, TestEventNoMs } from "./types";
import { TestEventNoMs } from "./types";
import { Config } from "../../config/store";
import { isFunboxActiveWithProperty } from "../funbox/list";
import Hangul from "hangul-js";
Expand Down Expand Up @@ -266,7 +265,7 @@ function getTargetWord(
}

function countCharsForWords(
eventsPerWord: Map<number, InputEventNoMs[]>,
eventsPerWord: Map<number, TestEventNoMs[]>,
lastWordIndex: number,
shouldCountPartialLastWord: boolean,
): CharCounts {
Expand Down Expand Up @@ -309,10 +308,10 @@ function countCharsForWords(
}

function inferActiveWordIndex(
eventsPerWord: Map<number, InputEventNoMs[]>,
eventsPerWord: Map<number, TestEventNoMs[]>,
): number {
let maxWordIndex = -1;
let lastWordEvents: InputEventNoMs[] | undefined;
let lastWordEvents: TestEventNoMs[] | undefined;
for (const [k, wordEvents] of eventsPerWord) {
if (getInputFromDom(wordEvents).length > 0 && k > maxWordIndex) {
maxWordIndex = k;
Expand All @@ -324,6 +323,7 @@ function inferActiveWordIndex(
// committed trailing space → cursor advanced to the next word
if (
lastEvt !== undefined &&
"inputType" in lastEvt.data &&
lastEvt.data.inputType === "insertText" &&
lastEvt.data.data === " "
) {
Expand All @@ -338,14 +338,14 @@ export function getChars(): CharCounts {
(Config.mode === "words" && Config.words === 0) ||
(Config.mode === "custom" && CustomText.getLimit().mode === "time");
return countCharsForWords(
getInputEventsPerWord(),
getEventsPerWord(),
isTimedTest ? activeWordIndex : TestWords.words.list.length - 1,
isTimedTest,
);
}

export function getInputHistory(): string[] {
const eventsPerWordIndex = getInputEventsPerWord();
const eventsPerWordIndex = getEventsPerWord();
const history: string[] = [];

for (const [wordIndex, events] of eventsPerWordIndex) {
Expand Down Expand Up @@ -380,12 +380,14 @@ export function getAccuracy(): {
incorrect: number;
percentage: number;
} {
const events = getInputEvents();
const events = getAllTestEvents();

let correct = 0;
let incorrect = 0;

for (const event of events) {
if (event.type !== "input") continue;

if (!("correct" in event.data)) {
continue;
}
Expand Down Expand Up @@ -472,7 +474,7 @@ export function getWpmHistory(): number[] {
const wpmHistory: number[] = [];

for (const boundary of getTimerBoundaries(events)) {
const eventsPerWord = getInputEventsPerWord(undefined, boundary);
const eventsPerWord = getEventsPerWord(undefined, boundary);
const lastWordIndex = inferActiveWordIndex(eventsPerWord);
const { correctWord } = countCharsForWords(
eventsPerWord,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/ts/test/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ export type CompositionTestEvent = EventProps<
export type CompositionTestEventData =
| {
event: "start";
wordIndex: number;
}
| {
event: "update" | "end";
data: string;
wordIndex: number;
};
Loading