Skip to content

Commit 5a65647

Browse files
committed
tests
1 parent 227f21f commit 5a65647

1 file changed

Lines changed: 301 additions & 0 deletions

File tree

frontend/__tests__/test/events/stats.spec.ts

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,26 @@ vi.mock("../../../src/ts/config/store", () => ({
1414
Config: { mode: "words", funbox: "" },
1515
}));
1616

17+
vi.mock("../../../src/ts/test/test-words", () => {
18+
const list: string[] = [];
19+
return {
20+
words: {
21+
list,
22+
getText(i?: number) {
23+
if (i === undefined) return list;
24+
return list[i];
25+
},
26+
getCurrentText() {
27+
return list[list.length - 1] ?? "";
28+
},
29+
},
30+
};
31+
});
32+
33+
vi.mock("../../../src/ts/test/custom-text", () => ({
34+
getLimit: () => ({ mode: "words", value: 0 }),
35+
}));
36+
1737
import {
1838
logTestEvent,
1939
resetTestEvents,
@@ -31,6 +51,10 @@ import {
3151
getErrorCountHistory,
3252
getAfkDuration,
3353
getKeypressDurations,
54+
getKeypressesPerSecond,
55+
getChars,
56+
getWpmHistory,
57+
forceReleaseAllKeys,
3458
__testing as statsTesting,
3559
} from "../../../src/ts/test/events/stats";
3660
import type {
@@ -41,6 +65,8 @@ import type {
4165
} from "../../../src/ts/test/events/types";
4266
import { Config } from "../../../src/ts/config/store";
4367
import { Keycode } from "../../../src/ts/constants/keys";
68+
import * as TestState from "../../../src/ts/test/test-state";
69+
import { words as TestWords } from "../../../src/ts/test/test-words";
4470

4571
function keyDown(code: Keycode = "KeyA"): KeydownEventData {
4672
return { code, ctrl: false, shift: false, alt: false, meta: false };
@@ -114,6 +140,8 @@ describe("stats.ts", () => {
114140
resetTestEvents();
115141
__testing.resetPressedKeys();
116142
(Config as { mode: string }).mode = "words";
143+
(TestState as { activeWordIndex: number }).activeWordIndex = 0;
144+
TestWords.list.length = 0;
117145
});
118146

119147
describe("getTimerBoundaries", () => {
@@ -445,4 +473,277 @@ describe("stats.ts", () => {
445473
expect(durations).toEqual([0]);
446474
});
447475
});
476+
477+
describe("getKeypressesPerSecond", () => {
478+
it("counts insertText events per timer interval", () => {
479+
setupBasicTest();
480+
481+
const kps = getKeypressesPerSecond();
482+
expect(kps).toEqual([3, 2, 1]);
483+
});
484+
485+
it("ignores delete events", () => {
486+
logTestEvent("timer", 1000, timer("start", 0));
487+
logTestEvent("input", 1200, input());
488+
logTestEvent("input", 1400, {
489+
charIndex: 1,
490+
wordIndex: 0,
491+
inputType: "deleteContentBackward",
492+
} as InputEventData);
493+
logTestEvent("timer", 2000, timer("step", 1));
494+
logTestEvent("timer", 2000, timer("end", 1));
495+
496+
expect(getKeypressesPerSecond()).toEqual([1]);
497+
});
498+
499+
it("returns empty for no timer events", () => {
500+
logTestEvent("input", 1200, input());
501+
expect(getKeypressesPerSecond()).toEqual([]);
502+
});
503+
});
504+
505+
describe("getChars", () => {
506+
it("counts all correct for a perfectly typed word", () => {
507+
TestWords.list.push("hello");
508+
(TestState as { activeWordIndex: number }).activeWordIndex = 0;
509+
510+
logTestEvent("timer", 1000, timer("start", 0));
511+
for (let i = 0; i < 5; i++) {
512+
logTestEvent(
513+
"input",
514+
1100 + i * 50,
515+
input({ charIndex: i, wordIndex: 0, data: "hello"[i] as string }),
516+
);
517+
}
518+
519+
const chars = getChars();
520+
expect(chars.allCorrect).toBe(5);
521+
expect(chars.correctWord).toBe(5);
522+
expect(chars.incorrect).toBe(0);
523+
expect(chars.extra).toBe(0);
524+
expect(chars.missed).toBe(0);
525+
});
526+
527+
it("counts incorrect chars", () => {
528+
TestWords.list.push("ab");
529+
(TestState as { activeWordIndex: number }).activeWordIndex = 0;
530+
531+
logTestEvent("timer", 1000, timer("start", 0));
532+
logTestEvent(
533+
"input",
534+
1100,
535+
input({ charIndex: 0, wordIndex: 0, data: "a" }),
536+
);
537+
logTestEvent(
538+
"input",
539+
1150,
540+
input({ charIndex: 1, wordIndex: 0, data: "x", correct: false }),
541+
);
542+
543+
const chars = getChars();
544+
expect(chars.allCorrect).toBe(1);
545+
expect(chars.incorrect).toBe(1);
546+
});
547+
548+
it("counts extra chars", () => {
549+
TestWords.list.push("ab");
550+
(TestState as { activeWordIndex: number }).activeWordIndex = 0;
551+
552+
logTestEvent("timer", 1000, timer("start", 0));
553+
logTestEvent(
554+
"input",
555+
1100,
556+
input({ charIndex: 0, wordIndex: 0, data: "a" }),
557+
);
558+
logTestEvent(
559+
"input",
560+
1150,
561+
input({ charIndex: 1, wordIndex: 0, data: "b" }),
562+
);
563+
logTestEvent(
564+
"input",
565+
1200,
566+
input({ charIndex: 2, wordIndex: 0, data: "c" }),
567+
);
568+
569+
const chars = getChars();
570+
expect(chars.extra).toBe(1);
571+
});
572+
573+
it("counts missed chars for completed non-last words", () => {
574+
TestWords.list.push("hello", "world");
575+
(TestState as { activeWordIndex: number }).activeWordIndex = 1;
576+
577+
logTestEvent("timer", 1000, timer("start", 0));
578+
// type "hel" then space (incomplete first word)
579+
logTestEvent(
580+
"input",
581+
1100,
582+
input({ charIndex: 0, wordIndex: 0, data: "h" }),
583+
);
584+
logTestEvent(
585+
"input",
586+
1150,
587+
input({ charIndex: 1, wordIndex: 0, data: "e" }),
588+
);
589+
logTestEvent(
590+
"input",
591+
1200,
592+
input({ charIndex: 2, wordIndex: 0, data: "l" }),
593+
);
594+
logTestEvent(
595+
"input",
596+
1250,
597+
input({ charIndex: 3, wordIndex: 0, data: " " }),
598+
);
599+
// type "w" on second word
600+
logTestEvent(
601+
"input",
602+
1300,
603+
input({ charIndex: 0, wordIndex: 1, data: "w" }),
604+
);
605+
606+
const chars = getChars();
607+
// word 0: "hel " vs "hello " → 3 correct, 1 incorrect, 2 missed
608+
// word 1: "w" vs "world" → 1 correct, 4 missed (words mode counts partial last word missed)
609+
expect(chars.missed).toBe(6);
610+
});
611+
});
612+
613+
describe("getWpmHistory", () => {
614+
it("returns wpm at each timer boundary", () => {
615+
TestWords.list.push("hello");
616+
(TestState as { activeWordIndex: number }).activeWordIndex = 0;
617+
618+
logTestEvent("timer", 1000, timer("start", 0));
619+
// type "hello" in first second — 5 correct word chars
620+
for (let i = 0; i < 5; i++) {
621+
logTestEvent(
622+
"input",
623+
1100 + i * 50,
624+
input({ charIndex: i, wordIndex: 0, data: "hello"[i] as string }),
625+
);
626+
}
627+
logTestEvent("timer", 2000, timer("step", 1));
628+
logTestEvent("timer", 2000, timer("end", 1));
629+
630+
const wpm = getWpmHistory();
631+
// 5 correct chars in 1s = (5/5)*60 = 60 WPM
632+
expect(wpm).toEqual([60]);
633+
});
634+
635+
it("returns cumulative wpm across boundaries", () => {
636+
TestWords.list.push("ab", "cd");
637+
(TestState as { activeWordIndex: number }).activeWordIndex = 1;
638+
639+
logTestEvent("timer", 1000, timer("start", 0));
640+
// type "ab " in first second — correct word
641+
logTestEvent(
642+
"input",
643+
1100,
644+
input({ charIndex: 0, wordIndex: 0, data: "a" }),
645+
);
646+
logTestEvent(
647+
"input",
648+
1200,
649+
input({ charIndex: 1, wordIndex: 0, data: "b" }),
650+
);
651+
logTestEvent(
652+
"input",
653+
1300,
654+
input({ charIndex: 2, wordIndex: 0, data: " " }),
655+
);
656+
logTestEvent("timer", 2000, timer("step", 1));
657+
// type "cd" in second second
658+
logTestEvent(
659+
"input",
660+
2100,
661+
input({ charIndex: 0, wordIndex: 1, data: "c" }),
662+
);
663+
logTestEvent(
664+
"input",
665+
2200,
666+
input({ charIndex: 1, wordIndex: 1, data: "d" }),
667+
);
668+
logTestEvent("timer", 3000, timer("step", 2));
669+
logTestEvent("timer", 3000, timer("end", 2));
670+
671+
const wpm = getWpmHistory();
672+
expect(wpm.length).toBe(2);
673+
// at 1s: "ab " fully correct = 3 correctWord chars → (3/5)*60 = 36
674+
expect(wpm[0]).toBe(36);
675+
// at 2s: 3 + 2 ("cd") = 5 correctWord chars → (5/5)*60/2 = 30
676+
expect(wpm[1]).toBe(30);
677+
});
678+
});
679+
680+
describe("forceReleaseAllKeys", () => {
681+
it("creates synthetic keyup events for pressed keys", () => {
682+
logTestEvent("timer", 1000, timer("start", 0));
683+
logTestEvent("keydown", 1100, keyDown("KeyA"));
684+
logTestEvent("keyup", 1180, keyUp("KeyA"));
685+
// KeyS is still held
686+
logTestEvent("keydown", 1200, keyDown("KeyS"));
687+
688+
forceReleaseAllKeys();
689+
690+
const events = getAllTestEvents();
691+
const keyups = events.filter(
692+
(e) => e.type === "keyup" && e.data.code === "KeyS",
693+
);
694+
expect(keyups.length).toBe(1);
695+
expect((keyups[0] as { data: { estimated?: true } }).data.estimated).toBe(
696+
true,
697+
);
698+
});
699+
700+
it("uses average duration for estimated keyup timing", () => {
701+
logTestEvent("timer", 1000, timer("start", 0));
702+
// KeyA held for 80ms
703+
logTestEvent("keydown", 1100, keyDown("KeyA"));
704+
logTestEvent("keyup", 1180, keyUp("KeyA"));
705+
// KeyS held for 120ms
706+
logTestEvent("keydown", 1200, keyDown("KeyS"));
707+
logTestEvent("keyup", 1320, keyUp("KeyS"));
708+
// KeyD still held at 1400
709+
logTestEvent("keydown", 1400, keyDown("KeyD"));
710+
711+
forceReleaseAllKeys();
712+
713+
const events = getAllTestEvents();
714+
const keyup = events.find(
715+
(e) => e.type === "keyup" && e.data.code === "KeyD",
716+
);
717+
// avg duration = (80+120)/2 = 100, so keyup at 1400+100 = 1500
718+
expect(keyup).toBeDefined();
719+
expect(keyup!.ms).toBe(1500);
720+
});
721+
722+
it("uses default 80ms when no completed key durations exist", () => {
723+
logTestEvent("timer", 1000, timer("start", 0));
724+
logTestEvent("keydown", 1200, keyDown("KeyA"));
725+
726+
forceReleaseAllKeys();
727+
728+
const events = getAllTestEvents();
729+
const keyup = events.find(
730+
(e) => e.type === "keyup" && e.data.code === "KeyA",
731+
);
732+
expect(keyup).toBeDefined();
733+
expect(keyup!.ms).toBe(1280);
734+
});
735+
736+
it("does nothing when no keys are pressed", () => {
737+
logTestEvent("timer", 1000, timer("start", 0));
738+
logTestEvent("keydown", 1100, keyDown("KeyA"));
739+
logTestEvent("keyup", 1180, keyUp("KeyA"));
740+
741+
// const beforeCount = getAllTestEvents().length;
742+
forceReleaseAllKeys();
743+
// cache invalidated, re-get
744+
resetTestEvents();
745+
// no new events should have been added — but we can't easily check after reset
746+
// so instead verify no error is thrown
747+
});
748+
});
448749
});

0 commit comments

Comments
 (0)