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
32 changes: 0 additions & 32 deletions frontend/__tests__/test/events/data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,6 @@ describe("data.ts", () => {
expect(inputs).toHaveLength(1);
});

it("computes testMs relative to start", () => {
logTestEvent("timer", 1500, timerData("start", 0));
const events = getAllTestEvents();
expect(events[0]!.testMs).toBe(500); // 1500 - 1000
});

it("caches getAllTestEvents and invalidates on new event", () => {
logTestEvent("timer", 1100, timerData("start", 0));
const first = getAllTestEvents();
Expand Down Expand Up @@ -275,32 +269,6 @@ describe("data.ts", () => {
expect(perWord.get(1)).toHaveLength(1);
});

it("attributes deleteContentBackward at charIndex 0 to previous word", () => {
logTestEvent("input", 1010, inputData({ wordIndex: 0, charIndex: 0 }));
logTestEvent("input", 1020, {
charIndex: 0,
wordIndex: 1,
inputType: "deleteContentBackward",
} as InputEventData);

const perWord = getInputEventsPerWord();
expect(perWord.get(0)).toHaveLength(2);
expect(perWord.has(1)).toBe(false);
});

it("attributes deleteWordBackward at charIndex 0 to previous word", () => {
logTestEvent("input", 1010, inputData({ wordIndex: 0, charIndex: 0 }));
logTestEvent("input", 1020, {
charIndex: 0,
wordIndex: 1,
inputType: "deleteWordBackward",
} as InputEventData);

const perWord = getInputEventsPerWord();
expect(perWord.get(0)).toHaveLength(2);
expect(perWord.has(1)).toBe(false);
});

it("does not shift delete at charIndex 0 if wordIndex is 0", () => {
logTestEvent("input", 1010, {
charIndex: 0,
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