diff --git a/frontend/__tests__/test/events/data.spec.ts b/frontend/__tests__/test/events/data.spec.ts index 1b2c22c5ae58..97c1a57a2c60 100644 --- a/frontend/__tests__/test/events/data.spec.ts +++ b/frontend/__tests__/test/events/data.spec.ts @@ -11,6 +11,7 @@ import { cleanupData, resetTestEvents, __testing, + forceReleaseAllKeys, } from "../../../src/ts/test/events/data"; import type { InputEventData, @@ -501,4 +502,74 @@ describe("data.ts", () => { expect(getAllTestEvents()).toEqual([]); }); }); + + describe("forceReleaseAllKeys", () => { + it("creates synthetic keyup events for pressed keys", () => { + logTestEvent("timer", 1000, timerData("start", 0)); + logTestEvent("keydown", 1100, keyDown("KeyA")); + logTestEvent("keyup", 1180, keyUp("KeyA")); + // KeyS is still held + logTestEvent("keydown", 1200, keyDown("KeyS")); + + forceReleaseAllKeys(); + + const events = getAllTestEvents(); + const keyups = events.filter( + (e) => e.type === "keyup" && e.data.code === "KeyS", + ); + expect(keyups.length).toBe(1); + expect((keyups[0] as { data: { estimated?: true } }).data.estimated).toBe( + true, + ); + }); + + it("uses average duration for estimated keyup timing", () => { + logTestEvent("timer", 1000, timerData("start", 0)); + // KeyA held for 80ms + logTestEvent("keydown", 1100, keyDown("KeyA")); + logTestEvent("keyup", 1180, keyUp("KeyA")); + // KeyS held for 120ms + logTestEvent("keydown", 1200, keyDown("KeyS")); + logTestEvent("keyup", 1320, keyUp("KeyS")); + // KeyD still held at 1400 + logTestEvent("keydown", 1400, keyDown("KeyD")); + + forceReleaseAllKeys(); + + const events = getAllTestEvents(); + const keyup = events.find( + (e) => e.type === "keyup" && e.data.code === "KeyD", + ); + // avg duration = (80+120)/2 = 100, so keyup at 1400+100 = 1500, testMs = 1500 - 1000 = 500 + expect(keyup).toBeDefined(); + expect(keyup?.testMs).toBe(500); + }); + + it("uses default 80ms when no completed key durations exist", () => { + logTestEvent("timer", 1000, timerData("start", 0)); + logTestEvent("keydown", 1200, keyDown("KeyA")); + + forceReleaseAllKeys(); + + const events = getAllTestEvents(); + const keyup = events.find( + (e) => e.type === "keyup" && e.data.code === "KeyA", + ); + expect(keyup).toBeDefined(); + expect(keyup?.testMs).toBe(280); + }); + + it("does nothing when no keys are pressed", () => { + logTestEvent("timer", 1000, timerData("start", 0)); + logTestEvent("keydown", 1100, keyDown("KeyA")); + logTestEvent("keyup", 1180, keyUp("KeyA")); + + // const beforeCount = getAllTestEvents().length; + forceReleaseAllKeys(); + // cache invalidated, re-get + resetTestEvents(); + // no new events should have been added — but we can't easily check after reset + // so instead verify no error is thrown + }); + }); }); diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts index 24e68977d08d..8a8ab0044f63 100644 --- a/frontend/__tests__/test/events/stats.spec.ts +++ b/frontend/__tests__/test/events/stats.spec.ts @@ -59,7 +59,6 @@ import { getChars, getInputHistory, getWpmHistory, - forceReleaseAllKeys, __testing as statsTesting, } from "../../../src/ts/test/events/stats"; import type { @@ -1097,74 +1096,4 @@ describe("stats.ts", () => { expect(wpm).toEqual([132]); }); }); - - describe("forceReleaseAllKeys", () => { - it("creates synthetic keyup events for pressed keys", () => { - logTestEvent("timer", 1000, timer("start", 0)); - logTestEvent("keydown", 1100, keyDown("KeyA")); - logTestEvent("keyup", 1180, keyUp("KeyA")); - // KeyS is still held - logTestEvent("keydown", 1200, keyDown("KeyS")); - - forceReleaseAllKeys(); - - const events = getAllTestEvents(); - const keyups = events.filter( - (e) => e.type === "keyup" && e.data.code === "KeyS", - ); - expect(keyups.length).toBe(1); - expect((keyups[0] as { data: { estimated?: true } }).data.estimated).toBe( - true, - ); - }); - - it("uses average duration for estimated keyup timing", () => { - logTestEvent("timer", 1000, timer("start", 0)); - // KeyA held for 80ms - logTestEvent("keydown", 1100, keyDown("KeyA")); - logTestEvent("keyup", 1180, keyUp("KeyA")); - // KeyS held for 120ms - logTestEvent("keydown", 1200, keyDown("KeyS")); - logTestEvent("keyup", 1320, keyUp("KeyS")); - // KeyD still held at 1400 - logTestEvent("keydown", 1400, keyDown("KeyD")); - - forceReleaseAllKeys(); - - const events = getAllTestEvents(); - const keyup = events.find( - (e) => e.type === "keyup" && e.data.code === "KeyD", - ); - // avg duration = (80+120)/2 = 100, so keyup at 1400+100 = 1500, testMs = 1500 - 1000 = 500 - expect(keyup).toBeDefined(); - expect(keyup?.testMs).toBe(500); - }); - - it("uses default 80ms when no completed key durations exist", () => { - logTestEvent("timer", 1000, timer("start", 0)); - logTestEvent("keydown", 1200, keyDown("KeyA")); - - forceReleaseAllKeys(); - - const events = getAllTestEvents(); - const keyup = events.find( - (e) => e.type === "keyup" && e.data.code === "KeyA", - ); - expect(keyup).toBeDefined(); - expect(keyup?.testMs).toBe(280); - }); - - it("does nothing when no keys are pressed", () => { - logTestEvent("timer", 1000, timer("start", 0)); - logTestEvent("keydown", 1100, keyDown("KeyA")); - logTestEvent("keyup", 1180, keyUp("KeyA")); - - // const beforeCount = getAllTestEvents().length; - forceReleaseAllKeys(); - // cache invalidated, re-get - resetTestEvents(); - // no new events should have been added — but we can't easily check after reset - // so instead verify no error is thrown - }); - }); }); diff --git a/frontend/src/ts/test/events/data.ts b/frontend/src/ts/test/events/data.ts index ee0664d13cd5..ffd24f6a2a11 100644 --- a/frontend/src/ts/test/events/data.ts +++ b/frontend/src/ts/test/events/data.ts @@ -15,7 +15,7 @@ import { } from "./types"; import { keysToTrack } from "./helpers"; import { Keycode } from "../../constants/keys"; -import { roundTo2 } from "@monkeytype/util/numbers"; +import { mean, roundTo2 } from "@monkeytype/util/numbers"; import { resultCalculating } from "../test-state"; let keydownEvents: KeydownEvent[] = []; @@ -351,6 +351,30 @@ export function getEventsPerWord( return eventsPerWordIndex; } +export function forceReleaseAllKeys(): void { + const keydownMsByCode = new Map(); + for (const e of keydownEvents) keydownMsByCode.set(e.data.code, e.ms); + + const durations: number[] = []; + for (const e of keyupEvents) { + const downMs = keydownMsByCode.get(e.data.code); + if (downMs === undefined) continue; + const d = e.ms - downMs; + if (d > 0) durations.push(d); + keydownMsByCode.delete(e.data.code); + } + + // empty → test ended with all keys still held; will be "too short" anyway, magic number is fine + const avg = durations.length === 0 ? 80 : roundTo2(mean(durations)); + + for (const [key, { timestamp }] of pressedKeys.entries()) { + logTestEvent("keyup", timestamp + avg, { + code: key, //entries is not picking up the type + estimated: true, + }); + } +} + export const __testing = { resetPressedKeys(): void { pressedKeys = new Map(); diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 6fdd70e0cfb9..9c325728a032 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -1,16 +1,11 @@ -import { - getAllTestEvents, - getEventsPerWord, - getPressedKeys, - logTestEvent, -} from "./data"; +import { getAllTestEvents, getEventsPerWord } from "./data"; import * as TestWords from "../../test/test-words"; import { CharCounts, countChars } from "../../utils/strings"; import * as CustomText from "../../test/custom-text"; import { getInputFromDom } from "./helpers"; import { activeWordIndex, bailedOut, koreanStatus } from "../test-state"; import { calculateWpm } from "../../utils/numbers"; -import { mean, roundTo2 } from "@monkeytype/util/numbers"; +import { roundTo2 } from "@monkeytype/util/numbers"; import { TestEventNoMs } from "./types"; import { Config } from "../../config/store"; import { isFunboxActiveWithProperty } from "../funbox/list"; @@ -731,26 +726,6 @@ export function getKeypressDurations(): number[] { return durations; } -export function forceReleaseAllKeys(): void { - const filteredDurations = getKeypressDurations().filter((d) => d > 0); - - let avg: number; - if (filteredDurations.length === 0) { - // this means the test ended while all keys were still held - probably safe to ignore - // since this will result in a "too short" test anyway, but ill just set it to a magic number - avg = 80; - } else { - avg = roundTo2(mean(filteredDurations)); - } - - for (const [key, { timestamp }] of getPressedKeys().entries()) { - logTestEvent("keyup", timestamp + avg, { - code: key, //entries is not picking up the type - estimated: true, - }); - } -} - export const __testing = { getTimerBoundaries, getTargetWord, diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 88711c5aff3d..7ab826b97d5f 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -103,6 +103,7 @@ import { cleanupData, logEventsDataToTheConsoleTable, getAllTestEvents, + forceReleaseAllKeys, } from "./events/data"; import { getKeypressDurations, @@ -118,7 +119,6 @@ import { getErrorCountHistory, getWpmHistory, getAfkDuration, - forceReleaseAllKeys, getKeypressesPerSecond, getInputHistory as getEventsInputHistory, } from "./events/stats";