diff --git a/frontend/__tests__/test/events/data.spec.ts b/frontend/__tests__/test/events/data.spec.ts index 14cbaaaccd79..572f3aa9f1e5 100644 --- a/frontend/__tests__/test/events/data.spec.ts +++ b/frontend/__tests__/test/events/data.spec.ts @@ -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", () => { diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts index 3c7d03a55a32..4a647dd650b8 100644 --- a/frontend/__tests__/test/events/stats.spec.ts +++ b/frontend/__tests__/test/events/stats.spec.ts @@ -55,6 +55,7 @@ import { getChars, getWpmHistory, forceReleaseAllKeys, + getCorrectedWords, __testing as statsTesting, } from "../../../src/ts/test/events/stats"; import type { @@ -112,7 +113,7 @@ function timer( if (event === "step") { return { event, timer: timerVal, drift: 0 }; } - return { event, timer: timerVal }; + return { event, timer: timerVal, date: 0 }; } // Helper: sets up a basic test with timer start, steps at 1s intervals, @@ -677,6 +678,284 @@ describe("stats.ts", () => { }); }); + describe("getCorrectedWords", () => { + it("returns input as-is when no corrections made", () => { + logTestEvent("timer", 1000, timer("start", 0)); + logTestEvent( + "input", + 1100, + input({ charIndex: 0, wordIndex: 0, data: "t" }), + ); + logTestEvent( + "input", + 1150, + input({ charIndex: 1, wordIndex: 0, data: "e" }), + ); + logTestEvent( + "input", + 1200, + input({ charIndex: 2, wordIndex: 0, data: "s" }), + ); + logTestEvent( + "input", + 1250, + input({ charIndex: 3, wordIndex: 0, data: "t" }), + ); + + expect(getCorrectedWords()).toEqual(["test"]); + }); + + it("returns last deleted char per position (xact -> fact)", () => { + logTestEvent("timer", 1000, timer("start", 0)); + // type "xact" + logTestEvent( + "input", + 1100, + input({ charIndex: 0, wordIndex: 0, data: "x" }), + ); + logTestEvent( + "input", + 1150, + input({ charIndex: 1, wordIndex: 0, data: "a" }), + ); + logTestEvent( + "input", + 1200, + input({ charIndex: 2, wordIndex: 0, data: "c" }), + ); + logTestEvent( + "input", + 1250, + input({ charIndex: 3, wordIndex: 0, data: "t" }), + ); + // delete all + logTestEvent("input", 1300, { + charIndex: 3, + wordIndex: 0, + inputType: "deleteContentBackward", + } as InputEventData); + logTestEvent("input", 1350, { + charIndex: 2, + wordIndex: 0, + inputType: "deleteContentBackward", + } as InputEventData); + logTestEvent("input", 1400, { + charIndex: 1, + wordIndex: 0, + inputType: "deleteContentBackward", + } as InputEventData); + logTestEvent("input", 1450, { + charIndex: 0, + wordIndex: 0, + inputType: "deleteContentBackward", + } as InputEventData); + // type "fact" + logTestEvent( + "input", + 1500, + input({ charIndex: 0, wordIndex: 0, data: "f" }), + ); + logTestEvent( + "input", + 1550, + input({ charIndex: 1, wordIndex: 0, data: "a" }), + ); + logTestEvent( + "input", + 1600, + input({ charIndex: 2, wordIndex: 0, data: "c" }), + ); + logTestEvent( + "input", + 1650, + input({ charIndex: 3, wordIndex: 0, data: "t" }), + ); + + expect(getCorrectedWords()).toEqual(["xact"]); + }); + + it("returns last deleted char per position across multiple corrections (xest -> west -> test)", () => { + logTestEvent("timer", 1000, timer("start", 0)); + // type "xest" + logTestEvent( + "input", + 1100, + input({ charIndex: 0, wordIndex: 0, data: "x" }), + ); + logTestEvent( + "input", + 1150, + input({ charIndex: 1, wordIndex: 0, data: "e" }), + ); + logTestEvent( + "input", + 1200, + input({ charIndex: 2, wordIndex: 0, data: "s" }), + ); + logTestEvent( + "input", + 1250, + input({ charIndex: 3, wordIndex: 0, data: "t" }), + ); + // delete all + logTestEvent("input", 1300, { + charIndex: 3, + wordIndex: 0, + inputType: "deleteWordBackward", + } as InputEventData); + // type "west" + logTestEvent( + "input", + 1400, + input({ charIndex: 0, wordIndex: 0, data: "w" }), + ); + logTestEvent( + "input", + 1450, + input({ charIndex: 1, wordIndex: 0, data: "e" }), + ); + logTestEvent( + "input", + 1500, + input({ charIndex: 2, wordIndex: 0, data: "s" }), + ); + logTestEvent( + "input", + 1550, + input({ charIndex: 3, wordIndex: 0, data: "t" }), + ); + // delete all + logTestEvent("input", 1600, { + charIndex: 3, + wordIndex: 0, + inputType: "deleteWordBackward", + } as InputEventData); + // type "test" + logTestEvent( + "input", + 1700, + input({ charIndex: 0, wordIndex: 0, data: "t" }), + ); + logTestEvent( + "input", + 1750, + input({ charIndex: 1, wordIndex: 0, data: "e" }), + ); + logTestEvent( + "input", + 1800, + input({ charIndex: 2, wordIndex: 0, data: "s" }), + ); + logTestEvent( + "input", + 1850, + input({ charIndex: 3, wordIndex: 0, data: "t" }), + ); + + expect(getCorrectedWords()).toEqual(["west"]); + }); + + it("handles partial correction (tset -> delete last 2 -> st)", () => { + logTestEvent("timer", 1000, timer("start", 0)); + // type "tset" + logTestEvent( + "input", + 1100, + input({ charIndex: 0, wordIndex: 0, data: "t" }), + ); + logTestEvent( + "input", + 1150, + input({ charIndex: 1, wordIndex: 0, data: "s" }), + ); + logTestEvent( + "input", + 1200, + input({ charIndex: 2, wordIndex: 0, data: "e" }), + ); + logTestEvent( + "input", + 1250, + input({ charIndex: 3, wordIndex: 0, data: "t" }), + ); + // delete last 2 + logTestEvent("input", 1300, { + charIndex: 3, + wordIndex: 0, + inputType: "deleteContentBackward", + } as InputEventData); + logTestEvent("input", 1350, { + charIndex: 2, + wordIndex: 0, + inputType: "deleteContentBackward", + } as InputEventData); + // type "st" + logTestEvent( + "input", + 1400, + input({ charIndex: 2, wordIndex: 0, data: "s" }), + ); + logTestEvent( + "input", + 1450, + input({ charIndex: 3, wordIndex: 0, data: "t" }), + ); + + // pos 0: "t" never deleted, pos 1: "s" never deleted, pos 2: "e" deleted, pos 3: "t" deleted + expect(getCorrectedWords()).toEqual(["tset"]); + }); + + it("handles multiple words", () => { + logTestEvent("timer", 1000, timer("start", 0)); + // word 0: type "ab" correctly + logTestEvent( + "input", + 1100, + input({ charIndex: 0, wordIndex: 0, data: "a" }), + ); + logTestEvent( + "input", + 1150, + input({ charIndex: 1, wordIndex: 0, data: "b" }), + ); + // word 1: type "xy", delete both, type "zw" + logTestEvent( + "input", + 1200, + input({ charIndex: 0, wordIndex: 1, data: "x" }), + ); + logTestEvent( + "input", + 1250, + input({ charIndex: 1, wordIndex: 1, data: "y" }), + ); + logTestEvent("input", 1300, { + charIndex: 1, + wordIndex: 1, + inputType: "deleteContentBackward", + } as InputEventData); + logTestEvent("input", 1350, { + charIndex: 1, + wordIndex: 1, + inputType: "deleteContentBackward", + } as InputEventData); + logTestEvent( + "input", + 1400, + input({ charIndex: 0, wordIndex: 1, data: "z" }), + ); + logTestEvent( + "input", + 1450, + input({ charIndex: 1, wordIndex: 1, data: "w" }), + ); + + const result = getCorrectedWords(); + expect(result[0]).toEqual("ab"); + expect(result[1]).toEqual("xy"); + }); + }); + describe("forceReleaseAllKeys", () => { it("creates synthetic keyup events for pressed keys", () => { logTestEvent("timer", 1000, timer("start", 0)); diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index e7e62a75f307..fd4c83bffa46 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -24,11 +24,9 @@ import { randomizeTheme } from "../controllers/theme-controller"; import { showModal } from "../states/modals"; import { showErrorNotification, - showSuccessNotification, clearAllNotifications, } from "../states/notifications"; import * as VideoAdPopup from "../popups/video-ad-popup"; -import * as TestStats from "../test/test-stats"; import { Command, CommandsSubgroup } from "./types"; import { buildCommandForConfigKey } from "./util"; import { CommandlineConfigMetadataObject } from "./commandline-metadata"; @@ -288,22 +286,23 @@ export const commands: CommandsSubgroup = { alert(await caches.keys()); }, }, - { - id: "copyResultStats", - display: "Copy result stats", - icon: "fa-cog", - visible: false, - exec: async (): Promise => { - navigator.clipboard - .writeText(JSON.stringify(TestStats.getStats())) - .then(() => { - showSuccessNotification("Copied to clipboard"); - }) - .catch((e: unknown) => { - showErrorNotification("Failed to copy to clipboard", { error: e }); - }); - }, - }, + // todo: bring back? + // { + // id: "copyResultStats", + // display: "Copy result stats", + // icon: "fa-cog", + // visible: false, + // exec: async (): Promise => { + // navigator.clipboard + // .writeText(JSON.stringify(TestStats.getStats())) + // .then(() => { + // showSuccessNotification("Copied to clipboard"); + // }) + // .catch((e: unknown) => { + // showErrorNotification("Failed to copy to clipboard", { error: e }); + // }); + // }, + // }, { id: "fpsCounter", display: "FPS counter...", diff --git a/frontend/src/ts/commandline/lists/result-screen.ts b/frontend/src/ts/commandline/lists/result-screen.ts index 1debee3ac804..2f5017aabd4d 100644 --- a/frontend/src/ts/commandline/lists/result-screen.ts +++ b/frontend/src/ts/commandline/lists/result-screen.ts @@ -5,13 +5,13 @@ import { showErrorNotification, showSuccessNotification, } from "../../states/notifications"; -import * as TestInput from "../../test/test-input"; import * as TestState from "../../test/test-state"; import * as TestWords from "../../test/test-words"; import { Config } from "../../config/store"; import * as PractiseWords from "../../test/practise-words"; import { Command, CommandsSubgroup } from "../types"; import * as TestScreenshot from "../../test/test-screenshot"; +import { getInputHistory } from "../../test/events/stats"; const practiceSubgroup: CommandsSubgroup = { title: "Practice words...", @@ -141,8 +141,8 @@ const commands: Command[] = [ exec: (): void => { const words = ( Config.mode === "zen" - ? TestInput.input.getHistory() - : TestWords.words.list.slice(0, TestInput.input.getHistory().length) + ? getInputHistory() + : TestWords.words.list.slice(0, getInputHistory().length) ).join(" "); navigator.clipboard.writeText(words).then( diff --git a/frontend/src/ts/controllers/chart-controller.ts b/frontend/src/ts/controllers/chart-controller.ts index 572c5060ee24..2142abf340c4 100644 --- a/frontend/src/ts/controllers/chart-controller.ts +++ b/frontend/src/ts/controllers/chart-controller.ts @@ -58,13 +58,13 @@ Chart.defaults.elements.line.fill = "origin"; import "chartjs-adapter-date-fns"; import { Config } from "../config/store"; import { configEvent } from "../events/config"; -import * as TestInput from "../test/test-input"; import * as Arrays from "../utils/arrays"; import { blendTwoHexColors } from "../utils/colors"; import { typedKeys } from "../utils/misc"; import { getTheme } from "../states/theme"; import { Theme } from "../constants/themes"; import { createDebouncedEffectOn } from "../hooks/effects"; +import { getWordIndexesForSecond } from "../test/events/stats"; export class ChartWithUpdateColors< TType extends ChartType = ChartType, @@ -274,8 +274,7 @@ export const result = new ChartWithUpdateColors< prevTi = ti; try { const keypressIndex = Math.round(parseFloat(ti.label)) - 1; - const wordsToHighlight = - TestInput.errorHistory[keypressIndex]?.words; + const wordsToHighlight = getWordIndexesForSecond(keypressIndex); const unique = [...new Set(wordsToHighlight)]; const firstHighlightWordIndex = unique[0]; diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts index 10a5f7efaf93..2e1ccac678b0 100644 --- a/frontend/src/ts/index.ts +++ b/frontend/src/ts/index.ts @@ -15,7 +15,6 @@ import * as DB from "./db"; import "./ui"; import "./controllers/ad-controller"; import { Config } from "./config/store"; -import * as TestStats from "./test/test-stats"; import * as Replay from "./test/replay"; import * as TestTimer from "./test/test-timer"; import * as Result from "./test/result"; @@ -88,7 +87,6 @@ addToGlobal({ snapshot: DB.getSnapshot, config: Config, glarsesMode: enable, - stats: TestStats.getStats, replay: Replay.getReplayExport, enableTimerDebug: TestTimer.enableTimerDebug, getTimerStats: TestTimer.getTimerStats, diff --git a/frontend/src/ts/input/handlers/before-delete.ts b/frontend/src/ts/input/handlers/before-delete.ts index c6bbf68c0f57..8809a264383d 100644 --- a/frontend/src/ts/input/handlers/before-delete.ts +++ b/frontend/src/ts/input/handlers/before-delete.ts @@ -1,10 +1,10 @@ import { Config } from "../../config/store"; -import * as TestInput from "../../test/test-input"; import * as TestState from "../../test/test-state"; import * as TestWords from "../../test/test-words"; import { getInputElementValue } from "../input-element"; import * as TestUI from "../../test/test-ui"; import { isAwaitingNextWord } from "../state"; +import { getInputForWord } from "../../test/events/stats"; export function onBeforeDelete(event: InputEvent): void { if (!TestState.isActive) { @@ -12,12 +12,15 @@ export function onBeforeDelete(event: InputEvent): void { return; } if (TestState.testRestarting) { + event.preventDefault(); return; } if (isAwaitingNextWord()) { + event.preventDefault(); return; } if (TestState.resultCalculating) { + event.preventDefault(); return; } @@ -42,7 +45,7 @@ export function onBeforeDelete(event: InputEvent): void { const confidence = Config.confidenceMode; const previousWordCorrect = - (TestInput.input.get(TestState.activeWordIndex - 1) ?? "") === + getInputForWord(TestState.activeWordIndex - 1) === TestWords.words.getText(TestState.activeWordIndex - 1); if (confidence === "on" && inputIsEmpty && !previousWordCorrect) { diff --git a/frontend/src/ts/input/handlers/delete.ts b/frontend/src/ts/input/handlers/delete.ts index 8d2a44340a47..f9a93c885a6d 100644 --- a/frontend/src/ts/input/handlers/delete.ts +++ b/frontend/src/ts/input/handlers/delete.ts @@ -19,7 +19,6 @@ export function onDelete(inputType: DeleteInputType, now: number): void { TestInput.input.syncWithInputElement(); Replay.addReplayEvent("setLetterIndex", TestInput.input.current.length); - TestInput.setCurrentNotAfk(); const beforeDeleteOnlyTabs = /^\t*$/.test(inputBeforeDelete); const allTabsCorrect = TestWords.words diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index 6145641c5a49..f474e4d06e30 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -162,25 +162,11 @@ export async function onInsertText(options: OnInsertTextParams): Promise { } // general per keypress updates - TestInput.setCurrentNotAfk(); Replay.addReplayEvent(correct ? "correctLetter" : "incorrectLetter", data); - TestInput.incrementAccuracy(correct); WeakSpot.updateScore(data, correct); - TestInput.incrementKeypressCount(); - TestInput.pushKeypressWord(wordIndex); - if (!correct) { - TestInput.incrementKeypressErrors(); - TestInput.pushMissedWord(TestWords.words.getCurrentText()); - } if (Config.keymapMode === "react") { flash(data, correct); } - if (testInput.length === 0 && !isCompositionEnding) { - TestInput.setBurstStart(now); - } - if (!shouldGoToNextWord) { - TestInput.corrected.update(data, correct); - } // handing cases where last char needs to be removed // this is here and not in beforeInsertText because we want to penalize for incorrect spaces @@ -221,6 +207,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { correctInsert: correct, isCompositionEnding: isCompositionEnding === true, zenNewline: charIsNewline && Config.mode === "zen", + now, }); lastBurst = result.lastBurst; increasedWordIndex = result.increasedWordIndex; diff --git a/frontend/src/ts/input/handlers/keydown.ts b/frontend/src/ts/input/handlers/keydown.ts index a2beca801445..9194d3ca7419 100644 --- a/frontend/src/ts/input/handlers/keydown.ts +++ b/frontend/src/ts/input/handlers/keydown.ts @@ -1,5 +1,4 @@ import { Config } from "../../config/store"; -import * as TestInput from "../../test/test-input"; import * as TestLogic from "../../test/test-logic"; import { getCharFromEvent } from "../../test/layout-emulator"; import * as Monkey from "../../test/monkey"; @@ -133,9 +132,6 @@ export async function onKeydown(event: KeyboardEvent): Promise { } const now = performance.now(); - if (!TestState.resultCalculating) { - TestInput.recordKeydownTime(now, event); - } logTestEvent("keydown", now, { code: getTestEventCode(event), diff --git a/frontend/src/ts/input/handlers/keyup.ts b/frontend/src/ts/input/handlers/keyup.ts index 2e04d12a7aba..5da7d8a86985 100644 --- a/frontend/src/ts/input/handlers/keyup.ts +++ b/frontend/src/ts/input/handlers/keyup.ts @@ -1,12 +1,10 @@ import { Config } from "../../config/store"; -import * as TestInput from "../../test/test-input"; import * as Monkey from "../../test/monkey"; import { logTestEvent } from "../../test/events/data"; import { getTestEventCode } from "../../test/events/helpers"; export async function onKeyup(event: KeyboardEvent): Promise { const now = performance.now(); - TestInput.recordKeyupTime(now, event); logTestEvent("keyup", now, { code: getTestEventCode(event), ctrl: event.ctrlKey, diff --git a/frontend/src/ts/input/helpers/word-navigation.ts b/frontend/src/ts/input/helpers/word-navigation.ts index 897d115e65c0..2a5ab05dc118 100644 --- a/frontend/src/ts/input/helpers/word-navigation.ts +++ b/frontend/src/ts/input/helpers/word-navigation.ts @@ -9,19 +9,20 @@ import { getActiveFunboxesWithFunction, isFunboxActiveWithProperty, } from "../../test/funbox/list"; -import * as TestStats from "../../test/test-stats"; import * as Replay from "../../test/replay"; import * as Funbox from "../../test/funbox/funbox"; import { showLoaderBar, hideLoaderBar } from "../../states/loader-bar"; import { setInputElementValue } from "../input-element"; import { setAwaitingNextWord } from "../state"; import { DeleteInputType } from "./input-type"; +import { getInputForWord, getWordBurst } from "../../test/events/stats"; type GoToNextWordParams = { correctInsert: boolean; // this is used to tell test ui to update the word before moving to the next word (in case of a composition that ends with a space) isCompositionEnding: boolean; zenNewline?: boolean; + now: number; }; type GoToNextWordReturn = { @@ -33,6 +34,7 @@ export async function goToNextWord({ correctInsert, isCompositionEnding, zenNewline, + now, }: GoToNextWordParams): Promise { const ret = { increasedWordIndex: false, @@ -56,16 +58,14 @@ export async function goToNextWord({ } //burst calculation and fail - const burst: number = TestStats.calculateBurst(); - TestInput.pushBurstToHistory(burst); + const burst = getWordBurst(TestState.activeWordIndex, now); ret.lastBurst = burst; PaceCaret.handleSpace(correctInsert, TestWords.words.getCurrentText()); Funbox.toggleScript(TestWords.words.getText(TestState.activeWordIndex + 1)); - TestInput.input.pushHistory(); - TestInput.corrected.pushHistory(); + // TestInput.input.pushHistory(); const lastWord = TestState.activeWordIndex >= TestWords.words.length - 1; if (lastWord) { @@ -88,7 +88,7 @@ export async function goToNextWord({ setInputElementValue(""); TestInput.input.syncWithInputElement(); - void TestUI.afterTestWordChange("forward"); + void TestUI.afterTestWordChange("forward", burst); return ret; } @@ -107,9 +107,8 @@ export function goToPreviousWord( Replay.addReplayEvent("backWord"); - const word = TestInput.input.popHistory(); + const word = getInputForWord(TestState.activeWordIndex); TestState.decreaseActiveWordIndex(); - TestInput.corrected.popHistory(); Funbox.toggleScript(TestWords.words.getText(TestState.activeWordIndex)); diff --git a/frontend/src/ts/input/listeners/composition.ts b/frontend/src/ts/input/listeners/composition.ts index cec96ead8fad..00c75128d13c 100644 --- a/frontend/src/ts/input/listeners/composition.ts +++ b/frontend/src/ts/input/listeners/composition.ts @@ -2,7 +2,6 @@ import { getInputElement } from "../input-element"; import * as CompositionState from "../../legacy-states/composition"; import * as TestState from "../../test/test-state"; import * as TestLogic from "../../test/test-logic"; -import * as TestInput from "../../test/test-input"; import { setLastInsertCompositionTextData } from "../state"; import * as CompositionDisplay from "../../elements/composition-display"; import { onInsertText } from "../handlers/insert-text"; @@ -25,9 +24,6 @@ inputEl.addEventListener("compositionstart", (event) => { if (!TestState.isActive) { TestLogic.startTest(now); } - if (TestInput.input.current.length === 0) { - TestInput.setBurstStart(now); - } logTestEvent("composition", now, { event: "start", diff --git a/frontend/src/ts/test/events/data.ts b/frontend/src/ts/test/events/data.ts index ebda2b4c76ba..0d4e4ac2a38d 100644 --- a/frontend/src/ts/test/events/data.ts +++ b/frontend/src/ts/test/events/data.ts @@ -14,7 +14,6 @@ import { TimerEventData, } from "./types"; import { keysToTrack } from "./helpers"; -import { start } from "../test-stats"; import { Keycode } from "../../constants/keys"; import { roundTo2 } from "@monkeytype/util/numbers"; import { resultCalculating } from "../test-state"; @@ -222,6 +221,9 @@ export function cleanupData(): void { export function getAllTestEvents(): TestEvent[] { if (cachedAllEvents !== undefined) return cachedAllEvents; + const startEventMs = + timerEvents.find((e) => e.data.event === "start")?.ms ?? 0; + // cachedAllEvents = testData300; // return cachedAllEvents; cachedAllEvents = [ @@ -237,7 +239,7 @@ export function getAllTestEvents(): TestEvent[] { (a.type === "timer" ? 1 : 0) - (b.type === "timer" ? 1 : 0), ) .map((event) => { - event.testMs = roundTo2(event.ms - start); + event.testMs = roundTo2(event.ms - startEventMs); return event; }); @@ -308,6 +310,30 @@ export function getPressedKeys(): Map< return pressedKeys; } +export function getInputEventsForWord(wordIndex: number): InputEvent[] { + const events = getAllTestEvents(); + const result: InputEvent[] = []; + for (const event of events) { + if (event.type !== "input") continue; + + let eventWordIndex = event.data.wordIndex; + + if ( + (event.data.inputType === "deleteWordBackward" || + event.data.inputType === "deleteContentBackward") && + event.data.charIndex === 0 && + eventWordIndex > 0 + ) { + eventWordIndex -= 1; + } + + if (eventWordIndex === wordIndex) { + result.push(event); + } + } + return result; +} + export function getInputEventsPerWord( startMs?: number, testMsLimit?: number, diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 3f8ae474ba7b..9ba1bdeb89fe 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -1,6 +1,7 @@ import { getAllTestEvents, getInputEvents, + getInputEventsForWord, getInputEventsPerWord, getPressedKeys, logTestEvent, @@ -183,6 +184,28 @@ export function getRawPerSecond(): number[] { }); } +export function getCurrentTestDurationMs(now: number): number { + const events = getAllTestEvents(); + + let start: number | undefined; + + for (const event of events) { + if ( + start === undefined && + event.type === "timer" && + event.data.event === "start" + ) { + start = event.ms; + } + } + + if (start === undefined) { + return 0; + } + + return now - start; +} + export function getTestDurationMs(): number { const events = getAllTestEvents(); @@ -224,6 +247,36 @@ export function getTestDurationMs(): number { return end; } +export function getDateBasedTestDurationMs(): number { + const events = getAllTestEvents(); + + let start: number | undefined; + let end: number | undefined; + + for (const event of events) { + if ( + start === undefined && + event.type === "timer" && + event.data.event === "start" + ) { + start = event.data.date; + } + if ( + end === undefined && + event.type === "timer" && + event.data.event === "end" + ) { + end = event.data.date; + } + } + + if (start === undefined || end === undefined) { + return 0; + } + + return end - start; +} + function getTargetWord( wordIndex: number, simulatedInput: string, @@ -243,12 +296,103 @@ function getTargetWord( } } -export function getChars(): CharCounts { +export function getCurrentWpmAndRaw(now?: number): { + wpm: number; + raw: number; +} { + const chars = getChars(true); + const currentTestDurationMs = getCurrentTestDurationMs( + now ?? performance.now(), + ); + const wpm = Math.round( + calculateWpm(chars.correctWord, currentTestDurationMs / 1000), + ); + const raw = Math.round( + calculateWpm( + chars.allCorrect + chars.extra + chars.incorrect, + currentTestDurationMs / 1000, + ), + ); + return { wpm, raw }; +} + +export function getCurrentAccuracy(): number { + const events = getAllTestEvents(); + + let correct = 0; + let total = 0; + + for (const event of events) { + if (event.type === "input" && "correct" in event.data) { + total++; + if (event.data.correct) { + correct++; + } + } + } + + return total === 0 ? 100 : (correct / total) * 100; +} + +//todo: composition start must be the start time for burst calculation +function computeBurst(events: InputEvent[], now?: number): number { + const input = getSimulatedInput(events); + + let inputLength = input.length; + if (!input.endsWith(" ")) { + inputLength += 1; // account for space that will be added on word submit + } + + let firstKeypressTime: number | undefined; + let lastKeypressTime: number | undefined; + + for (const event of events) { + if (event.type === "input" && event.data.inputType === "insertText") { + if (event.data.charIndex === 0) { + firstKeypressTime = event.ms; + } + if (event.data.data === " ") { + lastKeypressTime = event.ms; + } + } + } + + if (firstKeypressTime === undefined || input.length === 0) { + return 0; + } + + if (lastKeypressTime !== undefined && lastKeypressTime < firstKeypressTime) { + lastKeypressTime = undefined; + } + + const endTime = lastKeypressTime ?? now ?? performance.now(); + + const durationSeconds = (endTime - firstKeypressTime) / 1000; + if (durationSeconds <= 0) return Infinity; + + return Math.round(calculateWpm(inputLength, durationSeconds)); +} + +export function getWordBurst(wordIndex: number, now?: number): number { + const events = getInputEventsForWord(wordIndex); + return computeBurst(events, now); +} + +export function getBurstHistory(): number[] { + const eventsPerWord = getInputEventsPerWord(); + const burstHistory: number[] = []; + for (let i = 0; i < TestWords.words.length; i++) { + burstHistory.push(computeBurst(eventsPerWord.get(i) ?? [])); + } + return burstHistory; +} + +export function getChars(countPartialLastWord = false): CharCounts { const eventsPerWordIndex = getInputEventsPerWord(); const isTimedTest = Config.mode === "time" || (Config.mode === "custom" && CustomText.getLimit().mode === "time"); - const shouldCountPartialLastWord = isTimedTest; + const shouldCountPartialLastWord = isTimedTest || countPartialLastWord; let allCorrect = 0; let correctWord = 0; @@ -295,6 +439,25 @@ export function getChars(): CharCounts { }; } +export function getInputForWord(wordIndex: number): string { + const events = getInputEventsForWord(wordIndex); + return getSimulatedInput(events).trimEnd(); +} + +export function getInputHistory(): string[] { + console.log("getting input history"); + console.trace("getting input"); + const eventsPerWordIndex = getInputEventsPerWord(); + const history: string[] = []; + + for (const events of eventsPerWordIndex.values()) { + const simulatedInput = getSimulatedInput(events); + history.push(simulatedInput.trimEnd()); + } + + return history; +} + export function getAccuracy(): { correct: number; incorrect: number; @@ -375,6 +538,54 @@ export function getKeypressOverlap(): number { return roundTo2(overlap); } +export function getIncorrectWordIndexesForSecond(second: number): number[] { + const events = getAllTestEvents(); + const boundaries = getTimerBoundaries(events); + + const boundary = boundaries[second]; + if (boundary === undefined) return []; + + const prevBoundary = second > 0 ? boundaries[second - 1] : undefined; + const wordIndexes = new Set(); + + for (const event of events) { + if (prevBoundary !== undefined && event.testMs <= prevBoundary) continue; + if (event.testMs > boundary) break; + + if ( + event.type === "input" && + event.data.inputType === "insertText" && + !event.data.correct + ) { + wordIndexes.add(event.data.wordIndex); + } + } + + return [...wordIndexes]; +} + +export function getWordIndexesForSecond(second: number): number[] { + const events = getAllTestEvents(); + const boundaries = getTimerBoundaries(events); + + const boundary = boundaries[second]; + if (boundary === undefined) return []; + + const prevBoundary = second > 0 ? boundaries[second - 1] : undefined; + const wordIndexes = new Set(); + + for (const event of events) { + if (prevBoundary !== undefined && event.testMs <= prevBoundary) continue; + if (event.testMs > boundary) break; + + if (event.type === "input" && event.data.inputType === "insertText") { + wordIndexes.add(event.data.wordIndex); + } + } + + return [...wordIndexes]; +} + export function getErrorCountHistory(): number[] { const { counts } = countPerInterval( (e) => @@ -443,6 +654,62 @@ export function getWpmHistory(): number[] { return wpmHistory; } +export function getRawHistory(): number[] { + const events = getAllTestEvents(); + const timerBoundaries = getTimerBoundaries(events); + const wpmHistory: number[] = []; + + for (const boundary of timerBoundaries) { + const eventsPerWord = getInputEventsPerWord(undefined, boundary); + + // Compute simulated inputs first so we can determine the effective last word + const wordInputs = new Map< + number, + { input: string; events: InputEvent[] } + >(); + let maxWordIndex = 0; + for (const [k, wordEvents] of eventsPerWord) { + const input = getSimulatedInput(wordEvents); + wordInputs.set(k, { input, events: wordEvents }); + // Only count words with non-empty input for maxWordIndex, + // so that fully-deleted words don't prevent earlier words + // from being treated as the last word + if (input.length > 0 && k > maxWordIndex) maxWordIndex = k; + } + + let totalCorrect = 0; + for (const [wordIndex, { input, events: wordEvents }] of wordInputs) { + if (input.length === 0) continue; + + const lastEvt = wordEvents[wordEvents.length - 1]; + let adjustedMax = maxWordIndex; + if ( + lastEvt !== undefined && + lastEvt.data.inputType === "insertText" && + lastEvt.data.data === " " + ) { + adjustedMax = maxWordIndex + 1; + } + const lastWord = wordIndex === adjustedMax; + + const trimmed = lastWord ? input.trimEnd() : input; + const targetWord = + Config.mode === "zen" + ? trimmed + : TestWords.words.getText(wordIndex) + (lastWord ? "" : " "); + + const count = countChars(trimmed, targetWord, lastWord, true); + + totalCorrect += count.allCorrect + count.extra + count.incorrect; + } + + const durationSeconds = boundary / 1000; + wpmHistory.push(Math.round(calculateWpm(totalCorrect, durationSeconds))); + } + + return wpmHistory; +} + export function getAfkDuration(): number { const { counts } = countPerInterval( (e) => e.type === "keydown" || e.type === "input", @@ -506,6 +773,69 @@ export function forceReleaseAllKeys(): void { } } +export function getMissedWords(): Record { + const events = getAllTestEvents(); + + const missedWords: Record = {}; + + for (const event of events) { + if ( + event.type === "input" && + event.data.inputType === "insertText" && + !event.data.correct + ) { + const word = TestWords.words.getText(event.data.wordIndex); + if (missedWords[word] === undefined) { + missedWords[word] = 1; + } else { + missedWords[word]++; + } + } + } + + return missedWords; +} + +export function getCorrectedWords(): string[] { + const ev = getInputEventsPerWord(); + const correctedWords: string[] = []; + + for (const [, events] of ev.entries()) { + const correctedChars: string[] = []; + const currentChars: string[] = []; + let cursorPos = 0; + + for (const event of events) { + if ( + event.data.inputType === "insertText" || + event.data.inputType === "insertCompositionText" + ) { + if (event.data.inputStopped || event.data.data === " ") continue; + currentChars[cursorPos] = event.data.data; + cursorPos++; + } else if (event.data.inputType === "deleteContentBackward") { + if (cursorPos > 0) { + cursorPos--; + correctedChars[cursorPos] = currentChars[cursorPos] ?? ""; + } + } else if (event.data.inputType === "deleteWordBackward") { + while (cursorPos > 0) { + cursorPos--; + correctedChars[cursorPos] = currentChars[cursorPos] ?? ""; + } + } + } + + const result: string[] = []; + for (let i = 0; i < currentChars.length; i++) { + result.push(correctedChars[i] ?? currentChars[i] ?? ""); + } + correctedWords.push(result.join("")); + } + + return correctedWords; +} + export const __testing = { getTimerBoundaries, }; diff --git a/frontend/src/ts/test/events/types.ts b/frontend/src/ts/test/events/types.ts index 94712b8dac63..b2db3ac86c92 100644 --- a/frontend/src/ts/test/events/types.ts +++ b/frontend/src/ts/test/events/types.ts @@ -65,6 +65,7 @@ export type TimerEventData = | { event: "start" | "end"; timer: number; + date: number; }; export type InputEvent = EventProps<"input", InputEventData>; diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 78377be3a229..512975f1bd29 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -28,6 +28,7 @@ import { WordGenError } from "../../utils/word-gen-error"; import { FunboxName, KeymapLayout, Layout } from "@monkeytype/schemas/configs"; import { Language, LanguageObject } from "@monkeytype/schemas/languages"; import { qs } from "../../utils/dom"; +import { getInputForWord } from "../events/stats"; export type FunboxFunctions = { getWord?: (wordset?: Wordset, wordIndex?: number) => string; @@ -62,7 +63,7 @@ async function readAheadHandleKeydown(event: KeyboardEvent): Promise { event.key === "Backspace" && !isCorrect && (TestInput.input.current !== "" || - TestInput.input.getHistory(TestState.activeWordIndex - 1) !== + getInputForWord(TestState.activeWordIndex - 1) !== TestWords.words.getText(TestState.activeWordIndex - 1) || Config.freedomMode) ) { @@ -425,7 +426,7 @@ const list: Partial> = { const outOf: number = TestWords.words.length; const wordsPerLayout = Math.floor(outOf / layouts.length); const index = Math.floor( - (TestInput.input.getHistory().length + 1) / wordsPerLayout, + (TestState.activeWordIndex + 1) / wordsPerLayout, ); const mod = wordsPerLayout - ((TestState.activeWordIndex + 1) % wordsPerLayout); diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index b1ee407c3dfd..35d57cf57a4b 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -4,11 +4,15 @@ import { showNoticeNotification } from "../states/notifications"; import { Config } from "../config/store"; import { setConfig } from "../config/setters"; import * as CustomText from "./custom-text"; -import * as TestInput from "./test-input"; import { configEvent } from "../events/config"; import { setCustomTextName } from "../legacy-states/custom-text-name"; import { Mode } from "@monkeytype/schemas/shared"; import { CustomTextSettings } from "@monkeytype/schemas/results"; +import { + getBurstHistory, + getInputHistory, + getMissedWords, +} from "./events/stats"; type Before = { mode: Mode | null; @@ -37,11 +41,13 @@ export function init( limit = 10; } + const missedWords = getMissedWords(); + // missed word, previous word, count let sortableMissedWords: [string, number][] = []; if (missed === "words") { - Object.keys(TestInput.missedWords).forEach((missedWord) => { - const missedWordCount = TestInput.missedWords[missedWord]; + Object.keys(missedWords).forEach((missedWord) => { + const missedWordCount = missedWords[missedWord]; if (missedWordCount !== undefined) { sortableMissedWords.push([missedWord, missedWordCount]); } @@ -56,7 +62,7 @@ export function init( if (missed === "biwords") { for (let i = 0; i < TestWords.words.length; i++) { const missedWord = TestWords.words.getText(i); - const missedWordCount = TestInput.missedWords[missedWord]; + const missedWordCount = missedWords[missedWord]; if (missedWordCount !== undefined) { if (i === 0) { sortableMissedBiwords.push([missedWord, "", missedWordCount]); @@ -88,12 +94,11 @@ export function init( if (slow) { const typedWords = TestWords.words .getText() - .slice(0, TestInput.input.getHistory().length - 1); + .slice(0, getInputHistory().length - 1); + + const burstHistory = getBurstHistory(); - sortableSlowWords = typedWords.map((e, i) => [ - e, - TestInput.burstHistory[i] ?? 0, - ]); + sortableSlowWords = typedWords.map((e, i) => [e, burstHistory[i] ?? 0]); sortableSlowWords.sort((a, b) => { return a[1] - b[1]; }); diff --git a/frontend/src/ts/test/replay.ts b/frontend/src/ts/test/replay.ts index 05a19e649155..f66d82fae4e2 100644 --- a/frontend/src/ts/test/replay.ts +++ b/frontend/src/ts/test/replay.ts @@ -1,5 +1,4 @@ import * as Sound from "../controllers/sound-controller"; -import * as TestInput from "./test-input"; import * as Arrays from "../utils/arrays"; import { qs, qsr } from "../utils/dom"; import { Config } from "../config/store"; @@ -228,7 +227,7 @@ function addReplayEvent(action: ReplayAction, value?: number | string): void { } function updateStatsString(time: number): void { - const wpm = TestInput.wpmHistory[time - 1] ?? 0; + const wpm = 0; const statsString = `${wpm}wpm\t${time}s`; qs("#replayStats")?.setText(statsString); } diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 65e7dda1e126..743d79da11b7 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -26,8 +26,6 @@ import * as Numbers from "@monkeytype/util/numbers"; import * as Arrays from "../utils/arrays"; import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; import * as PbCrown from "./pb-crown"; -import * as TestInput from "./test-input"; -import * as TestStats from "./test-stats"; import * as TestUI from "./test-ui"; import * as TodayTracker from "./today-tracker"; import { configEvent } from "../events/config"; @@ -62,6 +60,7 @@ import { currentQuote } from "./test-words"; import { qs, qsa } from "../utils/dom"; import { getTheme } from "../states/theme"; import { isTestInvalid } from "../states/test"; +import { getAccuracy, getRawHistory } from "./events/stats"; let result: CompletedEvent; let minChartVal: number; @@ -110,12 +109,12 @@ async function updateChartData(): Promise { let labels = []; - for (let i = 1; i <= TestInput.wpmHistory.length; i++) { - if (TestStats.lastSecondNotRound && i === TestInput.wpmHistory.length) { - labels.push(Numbers.roundTo2(result.testDuration).toString()); - } else { - labels.push(i.toString()); - } + for (let i = 1; i <= Math.floor(result.testDuration); i++) { + labels.push(i.toString()); + } + + if (result.testDuration % 1 >= 0.5) { + labels.push(Numbers.roundTo2(result.testDuration).toString()); } const chartData1 = [ @@ -124,11 +123,7 @@ async function updateChartData(): Promise { ), ]; - const chartData2 = [ - ...TestInput.rawHistory.map((a) => - Numbers.roundTo2(typingSpeedUnit.fromWpm(a)), - ), - ]; + const chartData2: number[] = getRawHistory(); const valueWindow = Math.max(...result.chartData.burst) * 0.25; let smoothedBurst = Arrays.smoothWithValueWindow( @@ -141,16 +136,6 @@ async function updateChartData(): Promise { ...smoothedBurst.map((a) => Numbers.roundTo2(typingSpeedUnit.fromWpm(a))), ]; - if ( - Config.mode !== "time" && - TestStats.lastSecondNotRound && - result.testDuration % 1 < 0.5 - ) { - labels.pop(); - chartData1.pop(); - chartData2.pop(); - } - const subcolor = getTheme().sub; if (Config.funbox.length > 0) { @@ -358,6 +343,7 @@ function updateWpmAndAcc(): void { result.acc === 100 ? "100%" : Format.accuracy(result.acc), ); + const acc = getAccuracy(); if (Config.alwaysShowDecimalPlaces) { if (Config.typingSpeedUnit !== "wpm") { qs("#result .stats .wpm .bottom")?.setAttribute( @@ -382,7 +368,7 @@ function updateWpmAndAcc(): void { qs("#result .stats .acc .bottom")?.setAttribute( "aria-label", - `${TestInput.accuracy.correct} correct\n${TestInput.accuracy.incorrect} incorrect`, + `${acc.correct} correct\n${acc.incorrect} incorrect`, ); } else { //not showing decimal places @@ -408,9 +394,7 @@ function updateWpmAndAcc(): void { result.acc === 100 ? "100%" : Format.percentage(result.acc, { showDecimalPlaces: true }) - }\n${TestInput.accuracy.correct} correct\n${ - TestInput.accuracy.incorrect - } incorrect`, + }\n${acc.correct} correct\n${acc.incorrect} incorrect`, ) ?.setAttribute("data-balloon-break", ""); } diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index e74f8ba05b07..79a5aa7ddf87 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -1,154 +1,16 @@ -import { lastElementFromArray } from "../utils/arrays"; -import { mean, roundTo2 } from "@monkeytype/util/numbers"; -import * as TestState from "./test-state"; -import { Config } from "../config/store"; import { getInputElementValue } from "../input/input-element"; -const keysToTrack = new Set([ - "NumpadMultiply", - "NumpadSubtract", - "NumpadAdd", - "NumpadDecimal", - "NumpadEqual", - "NumpadDivide", - "Numpad0", - "Numpad1", - "Numpad2", - "Numpad3", - "Numpad4", - "Numpad5", - "Numpad6", - "Numpad7", - "Numpad8", - "Numpad9", - "Backquote", - "Digit1", - "Digit2", - "Digit3", - "Digit4", - "Digit5", - "Digit6", - "Digit7", - "Digit8", - "Digit9", - "Digit0", - "Minus", - "Equal", - "KeyQ", - "KeyW", - "KeyE", - "KeyR", - "KeyT", - "KeyY", - "KeyU", - "KeyI", - "KeyO", - "KeyP", - "BracketLeft", - "BracketRight", - "Backslash", - "KeyA", - "KeyS", - "KeyD", - "KeyF", - "KeyG", - "KeyH", - "KeyJ", - "KeyK", - "KeyL", - "Semicolon", - "Quote", - "IntlBackslash", - "KeyZ", - "KeyX", - "KeyC", - "KeyV", - "KeyB", - "KeyN", - "KeyM", - "Comma", - "Period", - "Slash", - "Space", - "Enter", - "Tab", - "NoCode", //android (smells) and some keyboards might send no location data - need to use this as a fallback -]); - -type KeypressTimings = { - spacing: { - first: number; - last: number; - array: number[]; - }; - duration: { - array: number[]; - }; -}; - -type Keydata = { - timestamp: number; - index: number; -}; - -type ErrorHistoryObject = { - count: number; - words: number[]; -}; - class Input { current: string; - private history: string[]; - koreanStatus: boolean; + // private history: string[]; constructor() { this.current = ""; - this.history = []; - this.koreanStatus = false; + // this.history = []; } reset(): void { this.current = ""; - this.history = []; - } - - resetHistory(): void { - this.history = []; - } - - setKoreanStatus(val: boolean): void { - this.koreanStatus = val; - } - - getKoreanStatus(): boolean { - return this.koreanStatus; - } - - pushHistory(): void { - this.history.push(this.current); - this.current = ""; - } - - popHistory(): string { - const ret = this.history.pop() ?? ""; - return ret; - } - - get(index: number): string | undefined { - return this.history[index]; - } - - getHistory(): string[]; - getHistory(i: number): string | undefined; - getHistory(i?: number): unknown { - if (i === undefined) { - return this.history; - } else { - return this.history[i]; - } - } - - getHistoryLast(): string | undefined { - return lastElementFromArray(this.history); + // this.history = []; } syncWithInputElement(): void { @@ -156,404 +18,4 @@ class Input { } } -class Corrected { - current: string; - private history: string[]; - constructor() { - this.current = ""; - this.history = []; - } - - reset(): void { - this.history = []; - this.current = ""; - } - - update(char: string, correct: boolean): void { - if (this.current === "") { - this.current += input.current; - } else { - const currCorrectedTestInputLength = this.current.length; - - const charIndex = input.current.length - 1; - - if (charIndex >= currCorrectedTestInputLength) { - this.current += char; - } else if (!correct) { - this.current = - this.current.substring(0, charIndex) + - char + - this.current.substring(charIndex + 1); - } - } - } - - getHistory(i: number): string | undefined { - return this.history[i]; - } - - popHistory(): string { - const popped = this.history.pop() ?? ""; - this.current = popped; - return popped; - } - - pushHistory(): void { - this.history.push(this.current); - this.current = ""; - } -} - -let keyDownData: Record = {}; - export const input = new Input(); -export const corrected = new Corrected(); - -export let keypressCountHistory: number[] = []; -let currentKeypressCount = 0; -export let currentBurstStart = 0; -type MissedWordsType = Record; -// We're using Object.create(null) to make sure that __proto__ won't have any special meaning when it's used to index the missedWords object (so if a user mistypes the word __proto__ it will appear in the practise words test) -export let missedWords: MissedWordsType = Object.create( - null, -) as MissedWordsType; -export let accuracy = { - correct: 0, - incorrect: 0, -}; -export let keypressTimings: KeypressTimings = { - spacing: { - first: -1, - last: -1, - array: [], - }, - duration: { - array: [], - }, -}; -export let keyOverlap = { - total: 0, - lastStartTime: -1, -}; -export let wpmHistory: number[] = []; -export let rawHistory: number[] = []; -export let burstHistory: number[] = []; -export let errorHistory: ErrorHistoryObject[] = []; -let currentErrorHistory: ErrorHistoryObject = { - count: 0, - words: [], -}; - -export let afkHistory: boolean[] = []; -let currentAfk = true; - -export function incrementKeypressCount(): void { - currentKeypressCount++; -} - -export function setCurrentNotAfk(): void { - currentAfk = false; -} - -export function incrementKeypressErrors(): void { - currentErrorHistory.count++; -} - -export function pushKeypressWord(wordIndex: number): void { - currentErrorHistory.words.push(wordIndex); -} - -export function setBurstStart(time: number): void { - currentBurstStart = time; -} - -export function pushKeypressesToHistory(): void { - keypressCountHistory.push(currentKeypressCount); - currentKeypressCount = 0; -} - -export function pushAfkToHistory(): void { - afkHistory.push(currentAfk); - currentAfk = true; -} - -export function pushErrorToHistory(): void { - errorHistory.push(currentErrorHistory); - currentErrorHistory = { - count: 0, - words: [], - }; -} - -export function incrementAccuracy(correctincorrect: boolean): void { - if (correctincorrect) { - accuracy.correct++; - } else { - accuracy.incorrect++; - } -} - -export function forceKeyup(now: number): void { - //using mean here because for words mode, the last keypress ends the test. - //if we then force keyup on that last keypress, it will record a duration of 0 - //skewing the average and standard deviation - - const indexesToRemove = new Set( - Object.values(keyDownData).map((data) => data.index), - ); - - const keypressDurations = keypressTimings.duration.array.filter( - (_, index) => !indexesToRemove.has(index), - ); - let avg: number; - if (keypressDurations.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 - // or we should use a magic number - avg = 80; - } else { - avg = roundTo2(mean(keypressDurations)); - } - - const orderedKeys = Object.entries(keyDownData).sort( - (a, b) => a[1].timestamp - b[1].timestamp, - ); - - for (const [key, { index }] of orderedKeys) { - keypressTimings.duration.array[index] = avg; - - if (key === "NoCode") { - noCodeIndex--; - } - - // oxlint-disable-next-line no-dynamic-delete - delete keyDownData[key]; - - updateOverlap(now); - } -} - -function getEventCode(event: KeyboardEvent): string { - if (event.code === "NumpadEnter" && Config.funbox.includes("58008")) { - return "Space"; - } - - if (event.code.includes("Arrow") && Config.funbox.includes("arrows")) { - return "NoCode"; - } - - if ( - event.code === "" || - event.code === undefined || - event.key === "Unidentified" - ) { - return "NoCode"; - } - - return event.code; -} - -let noCodeIndex = 0; -export function recordKeyupTime(now: number, event: KeyboardEvent): void { - if (event.repeat) { - console.log( - "Keyup not recorded - repeat", - event.key, - event.code, - //ignore for logging - // oxlint-disable-next-line no-deprecated - event.which, - ); - return; - } - - let key = getEventCode(event); - - if (!keysToTrack.has(key)) return; - - if (key === "NoCode") { - noCodeIndex--; - key = `NoCode${noCodeIndex}`; - } - - const keyDownDataForKey = keyDownData[key]; - - if (keyDownDataForKey === undefined) return; - - const diff = Math.abs(keyDownDataForKey.timestamp - now); - keypressTimings.duration.array[keyDownDataForKey.index] = diff; - - console.debug("Keyup recorded", key, diff); - // oxlint-disable-next-line no-dynamic-delete - delete keyDownData[key]; - - updateOverlap(now); -} - -export function recordKeydownTime(now: number, event: KeyboardEvent): void { - if (event.repeat) { - console.log( - "Keydown not recorded - repeat", - event.key, - event.code, - //ignore for logging - // oxlint-disable-next-line no-deprecated - event.which, - ); - return; - } - - let key = getEventCode(event); - - if (!keysToTrack.has(key)) { - console.debug("Keydown not recorded - not tracked", key); - return; - } - - if (keyDownData[key] !== undefined) { - console.debug("Key already down", key); - return; - } - - if (key === "NoCode") { - key = `NoCode${noCodeIndex}`; - noCodeIndex++; - } - - keyDownData[key] = { - timestamp: now, - index: keypressTimings.duration.array.length, - }; - keypressTimings.duration.array.push(0); - - updateOverlap(keyDownData[key]?.timestamp as number); - - if (keypressTimings.spacing.last !== -1) { - const diff = Math.abs(now - keypressTimings.spacing.last); - keypressTimings.spacing.array.push(roundTo2(diff)); - console.debug("Keydown recorded", key, diff); - } - keypressTimings.spacing.last = now; - if (keypressTimings.spacing.first === -1) { - keypressTimings.spacing.first = now; - console.debug("First keydown recorded", key, now); - } -} - -function updateOverlap(now: number): void { - const keys = Object.keys(keyDownData); - if (keys.length > 1) { - if (keyOverlap.lastStartTime === -1) { - keyOverlap.lastStartTime = now; - } - } else { - if (keyOverlap.lastStartTime !== -1) { - keyOverlap.total += now - keyOverlap.lastStartTime; - keyOverlap.lastStartTime = -1; - } - } -} - -export function carryoverFirstKeypress(): void { - // Because keydown triggers before input, we need to grab the first keypress data here and carry it over - - // Take the key with the largest index - const lastKey = Object.keys(keyDownData).reduce((a, b) => { - const aIndex = keyDownData[a]?.index; - const bIndex = keyDownData[b]?.index; - if (aIndex === undefined) return b; - if (bIndex === undefined) return a; - return aIndex > bIndex ? a : b; - }, ""); - - // Get the data - const lastKeyData = keyDownData[lastKey]; - - // Carry over - if (lastKeyData !== undefined) { - keypressTimings = { - spacing: { - first: lastKeyData.timestamp, - last: lastKeyData.timestamp, - array: [], - }, - duration: { - array: [0], - }, - }; - keyDownData[lastKey] = { - timestamp: lastKeyData.timestamp, - // Make sure to set it to the first index - index: 0, - }; - } -} - -function resetKeypressTimings(): void { - keypressTimings = { - spacing: { - first: -1, - last: -1, - array: [], - }, - duration: { - array: [], - }, - }; - keyOverlap = { - total: 0, - lastStartTime: -1, - }; - keyDownData = {}; - noCodeIndex = 0; - - console.debug("Keypress timings reset"); -} - -export function pushMissedWord(word: string): void { - if (!Object.keys(missedWords).includes(word)) { - missedWords[word] = 1; - } else { - (missedWords[word] as number) += 1; - } -} - -export function pushToWpmHistory(wpm: number): void { - wpmHistory.push(wpm); -} - -export function pushToRawHistory(raw: number): void { - rawHistory.push(raw); -} - -export function pushBurstToHistory(speed: number): void { - if (burstHistory[TestState.activeWordIndex] === undefined) { - burstHistory.push(speed); - } else { - //repeated word - override - burstHistory[TestState.activeWordIndex] = speed; - } -} - -export function restart(): void { - wpmHistory = []; - rawHistory = []; - burstHistory = []; - keypressCountHistory = []; - currentKeypressCount = 0; - afkHistory = []; - currentAfk = true; - errorHistory = []; - currentErrorHistory = { - count: 0, - words: [], - }; - currentBurstStart = 0; - missedWords = Object.create(null) as MissedWordsType; - accuracy = { - correct: 0, - incorrect: 0, - }; - - resetKeypressTimings(); -} diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 6934fff82a95..8bfbb070f4bb 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -12,7 +12,6 @@ import { } from "../states/notifications"; import * as CustomText from "./custom-text"; import * as CustomTextState from "../legacy-states/custom-text-name"; -import * as TestStats from "./test-stats"; import * as PractiseWords from "./practise-words"; import * as ShiftTracker from "./shift-tracker"; import * as AltTracker from "./alt-tracker"; @@ -85,7 +84,11 @@ import { qs } from "../utils/dom"; import { setAccountButtonSpinner } from "../states/header"; import { Config } from "../config/store"; import { setQuoteLengthAll, toggleFunbox, setConfig } from "../config/setters"; -import { resetTestEvents, cleanupData } from "./events/data"; +import { + resetTestEvents, + cleanupData, + logEventsDataToTheConsoleTable, +} from "./events/data"; import { getKeypressDurations, getChars, @@ -100,7 +103,10 @@ import { getWpmHistory, getAfkDuration, forceReleaseAllKeys, - getKeypressesPerSecond, + getCurrentAccuracy, + getCurrentTestDurationMs, + getDateBasedTestDurationMs, + getInputHistory, } from "./events/stats"; import { calculateWpm } from "../utils/numbers"; @@ -159,7 +165,7 @@ export function setNotSignedInUidAndHash(uid: string): void { notSignedInLastResult.hash = objectHash(notSignedInLastResult); } -export function startTest(now: number): boolean { +export function startTest(_now: number): boolean { if (PageTransition.get()) { return false; } @@ -173,7 +179,6 @@ export function startTest(now: number): boolean { TestState.setActive(true); Replay.startReplayRecording(); Replay.replayGetWordsList(TestWords.words.list); - TestInput.carryoverFirstKeypress(); Time.set(0); TestTimer.clear(); @@ -190,7 +195,6 @@ export function startTest(now: number): boolean { } } catch (e) {} //use a recursive self-adjusting timer to avoid time drift - TestStats.setStart(now); void TestTimer.start(); TestUI.onTestStart(); return true; @@ -271,14 +275,11 @@ export function restart(options = {} as RestartOptions): void { } if (Config.resultSaving) { - TestInput.pushKeypressesToHistory(); - TestInput.pushErrorToHistory(); - TestInput.pushAfkToHistory(); - const testSeconds = TestStats.calculateTestSeconds(performance.now()); - const afkseconds = TestStats.calculateAfkSeconds(testSeconds); + const testSeconds = getCurrentTestDurationMs(performance.now()) / 1000; + const afkseconds = getAfkDuration(); let tt = Numbers.roundTo2(testSeconds - afkseconds); if (tt < 0) tt = 0; - const acc = Numbers.roundTo2(TestStats.calculateAccuracy()); + const acc = Numbers.roundTo2(getCurrentAccuracy()); pushIncompleteTest({ acc, seconds: tt }); } } @@ -322,9 +323,6 @@ export function restart(options = {} as RestartOptions): void { resetTestEvents(); TestTimer.clear(); setIsTestInvalid(false); - TestStats.restart(); - TestInput.restart(); - TestInput.corrected.reset(); ShiftTracker.reset(); AltTracker.reset(); Caret.hide(); @@ -334,7 +332,7 @@ export function restart(options = {} as RestartOptions): void { TestState.setBailedOut(false); Caret.resetPosition(); PaceCaret.reset(); - TestInput.input.setKoreanStatus(false); + TestState.setKoreanStatus(false); clearQuoteStats(); CompositionState.setComposing(false); CompositionState.setData(""); @@ -434,7 +432,7 @@ async function init(): Promise { Replay.stopReplayRecording(); TestWords.words.reset(); TestState.setActiveWordIndex(0); - TestInput.input.resetHistory(); + // TestInput.input.resetHistory(); TestInput.input.current = ""; showLoaderBar(); @@ -595,7 +593,7 @@ async function init(): Promise { /[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/g, ) ) { - TestInput.input.setKoreanStatus(true); + TestState.setKoreanStatus(true); } for (let i = 0; i < generatedWords.length; i++) { @@ -656,7 +654,7 @@ export function areAllTestWordsGenerated(): boolean { //add word during the test export async function addWord(): Promise { if (Config.mode === "zen") { - TestUI.appendEmptyWordElement(); + TestUI.appendEmptyWordElement(TestState.activeWordIndex + 1); return; } @@ -670,7 +668,7 @@ export async function addWord(): Promise { const toPushCount = funboxToPush?.split(":")[1]; if (toPushCount !== undefined) bound = +toPushCount - 1; - if (TestWords.words.length - TestInput.input.getHistory().length > bound) { + if (TestWords.words.length - TestState.activeWordIndex > bound) { console.debug("Not adding word, enough words already"); return; } @@ -768,385 +766,7 @@ export async function retrySavingResult(): Promise { await saveResult(completedEvent, true); } -function buildCompletedEvent( - stats: TestStats.Stats, - rawPerSecond: number[], -): Omit { - //build completed event object - let stfk = Numbers.roundTo2( - TestInput.keypressTimings.spacing.first - TestStats.start, - ); - if (stfk < 0 || Config.mode === "zen") { - stfk = 0; - } - - let lkte = Numbers.roundTo2( - TestStats.end - TestInput.keypressTimings.spacing.last, - ); - if (lkte < 0 || Config.mode === "zen") { - lkte = 0; - } - - //consistency - const stddev = Numbers.stdDev(rawPerSecond); - const avg = Numbers.mean(rawPerSecond); - let consistency = Numbers.roundTo2(Numbers.kogasa(stddev / avg)); - let keyConsistencyArray = TestInput.keypressTimings.spacing.array.slice(); - if (keyConsistencyArray.length > 0) { - keyConsistencyArray = keyConsistencyArray.slice( - 0, - keyConsistencyArray.length - 1, - ); - } - let keyConsistency = Numbers.roundTo2( - Numbers.kogasa( - Numbers.stdDev(keyConsistencyArray) / Numbers.mean(keyConsistencyArray), - ), - ); - if (!consistency || isNaN(consistency)) { - consistency = 0; - } - if (!keyConsistency || isNaN(keyConsistency)) { - keyConsistency = 0; - } - - const chartErr = []; - for (const error of TestInput.errorHistory) { - chartErr.push(error.count ?? 0); - } - - const chartData = { - wpm: TestInput.wpmHistory, - burst: rawPerSecond, - err: chartErr, - }; - - //wpm consistency - const stddev3 = Numbers.stdDev(chartData.wpm ?? []); - const avg3 = Numbers.mean(chartData.wpm ?? []); - const wpmCons = Numbers.roundTo2(Numbers.kogasa(stddev3 / avg3)); - const wpmConsistency = isNaN(wpmCons) ? 0 : wpmCons; - - let customText: CompletedEventCustomText | undefined = undefined; - if (Config.mode === "custom") { - const temp = CustomText.getData(); - customText = { - textLen: temp.text.length, - mode: temp.mode, - pipeDelimiter: temp.pipeDelimiter, - limit: temp.limit, - }; - } - - //tags - const activeTagsIds: string[] = __nonReactive - .getActiveTags() - .map((tag) => tag._id); - - const duration = parseFloat(stats.time.toString()); - const afkDuration = TestStats.calculateAfkSeconds(duration); - let language = Config.language; - if (Config.mode === "quote") { - language = Strings.removeLanguageSize(Config.language); - } - - const quoteLength = TestWords.currentQuote?.group ?? -1; - - const completedEvent: Omit = { - wpm: stats.wpm, - rawWpm: stats.wpmRaw, - charStats: [ - stats.correctChars + stats.correctSpaces, - stats.incorrectChars, - stats.extraChars, - stats.missedChars, - ], - charTotal: stats.allChars, - acc: stats.acc, - mode: Config.mode, - mode2: Misc.getMode2(Config, TestWords.currentQuote), - quoteLength: quoteLength, - punctuation: Config.punctuation, - numbers: Config.numbers, - lazyMode: Config.lazyMode, - timestamp: Date.now(), - language: language, - restartCount: getRestartCount(), - incompleteTests: getIncompleteTests(), - incompleteTestSeconds: - getIncompleteSeconds() < 0 ? 0 : Numbers.roundTo2(getIncompleteSeconds()), - difficulty: Config.difficulty, - blindMode: Config.blindMode, - tags: activeTagsIds, - keySpacing: TestInput.keypressTimings.spacing.array, - keyDuration: TestInput.keypressTimings.duration.array, - keyOverlap: Numbers.roundTo2(TestInput.keyOverlap.total), - lastKeyToEnd: lkte, - startToFirstKey: stfk, - consistency: consistency, - wpmConsistency: wpmConsistency, - keyConsistency: keyConsistency, - funbox: Config.funbox, - bailedOut: TestState.bailedOut, - chartData: chartData, - customText: customText, - testDuration: duration, - afkDuration: afkDuration, - stopOnLetter: Config.stopOnError === "letter", - }; - - if (completedEvent.mode !== "custom") delete completedEvent.customText; - if (completedEvent.mode !== "quote") delete completedEvent.quoteLength; - - return completedEvent; -} - -function compareCompletedEvents( - ce: Omit, -): void { - const start = performance.now(); - const ce2 = buildCompletedEvent2(); - const end = performance.now(); - - console.debug( - `Built completed event 2 in ${Numbers.roundTo2(end - start)} ms`, - ); - - //compare ce and ce2, log differences - const notMatching: string[] = []; - const mismatchedKeys: string[] = []; - const ceKeys = Object.keys(ce) as (keyof typeof ce)[]; - for (const key of ceKeys) { - let val1 = ce[key]; - let val2 = ce2[key]; - - if (key === "keyDuration" || key === "keySpacing") { - const a = (val1 as number[]).map((v) => Numbers.roundTo2(v)); - const b = (val2 as number[]).map((v) => Numbers.roundTo2(v)); - const total = Math.max(a.length, b.length); - let mismatchCount = 0; - if (a.length !== b.length) { - mismatchCount = total; - console.error( - `Completed event length mismatch on key ${key}: ${a.length} vs ${b.length}`, - ); - } else { - for (let i = 0; i < total; i++) { - if (a[i] !== b[i]) mismatchCount++; - } - } - if (mismatchCount === 0) { - console.debug(`Completed event match on key ${key}:`, a); - } else { - notMatching.push(`${key} (${mismatchCount}/${total} elements differ)`); - mismatchedKeys.push(key); - console.error( - `Completed event mismatch on key ${key}: ${mismatchCount}/${total} elements differ`, - a, - b, - ); - } - continue; - } - - if (key === "charStats") { - const a = val1 as number[]; - const b = val2 as number[]; - const labels = ["correct", "incorrect", "extra", "missed"]; - const diffs: string[] = []; - for (let i = 0; i < Math.max(a.length, b.length); i++) { - if (a[i] !== b[i]) { - const label = labels[i] ?? `[${i}]`; - diffs.push(`${label}: ${a[i]} vs ${b[i]}`); - } - } - if (diffs.length === 0) { - console.debug(`Completed event match on key charStats:`, a); - } else { - notMatching.push(`charStats (${diffs.join(", ")})`); - mismatchedKeys.push("charStats"); - console.error(`Completed event mismatch on key charStats:`, a, b); - } - continue; - } - - if (key === "keyOverlap") { - val1 = Numbers.roundTo2(val1 as number); - val2 = Numbers.roundTo2(val2 as number); - } - - if (key === "timestamp") { - continue; - } - - if (key === "consistency") { - continue; - } - - // if (key === "chartData") { - // val1 = { - // //@ts-expect-error temp - // // eslint-disable-next-line - // wpm: (val1 as CompletedEvent["chartData"]).wpm.map((v) => - // // eslint-disable-next-line - // Math.round(v), - // ), - // //@ts-expect-error temp - // // eslint-disable-next-line - // burst: (val1 as CompletedEvent["chartData"]).burst, - // //@ts-expect-error temp - // // eslint-disable-next-line - // err: (val1 as CompletedEvent["chartData"]).err, - // }; - // val2 = { - // //@ts-expect-error temp - // // eslint-disable-next-line - // wpm: (val2 as CompletedEvent["chartData"]).wpm.map((v) => - // // eslint-disable-next-line - // Math.round(v), - // ), - // //@ts-expect-error temp - // // eslint-disable-next-line - // burst: (val2 as CompletedEvent["chartData"]).burst, - // //@ts-expect-error temp - // // eslint-disable-next-line - // err: (val2 as CompletedEvent["chartData"]).err, - // }; - // } - - if (key === "chartData") { - const v1 = val1 as CompletedEvent["chartData"]; - const v2 = val2 as CompletedEvent["chartData"]; - - if (v1 === "toolong" || v2 === "toolong") { - if (v1 === v2) { - console.debug( - `Completed event match on key chartData: both are "toolong"`, - ); - } else { - notMatching.push("chartData (one is 'toolong' and the other is not)"); - mismatchedKeys.push("chartData"); - console.error( - `Completed event mismatch on key chartData: one is "toolong" and the other is not`, - v1, - v2, - ); - } - continue; - } - - for (const field of ["wpm", "err"] as const) { - const a = v1[field]; - const b = v2[field]; - const withinTolerance = - a.length === b.length && - a.every((val, i) => { - if (val === 0 && b[i] === 0) return true; - const ref = Math.max(Math.abs(val), Math.abs(b[i] ?? 0)); - return Math.abs(val - (b[i] ?? 0)) / ref <= 0.05; - }); - if (withinTolerance) { - console.debug(`Completed event match on key chartData.${field}:`, a); - } else { - notMatching.push(`chartData.${field} (values differ)`); - mismatchedKeys.push(`chartData.${field}`); - console.error( - `Completed event mismatch on key chartData.${field}:`, - a, - b, - ); - } - } - - { - const a = TestInput.keypressCountHistory; - const b = getKeypressesPerSecond(); - if (a.length === b.length && a.every((val, i) => val === b[i])) { - console.debug( - `Completed event match on key keypressCountHistory:`, - a, - ); - } else { - notMatching.push(`keypressCountHistory (values differ)`); - mismatchedKeys.push("keypressCountHistory"); - console.error( - `Completed event mismatch on key keypressCountHistory:`, - a, - b, - ); - } - } - } else if (key === "wpmConsistency" || key === "keyConsistency") { - const a = val1 as number; - const b = val2 as number; - const ref = Math.max( - Numbers.roundTo2(Math.abs(a)), - Numbers.roundTo2(Math.abs(b)), - ); - const within = (a === 0 && b === 0) || Math.abs(a - b) / ref <= 0.05; - if (within) { - console.debug(`Completed event match on key ${key}:`, a); - } else { - const diff = Numbers.roundTo2(Math.abs(a - b)); - const dir = a > b ? "ce1 larger" : "ce2 larger"; - notMatching.push(`${key} (off by ${diff}, ${dir})`); - mismatchedKeys.push(key); - console.error(`Completed event mismatch on key ${key}:`, a, b); - } - } else if (typeof val1 === "number" && typeof val2 === "number") { - const a = Numbers.roundTo2(val1); - const b = Numbers.roundTo2(val2); - if (a !== b) { - const diff = Numbers.roundTo2(Math.abs(a - b)); - const dir = a > b ? "ce1 larger" : "ce2 larger"; - notMatching.push(`${key} (off by ${diff}, ${dir})`); - mismatchedKeys.push(key); - console.error(`Completed event mismatch on key ${key}:`, a, b); - } else { - console.debug(`Completed event match on key ${key}:`, a); - } - } else if (JSON.stringify(val1) !== JSON.stringify(val2)) { - notMatching.push(`${key} (values differ)`); - mismatchedKeys.push(key); - console.error(`Completed event mismatch on key ${key}:`, val1, val2); - } else { - console.debug(`Completed event match on key ${key}:`, val1); - } - } - - if (notMatching.length === 0) { - // showSuccessNotification("Completed events match", { important: true }); - } else { - // showErrorNotification( - // `Completed event mismatch: ${notMatching.join(", ")}`, - // { important: true }, - // ); - mismatchedKeys.sort(); - const groupKey = mismatchedKeys.join(","); - Ape.results - .reportCompletedEventMismatch({ - body: { - notMatching, - mismatchedKeys, - groupKey, - language: ce.language, - mode: ce.mode, - mode2: ce.mode2, - difficulty: ce.difficulty, - duration: ce.testDuration, - // ce: ce as Record, - // ce2: ce2 as Record, - }, - }) - .catch(() => { - // - }); - } - - console.debug("Completed event object2", ce2); -} - -function buildCompletedEvent2(): Omit { +function buildCompletedEvent(): Omit { const chars = getChars(); //tags @@ -1262,7 +882,6 @@ export async function finish(difficultyFailed = false): Promise { TestState.setResultCalculating(true); const now = performance.now(); TestTimer.clear(true, now); - TestStats.setEnd(now); // fade out the test and show loading // because the css animation has a delay, @@ -1284,25 +903,18 @@ export async function finish(difficultyFailed = false): Promise { // in case the tests ends with a keypress (not a word submission) // we need to push the current input to history if (TestInput.input.current.length !== 0) { - TestInput.input.pushHistory(); - TestInput.corrected.pushHistory(); - Replay.replayGetWordsList(TestInput.input.getHistory()); + // TestInput.input.pushHistory(); + Replay.replayGetWordsList(getInputHistory()); } // in zen mode, ensure the replay words list reflects the typed input history // even if the current input was empty at finish (e.g., after submitting a word). if (Config.mode === "zen") { - Replay.replayGetWordsList(TestInput.input.getHistory()); + Replay.replayGetWordsList(getInputHistory()); } - TestInput.forceKeyup(now); //this ensures that the last keypress(es) are registered forceReleaseAllKeys(); - const endAfkSeconds = (now - TestInput.keypressTimings.spacing.last) / 1000; - if ((Config.mode === "zen" || TestState.bailedOut) && endAfkSeconds < 7) { - TestStats.setEnd(TestInput.keypressTimings.spacing.last); - } - setResultVisible(true); TestState.setResultVisible(true); TestState.setActive(false); @@ -1310,67 +922,11 @@ export async function finish(difficultyFailed = false): Promise { cleanupData(); - // logEventsDataToTheConsoleTable(); - - //need one more calculation for the last word if test auto ended - if (TestInput.burstHistory.length !== TestInput.input.getHistory()?.length) { - const burst = TestStats.calculateBurst(now); - TestInput.pushBurstToHistory(burst); - } + logEventsDataToTheConsoleTable(); + console.log(getInputHistory()); - //remove afk from zen - if (Config.mode === "zen" || TestState.bailedOut) { - TestStats.removeAfkData(); - } - - // stats - const stats = TestStats.calculateFinalStats(); - if ( - stats.time % 1 !== 0 && - !( - Config.mode === "time" || - (Config.mode === "custom" && CustomText.getLimitMode() === "time") - ) - ) { - TestStats.setLastSecondNotRound(); - } - - PaceCaret.setLastTestWpm(stats.wpm); - - // if the last second was not rounded, add another data point to the history - if ( - TestStats.lastSecondNotRound && - !difficultyFailed && - Math.round(stats.time % 1) >= 0.5 - ) { - const wpmAndRaw = TestStats.calculateWpmAndRaw(); - TestInput.pushToWpmHistory(wpmAndRaw.wpm); - TestInput.pushToRawHistory(wpmAndRaw.raw); - TestInput.pushKeypressesToHistory(); - TestInput.pushErrorToHistory(); - TestInput.pushAfkToHistory(); - } - - const rawPerSecond = TestInput.keypressCountHistory.map((count) => - Math.round((count / 5) * 60), - ); - - //adjust last second if last second is not round - // if (TestStats.lastSecondNotRound && stats.time % 1 >= 0.1) { - if ( - Config.mode !== "time" && - TestStats.lastSecondNotRound && - stats.time % 1 >= 0.5 - ) { - const timescale = 1 / (stats.time % 1); - - //multiply last element of rawBefore by scale, and round it - rawPerSecond[rawPerSecond.length - 1] = Math.round( - (rawPerSecond[rawPerSecond.length - 1] as number) * timescale, - ); - } - - const ce = buildCompletedEvent(stats, rawPerSecond); + const ce = buildCompletedEvent(); + PaceCaret.setLastTestWpm(ce.wpm); console.debug("Completed event object", ce); @@ -1406,16 +962,15 @@ export async function finish(difficultyFailed = false): Promise { ///////// completed event ready //afk check - const kps = TestInput.afkHistory.slice(-5); - let afkDetected = kps.length > 0 && kps.every((afk) => afk); + let afkDetected = false; if (TestState.bailedOut) afkDetected = false; const mode2Number = parseInt(completedEvent.mode2); let tooShort = false; //fail checks - const dateDur = (TestStats.end3 - TestStats.start3) / 1000; + const dateDur = getDateBasedTestDurationMs() / 1000; if ( Config.mode === "time" && !TestState.bailedOut && @@ -1499,15 +1054,6 @@ export async function finish(difficultyFailed = false): Promise { // test is valid - if ( - getAuthenticatedUser() !== null && - !dontSave && - !difficultyFailed && - Config.resultSaving - ) { - compareCompletedEvents(ce); - } - if (TestState.isRepeated || difficultyFailed) { if (Config.resultSaving) { const testSeconds = completedEvent.testDuration; @@ -1525,11 +1071,11 @@ export async function finish(difficultyFailed = false): Promise { // Let's update the custom text progress if ( TestState.bailedOut || - TestInput.input.getHistory().length < TestWords.words.length + getInputHistory().length < TestWords.words.length ) { // They bailed out - const history = TestInput.input.getHistory(); + const history = getInputHistory(); let historyLength = history?.length; const wordIndex = historyLength - 1; @@ -1758,11 +1304,6 @@ async function saveResult( export function fail(reason: string): void { failReason = reason; - // input.pushHistory(); - // corrected.pushHistory(); - TestInput.pushKeypressesToHistory(); - TestInput.pushErrorToHistory(); - TestInput.pushAfkToHistory(); void finish(true); } diff --git a/frontend/src/ts/test/test-state.ts b/frontend/src/ts/test/test-state.ts index 82c27c083657..831c101d08ec 100644 --- a/frontend/src/ts/test/test-state.ts +++ b/frontend/src/ts/test/test-state.ts @@ -13,6 +13,11 @@ export let isDirectionReversed = false; export let testRestarting = false; export let resultVisible = false; export let resultCalculating = false; +export let koreanStatus = false; + +export function setKoreanStatus(val: boolean): void { + koreanStatus = val; +} export function setRepeated(tf: boolean): void { isRepeated = tf; diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts deleted file mode 100644 index 9d7701fc45f7..000000000000 --- a/frontend/src/ts/test/test-stats.ts +++ /dev/null @@ -1,403 +0,0 @@ -import Hangul from "hangul-js"; -import { Config } from "../config/store"; -import * as Strings from "../utils/strings"; -import * as TestInput from "./test-input"; -import * as TestWords from "./test-words"; -import * as TestState from "./test-state"; -import * as Numbers from "@monkeytype/util/numbers"; -import { isFunboxActiveWithProperty } from "./funbox/list"; -import * as CustomText from "./custom-text"; -import { getLastResult } from "../states/test"; - -type CharCount = { - spaces: number; - correctWordChars: number; - allCorrectChars: number; - incorrectChars: number; - extraChars: number; - missedChars: number; - correctSpaces: number; -}; - -export type Stats = { - wpm: number; - wpmRaw: number; - acc: number; - correctChars: number; - incorrectChars: number; - missedChars: number; - extraChars: number; - allChars: number; - time: number; - spaces: number; - correctSpaces: number; -}; - -export let start: number, end: number; -export let start2: number, end2: number; -export let start3: number, end3: number; -export let lastSecondNotRound = false; - -export function getStats(): unknown { - const ret = { - lastResult: getLastResult(), - start, - end, - start3, - end3, - afkHistory: TestInput.afkHistory, - errorHistory: TestInput.errorHistory, - wpmHistory: TestInput.wpmHistory, - rawHistory: TestInput.rawHistory, - burstHistory: TestInput.burstHistory, - keypressCountHistory: TestInput.keypressCountHistory, - currentBurstStart: TestInput.currentBurstStart, - lastSecondNotRound, - missedWords: TestInput.missedWords, - accuracy: TestInput.accuracy, - keypressTimings: TestInput.keypressTimings, - keyOverlap: TestInput.keyOverlap, - wordsHistory: TestWords.words.list.slice( - 0, - TestInput.input.getHistory().length, - ), - inputHistory: TestInput.input.getHistory(), - }; - - try { - // @ts-expect-error --- - ret.keypressTimings.spacing.average = - TestInput.keypressTimings.spacing.array.reduce( - (previous, current) => (current += previous), - ) / TestInput.keypressTimings.spacing.array.length; - - // @ts-expect-error --- - ret.keypressTimings.spacing.sd = Numbers.stdDev( - TestInput.keypressTimings.spacing.array, - ); - } catch (e) { - // - } - try { - // @ts-expect-error --- - ret.keypressTimings.duration.average = - TestInput.keypressTimings.duration.array.reduce( - (previous, current) => (current += previous), - ) / TestInput.keypressTimings.duration.array.length; - - // @ts-expect-error --- - ret.keypressTimings.duration.sd = Numbers.stdDev( - TestInput.keypressTimings.duration.array, - ); - } catch (e) { - // - } - - return ret; -} - -export function restart(): void { - start = 0; - end = 0; - start2 = 0; - end2 = 0; - start3 = 0; - end3 = 0; - lastSecondNotRound = false; -} - -export function calculateTestSeconds(now?: number): number { - let duration = (end - start) / 1000; - - if (now !== undefined) { - duration = (now - start) / 1000; - } - - return duration; -} - -export function calculateWpmAndRaw( - withDecimalPoints?: true, - final = false, - testSecondsOverride?: number, -): { - wpm: number; - raw: number; -} { - const testSeconds = - testSecondsOverride ?? - calculateTestSeconds(TestState.isActive ? performance.now() : end); - - const chars = countChars(final); - const wpm = Numbers.roundTo2( - ((chars.correctWordChars + chars.correctSpaces) * (60 / testSeconds)) / 5, - ); - const raw = Numbers.roundTo2( - ((chars.allCorrectChars + - chars.spaces + - chars.incorrectChars + - chars.extraChars) * - (60 / testSeconds)) / - 5, - ); - return { - wpm: withDecimalPoints ? wpm : Math.round(wpm), - raw: withDecimalPoints ? raw : Math.round(raw), - }; -} - -export function setEnd(e: number): void { - end = e; - end2 = Date.now(); - end3 = new Date().getTime(); -} - -export function setStart(s: number): void { - start = s; - start2 = Date.now(); - start3 = new Date().getTime(); -} - -export function calculateAfkSeconds(testSeconds: number): number { - let extraAfk = 0; - if (testSeconds !== undefined) { - extraAfk = Math.round(testSeconds) - TestInput.keypressCountHistory.length; - if (extraAfk < 0) extraAfk = 0; - // console.log("-- extra afk debug"); - // console.log("should be " + Math.ceil(testSeconds)); - // console.log(keypressPerSecond.length); - // console.log( - // `gonna add extra ${extraAfk} seconds of afk because of no keypress data` - // ); - } - const ret = TestInput.afkHistory.filter((afk) => afk).length; - return ret + extraAfk; -} - -export function setLastSecondNotRound(): void { - lastSecondNotRound = true; -} - -export function calculateBurst(endTime: number = performance.now()): number { - const containsKorean = TestInput.input.getKoreanStatus(); - const timeToWrite = (endTime - TestInput.currentBurstStart) / 1000; - if (timeToWrite <= 0) return 0; - let wordLength: number; - wordLength = !containsKorean - ? TestInput.input.current.length - : Hangul.disassemble(TestInput.input.current).length; - if (wordLength === 0) { - wordLength = !containsKorean - ? (TestInput.input.getHistoryLast()?.length ?? 0) - : (Hangul.disassemble(TestInput.input.getHistoryLast() as string) - ?.length ?? 0); - } - if (wordLength === 0) return 0; - const speed = Numbers.roundTo2((wordLength * (60 / timeToWrite)) / 5); - return Math.round(speed); -} - -export function calculateAccuracy(): number { - const acc = - (TestInput.accuracy.correct / - (TestInput.accuracy.correct + TestInput.accuracy.incorrect)) * - 100; - return isNaN(acc) ? 100 : acc; -} - -export function removeAfkData(): void { - const testSeconds = calculateTestSeconds(); - TestInput.keypressCountHistory.splice(testSeconds); - TestInput.wpmHistory.splice(testSeconds); - TestInput.rawHistory.splice(testSeconds); -} - -function getInputWords(): string[] { - const containsKorean = TestInput.input.getKoreanStatus(); - - let inputWords = [...TestInput.input.getHistory()]; - - if (TestState.isActive) { - inputWords.push(TestInput.input.current); - } - - if (containsKorean) { - inputWords = inputWords.map((w) => Hangul.disassemble(w).join("")); - } - - return inputWords; -} - -function getTargetWords(): string[] { - const containsKorean = TestInput.input.getKoreanStatus(); - - let targetWords = [ - ...(Config.mode === "zen" - ? TestInput.input.getHistory() - : TestWords.words.list), - ]; - - if (TestState.isActive) { - targetWords.push( - Config.mode === "zen" - ? TestInput.input.current - : TestWords.words.getCurrentText(), - ); - } - - if (containsKorean) { - targetWords = targetWords.map((w) => Hangul.disassemble(w).join("")); - } - - return targetWords; -} - -function countChars(final = false): CharCount { - let correctWordChars = 0; - let correctChars = 0; - let incorrectChars = 0; - let extraChars = 0; - let missedChars = 0; - let spaces = 0; - let correctspaces = 0; - - const inputWords = getInputWords(); - const targetWords = getTargetWords(); - - for (let i = 0; i < inputWords.length; i++) { - const inputWord = inputWords[i] as string; - const targetWord = targetWords[i] as string; - - if (inputWord === targetWord) { - //the word is correct - correctWordChars += targetWord.length; - correctChars += targetWord.length; - if ( - i < inputWords.length - 1 && - Strings.getLastChar(inputWord) !== "\n" - ) { - correctspaces++; - } - } else if (inputWord.length >= targetWord.length) { - //too many chars - for (let c = 0; c < inputWord.length; c++) { - if (c < targetWord.length) { - //on char that still has a word list pair - if (inputWord[c] === targetWord[c]) { - correctChars++; - } else { - incorrectChars++; - } - } else { - //on char that is extra - extraChars++; - } - } - } else { - //not enough chars - const toAdd = { - correct: 0, - incorrect: 0, - missed: 0, - }; - for (let c = 0; c < targetWord.length; c++) { - if (c < inputWord.length) { - //on char that still has a word list pair - if (inputWord[c] === targetWord[c]) { - toAdd.correct++; - } else { - toAdd.incorrect++; - } - } else { - //on char that is extra - toAdd.missed++; - } - } - correctChars += toAdd.correct; - incorrectChars += toAdd.incorrect; - - const isTimedTest = - Config.mode === "time" || - (Config.mode === "custom" && CustomText.getLimit().mode === "time"); - const shouldCountPartialLastWord = !final || (final && isTimedTest); - - if (i === inputWords.length - 1 && shouldCountPartialLastWord) { - //last word - check if it was all correct - add to correct word chars - if (toAdd.incorrect === 0) correctWordChars += toAdd.correct; - } else { - missedChars += toAdd.missed; - } - } - if (i < inputWords.length - 1) { - spaces++; - } - } - if (isFunboxActiveWithProperty("nospace")) { - spaces = 0; - correctspaces = 0; - } - return { - spaces: spaces, - correctWordChars: correctWordChars, - allCorrectChars: correctChars, - incorrectChars: - Config.mode === "zen" ? TestInput.accuracy.incorrect : incorrectChars, - extraChars: extraChars, - missedChars: missedChars, - correctSpaces: correctspaces, - }; -} - -export function calculateFinalStats(): Stats { - console.debug("Calculating result stats"); - let testSeconds = calculateTestSeconds(); - console.debug( - "Test seconds", - testSeconds, - " (date based) ", - (end2 - start2) / 1000, - " (performance.now based)", - (end3 - start3) / 1000, - " (new Date based)", - ); - console.debug( - "Test seconds", - Numbers.roundTo1(testSeconds), - " (date based) ", - Numbers.roundTo1((end2 - start2) / 1000), - " (performance.now based)", - Numbers.roundTo1((end3 - start3) / 1000), - " (new Date based)", - ); - if (Config.mode !== "custom") { - testSeconds = Numbers.roundTo2(testSeconds); - console.debug( - "Mode is not custom - rounding to 2. New time: ", - testSeconds, - ); - } - - //todo: this counts chars twice - once here and once in calculateWpmAndRaw - const chars = countChars(true); - const { wpm, raw } = calculateWpmAndRaw(true, true, testSeconds); - const acc = Numbers.roundTo2(calculateAccuracy()); - const ret = { - wpm: isNaN(wpm) ? 0 : wpm, - wpmRaw: isNaN(raw) ? 0 : raw, - acc: acc, - correctChars: chars.correctWordChars, - incorrectChars: chars.incorrectChars + chars.spaces - chars.correctSpaces, - missedChars: chars.missedChars, - extraChars: chars.extraChars, - allChars: - chars.allCorrectChars + - chars.spaces + - chars.incorrectChars + - chars.extraChars, - time: Numbers.roundTo2(testSeconds), - spaces: chars.spaces, - correctSpaces: chars.correctSpaces, - }; - console.debug("Result stats", ret); - return ret; -} diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index b7467e0dab62..32cb7e556716 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -6,11 +6,9 @@ import { setConfig } from "../config/setters"; import * as CustomText from "./custom-text"; import * as TimerProgress from "./timer-progress"; import * as LiveSpeed from "./live-speed"; -import * as TestStats from "./test-stats"; import * as TestInput from "./test-input"; import * as TestWords from "./test-words"; import * as Monkey from "./monkey"; -import * as Numbers from "@monkeytype/util/numbers"; import { showNoticeNotification, showErrorNotification, @@ -29,6 +27,7 @@ import { clearLowFpsMode, setLowFpsMode } from "../anim"; import { createTimer } from "animejs"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; import { logTestEvent } from "./events/data"; +import { getCurrentAccuracy, getCurrentWpmAndRaw } from "./events/stats"; let lastLoop = 0; const newTimer = createTimer({ @@ -86,6 +85,7 @@ export function clear(logEnd = false, now = performance.now()): void { logTestEvent("timer", now, { event: "end", timer: Time.get(), + date: new Date().getTime(), }); } } @@ -100,17 +100,6 @@ function premid(): void { if (timerDebug) console.timeEnd("premid"); } -function calculateWpmRaw(): { wpm: number; raw: number } { - if (timerDebug) console.time("calculate wpm and raw"); - const wpmAndRaw = TestStats.calculateWpmAndRaw(); - if (timerDebug) console.timeEnd("calculate wpm and raw"); - if (timerDebug) console.time("push to history"); - TestInput.pushToWpmHistory(wpmAndRaw.wpm); - TestInput.pushToRawHistory(wpmAndRaw.raw); - if (timerDebug) console.timeEnd("push to history"); - return wpmAndRaw; -} - function monkey(wpmAndRaw: { wpm: number; raw: number }): void { if (timerDebug) console.time("update monkey"); const num = Config.blindMode ? wpmAndRaw.raw : wpmAndRaw.wpm; @@ -118,13 +107,6 @@ function monkey(wpmAndRaw: { wpm: number; raw: number }): void { if (timerDebug) console.timeEnd("update monkey"); } -function calculateAcc(): number { - if (timerDebug) console.time("calculate acc"); - const acc = Numbers.roundTo2(TestStats.calculateAccuracy()); - if (timerDebug) console.timeEnd("calculate acc"); - return acc; -} - function layoutfluid(): void { if (timerDebug) console.time("layoutfluid"); if (Config.funbox.includes("layoutfluid") && Config.mode === "time") { @@ -176,9 +158,6 @@ function checkIfFailed( acc: number, ): boolean { if (timerDebug) console.time("fail conditions"); - TestInput.pushKeypressesToHistory(); - TestInput.pushErrorToHistory(); - TestInput.pushAfkToHistory(); if ( Config.minWpm === "custom" && wpmAndRaw.wpm < Config.minWpmCustomSpeed && @@ -214,8 +193,7 @@ function checkIfTimeIsUp(): void { //times up if (timer !== null) clearTimeout(timer); Caret.hide(); - TestInput.input.pushHistory(); - TestInput.corrected.pushHistory(); + // TestInput.input.pushHistory(); SlowTimer.clear(); slowTimerCount = 0; timerEvent.dispatch({ key: "finish" }); @@ -258,8 +236,8 @@ function timerStep(): void { //calc Time.increment(); - const wpmAndRaw = calculateWpmRaw(); - const acc = calculateAcc(); + const wpmAndRaw = getCurrentWpmAndRaw(); + const acc = getCurrentAccuracy(); //ui updates requestDebouncedAnimationFrame("test-timer.timerStep", () => { @@ -329,12 +307,13 @@ async function _startNew(): Promise { logTestEvent("timer", performance.now(), { event: "start", timer: Time.get(), + date: new Date().getTime(), }); } async function _startOld(): Promise { timerStats = []; - expected = TestStats.start + interval; + // expected = TestStats.start + interval; logTestEvent("timer", performance.now(), { event: "start", timer: Time.get(), diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index ffc105d1dfda..58b1d1dd4ba5 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -34,7 +34,6 @@ import { } from "../utils/debounced-animation-frame"; import * as SoundController from "../controllers/sound-controller"; import * as Numbers from "@monkeytype/util/numbers"; -import * as TestStats from "./test-stats"; import { highlight } from "../events/keymap"; import * as LiveAcc from "./live-acc"; import * as Focus from "../test/focus"; @@ -69,6 +68,13 @@ import { import { getTheme } from "../states/theme"; import { skipBreakdownEvent } from "../states/header"; import { wordsHaveNewline } from "../states/test"; +import { + getBurstHistory, + getCorrectedWords, + getCurrentAccuracy, + getInputHistory, + getMissedWords, +} from "./events/stats"; export const updateHintsPositionDebounced = Misc.debounceUntilResolved( updateHintsPosition, @@ -493,7 +499,7 @@ function showWords(): void { wordsEl.setHtml(""); if (Config.mode === "zen") { - appendEmptyWordElement(); + appendEmptyWordElement(0); } else { let wordsHTML = ""; for (let i = 0; i < TestWords.words.length; i++) { @@ -509,9 +515,7 @@ function showWords(): void { PaceCaret.resetCaretPosition(); } -export function appendEmptyWordElement( - index = TestInput.input.getHistory().length, -): void { +export function appendEmptyWordElement(index: number): void { wordsEl.appendHtml( `
`, ); @@ -1312,10 +1316,13 @@ async function loadWordsHistory(): Promise { const wordsContainer = qs("#resultWordsHistory .words"); wordsContainer?.empty(); - const inputHistoryLength = TestInput.input.getHistory().length; + const inputHistory = getInputHistory(); + const burstHistory = getBurstHistory(); + const correctedHistory = getCorrectedWords(); + const inputHistoryLength = inputHistory.length; for (let i = 0; i < inputHistoryLength + 2; i++) { - const input = TestInput.input.getHistory(i); - const corrected = TestInput.corrected.getHistory(i); + const input = inputHistory[i]; + const corrected = correctedHistory[i]; const word = TestWords.words.getText(i) ?? ""; const koreanRegex = /[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/; @@ -1350,7 +1357,7 @@ async function loadWordsHistory(): Promise { wordEl.classList.add("error"); } - const burstValue = TestInput.burstHistory[i]; + const burstValue = burstHistory[i]; if (burstValue !== undefined) { wordEl.setAttribute("burst", String(burstValue)); } @@ -1459,7 +1466,8 @@ export async function applyBurstHeatmap(): Promise { if (Config.burstHeatmap) { qsa("#resultWordsHistory .heatmapLegend")?.show(); - let burstlist = [...TestInput.burstHistory]; + const burstHistory = getBurstHistory(); + let burstlist = [...burstHistory]; burstlist = burstlist.map((x) => (x >= 1000 ? Infinity : x)); @@ -1734,7 +1742,7 @@ function afterAnyTestInput( void SoundController.playClick(); } - const acc: number = Numbers.roundTo2(TestStats.calculateAccuracy()); + const acc: number = Numbers.roundTo2(getCurrentAccuracy()); if (!isNaN(acc)) LiveAcc.update(acc); if (Config.mode !== "time") { @@ -1832,13 +1840,13 @@ export function beforeTestWordChange( export async function afterTestWordChange( direction: "forward" | "back", + lastBurst?: number, ): Promise { updateActiveElement({ direction, }); Caret.updatePosition(); - const lastBurst = TestInput.burstHistory[TestInput.burstHistory.length - 1]; if (Numbers.isSafeNumber(lastBurst)) { void LiveBurst.update(Math.round(lastBurst)); } @@ -1944,11 +1952,11 @@ export function onTestFinish(): void { qs(".pageTest #copyWordsListButton")?.on("click", async () => { let words; if (Config.mode === "zen") { - words = TestInput.input.getHistory().join(" "); + words = getInputHistory().join(" "); } else { words = TestWords.words .getText() - .slice(0, TestInput.input.getHistory().length) + .slice(0, getInputHistory().length) .join(" "); } await copyToClipboard(words); @@ -1957,9 +1965,9 @@ qs(".pageTest #copyWordsListButton")?.on("click", async () => { qs(".pageTest #copyMissedWordsListButton")?.on("click", async () => { let words; if (Config.mode === "zen") { - words = TestInput.input.getHistory().join(" "); + words = getInputHistory().join(" "); } else { - words = Object.keys(TestInput.missedWords ?? {}).join(" "); + words = (Object.keys(getMissedWords()) ?? {}).join(" "); } await copyToClipboard(words); }); diff --git a/frontend/src/ts/test/timer-progress.ts b/frontend/src/ts/test/timer-progress.ts index 89a41bc521e9..717748e9bbbc 100644 --- a/frontend/src/ts/test/timer-progress.ts +++ b/frontend/src/ts/test/timer-progress.ts @@ -2,7 +2,6 @@ import { Config } from "../config/store"; import * as CustomText from "./custom-text"; import * as DateTime from "../utils/date-and-time"; import * as TestWords from "./test-words"; -import * as TestInput from "./test-input"; import * as Time from "../legacy-states/time"; import * as TestState from "./test-state"; import { configEvent } from "../events/config"; @@ -111,12 +110,12 @@ function getCurrentCount(): number { 1 ); } else { - return TestInput.input.getHistory().length; + return TestState.activeWordIndex; } } function setTimerHtmlToInputLength(el: HTMLElement, wrapInDiv: boolean): void { - let historyLength = `${TestInput.input.getHistory().length}`; + let historyLength = `${TestState.activeWordIndex}`; if (wrapInDiv) { historyLength = `
${historyLength}
`; diff --git a/frontend/src/ts/test/weak-spot.ts b/frontend/src/ts/test/weak-spot.ts index b248ddb6f46c..9bfce4c53a0a 100644 --- a/frontend/src/ts/test/weak-spot.ts +++ b/frontend/src/ts/test/weak-spot.ts @@ -1,4 +1,4 @@ -import * as TestInput from "./test-input"; +import { getKeypressSpacing } from "./events/stats"; import { Wordset } from "./wordset"; // Changes how quickly it 'learns' scores - very roughly the score for a char @@ -33,7 +33,7 @@ class Score { } export function updateScore(char: string, isCorrect: boolean): void { - const timings = TestInput.keypressTimings.spacing.array; + const timings = getKeypressSpacing(); if (timings.length === 0 || typeof timings === "string") { return; }