Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8a6fcdb
yeet wpm history, raw history, burst history, old burst calculation
Miodec May 30, 2026
ee43b07
burst improve perf
Miodec May 30, 2026
4f6f87c
yeet keypress timings
Miodec May 30, 2026
99a7953
yeet keypresscounthistory
Miodec May 30, 2026
c2bb661
yeet afk history
Miodec May 30, 2026
1dae6e4
missing after last yeet
Miodec May 30, 2026
1e01096
yeet error history
Miodec May 30, 2026
5d3bd07
fix word highlight
Miodec May 30, 2026
709e2ba
fix
Miodec May 30, 2026
1c61693
yeet comparison
Miodec May 30, 2026
2a45341
fix
Miodec May 30, 2026
fd39854
yeet last second not round
Miodec May 30, 2026
e61c110
yeet stats
Miodec May 30, 2026
7fd6d17
yeet acc
Miodec May 30, 2026
48a17b4
yeet test seconds
Miodec May 30, 2026
66bdfe3
fully yeet stats file
Miodec May 30, 2026
fa4e20b
yeet acc
Miodec May 30, 2026
2cf2bf8
yeet
Miodec May 30, 2026
297775f
fixes
Miodec May 30, 2026
c9fa677
yeet missed words
Miodec May 30, 2026
442ddb4
yeet correcred
Miodec May 30, 2026
36b5251
input history
Miodec May 30, 2026
9883c9b
yeet input history
Miodec May 30, 2026
e1a3540
yeet
Miodec May 30, 2026
358e88c
move korean state
Miodec May 30, 2026
b3f3df0
early return
Miodec Jun 1, 2026
e85e149
boom
Miodec Jun 1, 2026
4cbde49
yeet replay
Miodec Jun 1, 2026
b08aeb8
pass now from input to timer start
Miodec Jun 1, 2026
bb555f3
bring command back
Miodec Jun 1, 2026
999fb3b
just use active word index
Miodec Jun 1, 2026
eede7ee
always require param
Miodec Jun 1, 2026
ba8b8ac
accept now from input event
Miodec Jun 1, 2026
580724b
move korean state to test state
Miodec Jun 1, 2026
47f076e
simpler
Miodec Jun 1, 2026
4478f51
simpler
Miodec Jun 1, 2026
0ca7482
accept dom value
Miodec Jun 1, 2026
61d880e
pass now through, log earlier
Miodec Jun 1, 2026
04bb380
Merge branch 'test-events-phase-1.5' into test-events-phase2
Miodec Jun 1, 2026
21e4146
start time
Miodec Jun 1, 2026
92383be
delete events
Miodec Jun 1, 2026
e44ff3b
early
Miodec Jun 1, 2026
94b893d
Merge branch 'test-events-phase-1.5' into test-events-phase2
Miodec Jun 1, 2026
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
2 changes: 1 addition & 1 deletion frontend/__tests__/test/events/data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function timerData(
if (event === "step") {
return { event, timer, drift: 0 };
}
return { event, timer };
return { event, timer, date: 0 };
}

describe("data.ts", () => {
Expand Down
269 changes: 250 additions & 19 deletions frontend/__tests__/test/events/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ vi.mock("../../../src/ts/config/store", () => ({
}));

import {
getSimulatedInput,
findInputValueMismatches,
getInputFromDom,
getInputFromEvents,
getTestEventCode,
} from "../../../src/ts/test/events/helpers";
import type { InputEvent } from "../../../src/ts/test/events/types";
Expand Down Expand Up @@ -91,87 +93,316 @@ function reset(): void {
wordIndex = 0;
}

describe("getSimulatedInput", () => {
describe("getInputFromEvents", () => {
beforeEach(() => {
reset();
});

it("builds string from insertText events", () => {
expect(getSimulatedInput([...insert("hello")])).toBe("hello");
expect(getInputFromEvents([...insert("hello")])).toBe("hello");
});

it("builds string from insertText events with trailing space", () => {
expect(getSimulatedInput([...insert("hello ")])).toBe("hello ");
expect(getInputFromEvents([...insert("hello ")])).toBe("hello ");
});

it("handles deleteContentBackward", () => {
expect(getSimulatedInput([...insert("abc"), ...deleteBackward()])).toBe(
expect(getInputFromEvents([...insert("abc"), ...deleteBackward()])).toBe(
"ab",
);
});

it("handles deleteContentBackward after space", () => {
expect(getSimulatedInput([...insert("abc "), ...deleteBackward()])).toBe(
expect(getInputFromEvents([...insert("abc "), ...deleteBackward()])).toBe(
"abc",
);
});

it("handles multiple deletes", () => {
expect(getSimulatedInput([...insert("ab"), ...deleteBackward(2)])).toBe("");
expect(getInputFromEvents([...insert("ab"), ...deleteBackward(2)])).toBe(
"",
);
});

it("handles multiple deletes after space", () => {
expect(getSimulatedInput([...insert("ab "), ...deleteBackward(2)])).toBe(
expect(getInputFromEvents([...insert("ab "), ...deleteBackward(2)])).toBe(
"a",
);
});

it("handles deleteWordBackward", () => {
expect(getSimulatedInput([...insert("hello"), deleteWordBackward()])).toBe(
expect(getInputFromEvents([...insert("hello"), deleteWordBackward()])).toBe(
"",
);
});

it("handles deleteWordBackward after space", () => {
expect(getSimulatedInput([...insert("hello "), deleteWordBackward()])).toBe(
"",
);
expect(
getInputFromEvents([...insert("hello "), deleteWordBackward()]),
).toBe("");
});

it("returns empty string for no events", () => {
expect(getSimulatedInput([])).toBe("");
expect(getInputFromEvents([])).toBe("");
});

it("handles deleteContentBackward on empty string", () => {
const events = [...deleteBackward()];
expect(getSimulatedInput(events)).toBe("");
expect(getInputFromEvents(events)).toBe("");
});

it("skips inputStopped events", () => {
expect(
getSimulatedInput([
getInputFromEvents([
...insert("he"),
...insert("x", "insertText", { inputStopped: true }),
...insert("llo"),
]),
).toBe("hello");
});

it("handles deleteContentBackward within the same word correctly", () => {
expect(getInputFromEvents([...insert("a a"), deleteWordBackward()])).toBe(
"a ",
);
});

it("handles deleteWordBackward with multiple internal spaces", () => {
expect(
getInputFromEvents([...insert("foo bar baz"), deleteWordBackward()]),
).toBe("foo bar ");
});

it("handles deleteWordBackward with trailing space after multiple words", () => {
expect(
getInputFromEvents([...insert("foo bar "), deleteWordBackward()]),
).toBe("foo ");
});

it("handles consecutive deleteWordBackward events", () => {
expect(
getInputFromEvents([
...insert("foo bar baz"),
deleteWordBackward(),
deleteWordBackward(),
]),
).toBe("foo ");
});

it("handles deleteWordBackward on empty string", () => {
expect(getInputFromEvents([deleteWordBackward()])).toBe("");
});

it("handles deleteWordBackward on only whitespace", () => {
expect(getInputFromEvents([...insert(" "), deleteWordBackward()])).toBe(
"",
);
});

it("ignores recorded inputValue (pure op-based simulation)", () => {
const events: InputEvent[] = [
...insert("hello"),
{
type: "input",
ms: 100,
testMs: 100,
data: {
inputType: "deleteWordBackward",
charIndex: 5,
wordIndex: 0,
inputValue: "RECORDED_BUT_IGNORED",
},
},
];
// pure simulation: deleteWordBackward on "hello" → ""
expect(getInputFromEvents(events)).toBe("");
});
});

describe("getInputFromDom", () => {
beforeEach(() => {
reset();
});

it("falls through to op-based logic when inputValue is absent", () => {
expect(getInputFromDom([...insert("hello")])).toBe("hello");
});

it("uses recorded inputValue when present, overriding op-based logic", () => {
const events: InputEvent[] = [
...insert("hello"),
{
type: "input",
ms: 100,
testMs: 100,
data: {
inputType: "deleteWordBackward",
charIndex: 5,
wordIndex: 0,
inputValue: "he",
},
},
];
// op-based would yield "", but inputValue is truth
expect(getInputFromDom(events)).toBe("he");
});

it("uses latest event's inputValue across multiple recorded events", () => {
const events: InputEvent[] = [
...insert("hello"),
{
type: "input",
ms: 100,
testMs: 100,
data: {
inputType: "deleteContentBackward",
charIndex: 5,
wordIndex: 0,
inputValue: "hi",
},
},
];
expect(getInputFromDom(events)).toBe("hi");
});

it("mixes captured and op-based across events", () => {
const events: InputEvent[] = [
...insert("ab"), // no inputValue, op = "ab"
{
type: "input",
ms: 100,
testMs: 100,
data: {
inputType: "insertText",
data: "c",
charIndex: 2,
wordIndex: 0,
correct: true,
isCompositionEnding: false,
inputStopped: false,
inputValue: "abc",
},
},
// next event has no inputValue, falls through to op (append "d")
{
type: "input",
ms: 110,
testMs: 110,
data: {
inputType: "insertText",
data: "d",
charIndex: 3,
wordIndex: 0,
correct: true,
isCompositionEnding: false,
inputStopped: false,
},
},
];
expect(getInputFromDom(events)).toBe("abcd");
});
});

describe("findInputValueMismatches", () => {
beforeEach(() => {
reset();
});

it("returns empty when no events have recorded inputValue", () => {
expect(findInputValueMismatches([...insert("hello")])).toEqual([]);
});

it("returns empty when recorded values match derivation", () => {
const events: InputEvent[] = [
{
type: "input",
ms: 10,
testMs: 10,
data: {
inputType: "insertText",
data: "a",
charIndex: 0,
wordIndex: 0,
correct: true,
isCompositionEnding: false,
inputStopped: false,
inputValue: "a",
},
},
{
type: "input",
ms: 20,
testMs: 20,
data: {
inputType: "insertText",
data: "b",
charIndex: 1,
wordIndex: 0,
correct: true,
isCompositionEnding: false,
inputStopped: false,
inputValue: "ab",
},
},
];
expect(findInputValueMismatches(events)).toEqual([]);
});

it("returns mismatches when recorded value differs from derivation", () => {
const events: InputEvent[] = [
{
type: "input",
ms: 10,
testMs: 10,
data: {
inputType: "insertText",
data: "a",
charIndex: 0,
wordIndex: 0,
correct: true,
isCompositionEnding: false,
inputStopped: false,
inputValue: "DIFFERENT",
},
},
];
expect(findInputValueMismatches(events)).toEqual([
{ index: 0, derived: "a", recorded: "DIFFERENT" },
]);
});

it("skips events without inputValue, still tracks ones with it", () => {
const events: InputEvent[] = [
...insert("hello"), // no inputValue on these
{
type: "input",
ms: 100,
testMs: 100,
data: {
inputType: "deleteContentBackward",
charIndex: 5,
wordIndex: 0,
inputValue: "hell",
},
},
];
// derivation: "hello" then slice = "hell". Recorded = "hell". Match.
expect(findInputValueMismatches(events)).toEqual([]);
});

// it("handles insertCompositionText events", () => {
// const events = [
// ...insert("k", "insertCompositionText"),
// ...insert("ka", "insertCompositionText"),
// ];
// expect(getSimulatedInput(events)).toBe("ka");
// expect(getInputFromEvents(events)).toBe("ka");
// });

// it("handles composition followed by regular text", () => {
// const events = [
// ...insert("k", "insertCompositionText"),
// ...insert("ka", "insertCompositionText"),
// ...insert("b"),
// ];
// expect(getSimulatedInput(events)).toBe("kab");
// expect(getInputFromEvents(events)).toBe("kab");
// });
});

Expand Down
Loading
Loading