From e6d57a8b92dd110c741825c5ac0563cf1f0bb24e Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 14:09:19 +0200 Subject: [PATCH 01/28] chore: make it easier to debug mismatch --- frontend/src/ts/test/test-logic.ts | 35 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 6934fff82a95..c03958d4d076 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -85,7 +85,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, @@ -901,6 +905,8 @@ function buildCompletedEvent( return completedEvent; } +const ALWAYSREPORT = false; + function compareCompletedEvents( ce: Omit, ): void { @@ -1115,12 +1121,16 @@ function compareCompletedEvents( } if (notMatching.length === 0) { - // showSuccessNotification("Completed events match", { important: true }); + if (ALWAYSREPORT) { + showSuccessNotification("Completed events match", { important: true }); + } } else { - // showErrorNotification( - // `Completed event mismatch: ${notMatching.join(", ")}`, - // { important: true }, - // ); + if (ALWAYSREPORT) { + showErrorNotification( + `Completed event mismatch: ${notMatching.join(", ")}`, + { important: true }, + ); + } mismatchedKeys.sort(); const groupKey = mismatchedKeys.join(","); Ape.results @@ -1499,11 +1509,16 @@ export async function finish(difficultyFailed = false): Promise { // test is valid + if (ALWAYSREPORT) { + logEventsDataToTheConsoleTable(); + } + if ( - getAuthenticatedUser() !== null && - !dontSave && - !difficultyFailed && - Config.resultSaving + (getAuthenticatedUser() !== null && + !dontSave && + !difficultyFailed && + Config.resultSaving) || + ALWAYSREPORT ) { compareCompletedEvents(ce); } From d6c5b69956c265ca13abf25450f222ae6c9ef4b8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 14:09:49 +0200 Subject: [PATCH 02/28] fix(input): prevent default when on before delete returns --- frontend/src/ts/input/handlers/before-delete.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/ts/input/handlers/before-delete.ts b/frontend/src/ts/input/handlers/before-delete.ts index c6bbf68c0f57..3dc7c77a1c73 100644 --- a/frontend/src/ts/input/handlers/before-delete.ts +++ b/frontend/src/ts/input/handlers/before-delete.ts @@ -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; } From 3e1b4b1aae354773cb170212278925939e44154f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 16:09:47 +0200 Subject: [PATCH 03/28] chore: move keypresscount check, add total check --- frontend/src/ts/test/test-logic.ts | 57 ++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index c03958d4d076..337e19eb11c9 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1063,25 +1063,6 @@ function compareCompletedEvents( ); } } - - { - 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; @@ -1120,6 +1101,44 @@ function compareCompletedEvents( } } + { + 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, + ); + } + } + + { + const a = TestInput.keypressCountHistory.reduce((acc, val) => { + if (val === undefined) return acc; + return acc + val; + }, 0); + const b = getKeypressesPerSecond().reduce((acc, val) => { + if (val === undefined) return acc; + return acc + val; + }, 0); + if (a === b) { + console.debug(`Completed event match on totalKeypressCountHistory:`, a); + } else { + notMatching.push(`totalKeypressCountHistory (${a} vs ${b})`); + mismatchedKeys.push("totalKeypressCountHistory"); + console.error( + `Completed event mismatch on totalKeypressCountHistory:`, + a, + b, + ); + } + } + if (notMatching.length === 0) { if (ALWAYSREPORT) { showSuccessNotification("Completed events match", { important: true }); From 8a6fcdb667a02176e2f7788a7cde32d34623f0af Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 18:48:12 +0200 Subject: [PATCH 04/28] yeet wpm history, raw history, burst history, old burst calculation use new system for current wpm, raw, acc, burst --- frontend/src/ts/input/handlers/insert-text.ts | 4 +- .../src/ts/input/helpers/word-navigation.ts | 9 +- .../src/ts/input/listeners/composition.ts | 4 - frontend/src/ts/test/events/stats.ts | 114 +++++++++++++++++- frontend/src/ts/test/practise-words.ts | 8 +- frontend/src/ts/test/replay.ts | 3 +- frontend/src/ts/test/result.ts | 10 +- frontend/src/ts/test/test-input.ts | 31 +---- frontend/src/ts/test/test-logic.ts | 13 +- frontend/src/ts/test/test-stats.ts | 25 ---- frontend/src/ts/test/test-timer.ts | 14 +-- frontend/src/ts/test/test-ui.ts | 13 +- 12 files changed, 139 insertions(+), 109 deletions(-) diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index 6145641c5a49..54c8208a3bab 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -175,9 +175,6 @@ export async function onInsertText(options: OnInsertTextParams): Promise { if (Config.keymapMode === "react") { flash(data, correct); } - if (testInput.length === 0 && !isCompositionEnding) { - TestInput.setBurstStart(now); - } if (!shouldGoToNextWord) { TestInput.corrected.update(data, correct); } @@ -221,6 +218,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/helpers/word-navigation.ts b/frontend/src/ts/input/helpers/word-navigation.ts index 897d115e65c0..c1f5e99077a0 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 { 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,8 +58,7 @@ 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()); @@ -88,7 +89,7 @@ export async function goToNextWord({ setInputElementValue(""); TestInput.input.syncWithInputElement(); - void TestUI.afterTestWordChange("forward"); + void TestUI.afterTestWordChange("forward", burst); return ret; } 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/stats.ts b/frontend/src/ts/test/events/stats.ts index 3f8ae474ba7b..5c18b3ede77b 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -183,6 +183,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(); @@ -243,12 +265,100 @@ 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; +} + +export function getWordBurst(wordIndex: number, now?: number): number { + //todo: composition start must be the start time for burst calculation + + const events = getInputEventsPerWord().get(wordIndex) ?? []; + + 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; + } + + let endTime = lastKeypressTime ?? now ?? performance.now(); + + const durationSeconds = (endTime - firstKeypressTime) / 1000; + if (durationSeconds <= 0) return Infinity; + + return Math.round(calculateWpm(inputLength, durationSeconds)); +} + +export function getBurstHistory(): number[] { + let burstHistory: number[] = []; + for (let i = 0; i < TestWords.words.length; i++) { + burstHistory.push(getWordBurst(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; diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index b1ee407c3dfd..9e209662486d 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -9,6 +9,7 @@ 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 } from "./events/stats"; type Before = { mode: Mode | null; @@ -90,10 +91,9 @@ export function init( .getText() .slice(0, TestInput.input.getHistory().length - 1); - sortableSlowWords = typedWords.map((e, i) => [ - e, - TestInput.burstHistory[i] ?? 0, - ]); + const burstHistory = getBurstHistory(); + + 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..e5fcaf183807 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -110,8 +110,8 @@ async function updateChartData(): Promise { let labels = []; - for (let i = 1; i <= TestInput.wpmHistory.length; i++) { - if (TestStats.lastSecondNotRound && i === TestInput.wpmHistory.length) { + for (let i = 1; i <= 0; i++) { + if (TestStats.lastSecondNotRound && i === 0) { labels.push(Numbers.roundTo2(result.testDuration).toString()); } else { labels.push(i.toString()); @@ -124,11 +124,7 @@ async function updateChartData(): Promise { ), ]; - const chartData2 = [ - ...TestInput.rawHistory.map((a) => - Numbers.roundTo2(typingSpeedUnit.fromWpm(a)), - ), - ]; + const chartData2: number[] = []; const valueWindow = Math.max(...result.chartData.burst) * 0.25; let smoothedBurst = Arrays.smoothWithValueWindow( diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index e74f8ba05b07..55834d328730 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -1,6 +1,5 @@ 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"; @@ -211,7 +210,6 @@ 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( @@ -235,9 +233,7 @@ 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, @@ -263,10 +259,6 @@ 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; @@ -518,27 +510,7 @@ export function pushMissedWord(word: string): void { } } -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 = []; @@ -548,7 +520,6 @@ export function restart(): void { count: 0, words: [], }; - currentBurstStart = 0; missedWords = Object.create(null) as MissedWordsType; accuracy = { correct: 0, diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 337e19eb11c9..fed7988a3b17 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -820,7 +820,7 @@ function buildCompletedEvent( } const chartData = { - wpm: TestInput.wpmHistory, + wpm: [], burst: rawPerSecond, err: chartErr, }; @@ -905,7 +905,7 @@ function buildCompletedEvent( return completedEvent; } -const ALWAYSREPORT = false; +const ALWAYSREPORT = true; function compareCompletedEvents( ce: Omit, @@ -1341,12 +1341,6 @@ export async function finish(difficultyFailed = false): Promise { // 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); - } - //remove afk from zen if (Config.mode === "zen" || TestState.bailedOut) { TestStats.removeAfkData(); @@ -1372,9 +1366,6 @@ export async function finish(difficultyFailed = false): Promise { !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(); diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 9d7701fc45f7..c4fec1ef09c3 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -47,11 +47,7 @@ export function getStats(): unknown { 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, @@ -178,25 +174,6 @@ 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 / @@ -208,8 +185,6 @@ export function calculateAccuracy(): number { export function removeAfkData(): void { const testSeconds = calculateTestSeconds(); TestInput.keypressCountHistory.splice(testSeconds); - TestInput.wpmHistory.splice(testSeconds); - TestInput.rawHistory.splice(testSeconds); } function getInputWords(): string[] { diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index b7467e0dab62..3d112948cc13 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -29,6 +29,7 @@ import { clearLowFpsMode, setLowFpsMode } from "../anim"; import { createTimer } from "animejs"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; import { logTestEvent } from "./events/data"; +import { getCurrentWpmAndRaw } from "./events/stats"; let lastLoop = 0; const newTimer = createTimer({ @@ -100,17 +101,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; @@ -258,7 +248,7 @@ function timerStep(): void { //calc Time.increment(); - const wpmAndRaw = calculateWpmRaw(); + const wpmAndRaw = getCurrentWpmAndRaw(); const acc = calculateAcc(); //ui updates diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index ffc105d1dfda..05296b60b8ae 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,7 @@ import { import { getTheme } from "../states/theme"; import { skipBreakdownEvent } from "../states/header"; import { wordsHaveNewline } from "../states/test"; +import { getBurstHistory, getCurrentAccuracy } from "./events/stats"; export const updateHintsPositionDebounced = Misc.debounceUntilResolved( updateHintsPosition, @@ -1312,6 +1312,8 @@ async function loadWordsHistory(): Promise { const wordsContainer = qs("#resultWordsHistory .words"); wordsContainer?.empty(); + const burstHistory = getBurstHistory(); + const inputHistoryLength = TestInput.input.getHistory().length; for (let i = 0; i < inputHistoryLength + 2; i++) { const input = TestInput.input.getHistory(i); @@ -1350,7 +1352,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 +1461,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 +1737,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 +1835,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)); } From ee43b07fa4e7ea020a6d06655b0ef2dc12b4365e Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 19:04:34 +0200 Subject: [PATCH 05/28] burst improve perf --- frontend/src/ts/test/events/stats.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 5c18b3ede77b..efda7750be1e 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -303,11 +303,8 @@ export function getCurrentAccuracy(): number { return total === 0 ? 100 : (correct / total) * 100; } -export function getWordBurst(wordIndex: number, now?: number): number { - //todo: composition start must be the start time for burst calculation - - const events = getInputEventsPerWord().get(wordIndex) ?? []; - +//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; @@ -337,7 +334,7 @@ export function getWordBurst(wordIndex: number, now?: number): number { lastKeypressTime = undefined; } - let endTime = lastKeypressTime ?? now ?? performance.now(); + const endTime = lastKeypressTime ?? now ?? performance.now(); const durationSeconds = (endTime - firstKeypressTime) / 1000; if (durationSeconds <= 0) return Infinity; @@ -345,10 +342,16 @@ export function getWordBurst(wordIndex: number, now?: number): number { return Math.round(calculateWpm(inputLength, durationSeconds)); } +export function getWordBurst(wordIndex: number, now?: number): number { + const events = getInputEventsPerWord().get(wordIndex) ?? []; + return computeBurst(events, now); +} + export function getBurstHistory(): number[] { - let burstHistory: number[] = []; + const eventsPerWord = getInputEventsPerWord(); + const burstHistory: number[] = []; for (let i = 0; i < TestWords.words.length; i++) { - burstHistory.push(getWordBurst(i)); + burstHistory.push(computeBurst(eventsPerWord.get(i) ?? [])); } return burstHistory; } From 4f6f87cf8d0c49aa94a0546edf421f58b1f77ae4 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 19:53:33 +0200 Subject: [PATCH 06/28] yeet keypress timings --- frontend/src/ts/input/handlers/keydown.ts | 4 - frontend/src/ts/input/handlers/keyup.ts | 2 - frontend/src/ts/test/events/data.ts | 6 +- frontend/src/ts/test/test-input.ts | 321 +--------------------- frontend/src/ts/test/test-logic.ts | 48 +--- frontend/src/ts/test/test-stats.ts | 30 -- frontend/src/ts/test/weak-spot.ts | 4 +- 7 files changed, 13 insertions(+), 402 deletions(-) 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/test/events/data.ts b/frontend/src/ts/test/events/data.ts index ebda2b4c76ba..01cf95e30aa8 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; }); diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index 55834d328730..6e71e785626a 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -1,95 +1,6 @@ import { lastElementFromArray } from "../utils/arrays"; -import { mean, roundTo2 } from "@monkeytype/util/numbers"; -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[]; @@ -203,8 +114,6 @@ class Corrected { } } -let keyDownData: Record = {}; - export const input = new Input(); export const corrected = new Corrected(); @@ -219,16 +128,7 @@ 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, @@ -285,223 +185,6 @@ export function incrementAccuracy(correctincorrect: boolean): void { } } -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; @@ -525,6 +208,4 @@ export function restart(): void { correct: 0, incorrect: 0, }; - - resetKeypressTimings(); } diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index fed7988a3b17..33d5350fdc79 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -177,7 +177,6 @@ export function startTest(now: number): boolean { TestState.setActive(true); Replay.startReplayRecording(); Replay.replayGetWordsList(TestWords.words.list); - TestInput.carryoverFirstKeypress(); Time.set(0); TestTimer.clear(); @@ -776,43 +775,14 @@ 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) { @@ -882,14 +852,14 @@ function buildCompletedEvent( difficulty: Config.difficulty, blindMode: Config.blindMode, tags: activeTagsIds, - keySpacing: TestInput.keypressTimings.spacing.array, - keyDuration: TestInput.keypressTimings.duration.array, + keySpacing: [], + keyDuration: [], keyOverlap: Numbers.roundTo2(TestInput.keyOverlap.total), - lastKeyToEnd: lkte, - startToFirstKey: stfk, + lastKeyToEnd: 0, + startToFirstKey: 0, consistency: consistency, wpmConsistency: wpmConsistency, - keyConsistency: keyConsistency, + keyConsistency: 0, funbox: Config.funbox, bailedOut: TestState.bailedOut, chartData: chartData, @@ -1324,14 +1294,8 @@ export async function finish(difficultyFailed = false): Promise { Replay.replayGetWordsList(TestInput.input.getHistory()); } - 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); diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index c4fec1ef09c3..2dd2c53752e5 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -51,7 +51,6 @@ export function getStats(): unknown { lastSecondNotRound, missedWords: TestInput.missedWords, accuracy: TestInput.accuracy, - keypressTimings: TestInput.keypressTimings, keyOverlap: TestInput.keyOverlap, wordsHistory: TestWords.words.list.slice( 0, @@ -60,35 +59,6 @@ export function getStats(): unknown { 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; } 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; } From 99a7953217f65787223e2f39c31a3465ed2f0b34 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 19:58:59 +0200 Subject: [PATCH 07/28] yeet keypresscounthistory --- frontend/src/ts/input/handlers/insert-text.ts | 1 - frontend/src/ts/test/test-input.ts | 13 ---- frontend/src/ts/test/test-logic.ts | 68 +------------------ frontend/src/ts/test/test-stats.ts | 8 +-- frontend/src/ts/test/test-timer.ts | 1 - 5 files changed, 2 insertions(+), 89 deletions(-) diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index 54c8208a3bab..c99361d3c6ed 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -166,7 +166,6 @@ export async function onInsertText(options: OnInsertTextParams): Promise { Replay.addReplayEvent(correct ? "correctLetter" : "incorrectLetter", data); TestInput.incrementAccuracy(correct); WeakSpot.updateScore(data, correct); - TestInput.incrementKeypressCount(); TestInput.pushKeypressWord(wordIndex); if (!correct) { TestInput.incrementKeypressErrors(); diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index 6e71e785626a..1082ea11cb08 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -117,8 +117,6 @@ class Corrected { export const input = new Input(); export const corrected = new Corrected(); -export let keypressCountHistory: number[] = []; -let currentKeypressCount = 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( @@ -143,10 +141,6 @@ let currentErrorHistory: ErrorHistoryObject = { export let afkHistory: boolean[] = []; let currentAfk = true; -export function incrementKeypressCount(): void { - currentKeypressCount++; -} - export function setCurrentNotAfk(): void { currentAfk = false; } @@ -159,11 +153,6 @@ export function pushKeypressWord(wordIndex: number): void { currentErrorHistory.words.push(wordIndex); } -export function pushKeypressesToHistory(): void { - keypressCountHistory.push(currentKeypressCount); - currentKeypressCount = 0; -} - export function pushAfkToHistory(): void { afkHistory.push(currentAfk); currentAfk = true; @@ -194,8 +183,6 @@ export function pushMissedWord(word: string): void { } export function restart(): void { - keypressCountHistory = []; - currentKeypressCount = 0; afkHistory = []; currentAfk = true; errorHistory = []; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 33d5350fdc79..827ee6614f17 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -104,7 +104,6 @@ import { getWpmHistory, getAfkDuration, forceReleaseAllKeys, - getKeypressesPerSecond, } from "./events/stats"; import { calculateWpm } from "../utils/numbers"; @@ -274,7 +273,6 @@ export function restart(options = {} as RestartOptions): void { } if (Config.resultSaving) { - TestInput.pushKeypressesToHistory(); TestInput.pushErrorToHistory(); TestInput.pushAfkToHistory(); const testSeconds = TestStats.calculateTestSeconds(performance.now()); @@ -1071,44 +1069,6 @@ function compareCompletedEvents( } } - { - 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, - ); - } - } - - { - const a = TestInput.keypressCountHistory.reduce((acc, val) => { - if (val === undefined) return acc; - return acc + val; - }, 0); - const b = getKeypressesPerSecond().reduce((acc, val) => { - if (val === undefined) return acc; - return acc + val; - }, 0); - if (a === b) { - console.debug(`Completed event match on totalKeypressCountHistory:`, a); - } else { - notMatching.push(`totalKeypressCountHistory (${a} vs ${b})`); - mismatchedKeys.push("totalKeypressCountHistory"); - console.error( - `Completed event mismatch on totalKeypressCountHistory:`, - a, - b, - ); - } - } - if (notMatching.length === 0) { if (ALWAYSREPORT) { showSuccessNotification("Completed events match", { important: true }); @@ -1305,11 +1265,6 @@ export async function finish(difficultyFailed = false): Promise { // logEventsDataToTheConsoleTable(); - //remove afk from zen - if (Config.mode === "zen" || TestState.bailedOut) { - TestStats.removeAfkData(); - } - // stats const stats = TestStats.calculateFinalStats(); if ( @@ -1330,31 +1285,11 @@ export async function finish(difficultyFailed = false): Promise { !difficultyFailed && Math.round(stats.time % 1) >= 0.5 ) { - 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(stats, []); console.debug("Completed event object", ce); @@ -1749,7 +1684,6 @@ 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-stats.ts b/frontend/src/ts/test/test-stats.ts index 2dd2c53752e5..d7b1915733c8 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -47,7 +47,6 @@ export function getStats(): unknown { end3, afkHistory: TestInput.afkHistory, errorHistory: TestInput.errorHistory, - keypressCountHistory: TestInput.keypressCountHistory, lastSecondNotRound, missedWords: TestInput.missedWords, accuracy: TestInput.accuracy, @@ -127,7 +126,7 @@ export function setStart(s: number): void { export function calculateAfkSeconds(testSeconds: number): number { let extraAfk = 0; if (testSeconds !== undefined) { - extraAfk = Math.round(testSeconds) - TestInput.keypressCountHistory.length; + extraAfk = Math.round(testSeconds); if (extraAfk < 0) extraAfk = 0; // console.log("-- extra afk debug"); // console.log("should be " + Math.ceil(testSeconds)); @@ -152,11 +151,6 @@ export function calculateAccuracy(): number { return isNaN(acc) ? 100 : acc; } -export function removeAfkData(): void { - const testSeconds = calculateTestSeconds(); - TestInput.keypressCountHistory.splice(testSeconds); -} - function getInputWords(): string[] { const containsKorean = TestInput.input.getKoreanStatus(); diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 3d112948cc13..11b444398a9d 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -166,7 +166,6 @@ function checkIfFailed( acc: number, ): boolean { if (timerDebug) console.time("fail conditions"); - TestInput.pushKeypressesToHistory(); TestInput.pushErrorToHistory(); TestInput.pushAfkToHistory(); if ( From c2bb6619d7d9c05fe9fef36d4c40394203aaef6f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:01:53 +0200 Subject: [PATCH 08/28] yeet afk history --- frontend/src/ts/input/handlers/delete.ts | 1 - frontend/src/ts/input/handlers/insert-text.ts | 1 - frontend/src/ts/test/test-input.ts | 14 -------------- frontend/src/ts/test/test-logic.ts | 10 +++------- frontend/src/ts/test/test-stats.ts | 17 ----------------- frontend/src/ts/test/test-timer.ts | 1 - 6 files changed, 3 insertions(+), 41 deletions(-) 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 c99361d3c6ed..d3b77ce0ae7e 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -162,7 +162,6 @@ 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); diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index 1082ea11cb08..8a5269e4bda4 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -138,13 +138,6 @@ let currentErrorHistory: ErrorHistoryObject = { words: [], }; -export let afkHistory: boolean[] = []; -let currentAfk = true; - -export function setCurrentNotAfk(): void { - currentAfk = false; -} - export function incrementKeypressErrors(): void { currentErrorHistory.count++; } @@ -153,11 +146,6 @@ export function pushKeypressWord(wordIndex: number): void { currentErrorHistory.words.push(wordIndex); } -export function pushAfkToHistory(): void { - afkHistory.push(currentAfk); - currentAfk = true; -} - export function pushErrorToHistory(): void { errorHistory.push(currentErrorHistory); currentErrorHistory = { @@ -183,8 +171,6 @@ export function pushMissedWord(word: string): void { } export function restart(): void { - afkHistory = []; - currentAfk = true; errorHistory = []; currentErrorHistory = { count: 0, diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 827ee6614f17..99009484121d 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -274,9 +274,8 @@ export function restart(options = {} as RestartOptions): void { if (Config.resultSaving) { TestInput.pushErrorToHistory(); - TestInput.pushAfkToHistory(); const testSeconds = TestStats.calculateTestSeconds(performance.now()); - const afkseconds = TestStats.calculateAfkSeconds(testSeconds); + const afkseconds = 0; let tt = Numbers.roundTo2(testSeconds - afkseconds); if (tt < 0) tt = 0; const acc = Numbers.roundTo2(TestStats.calculateAccuracy()); @@ -816,7 +815,7 @@ function buildCompletedEvent( .map((tag) => tag._id); const duration = parseFloat(stats.time.toString()); - const afkDuration = TestStats.calculateAfkSeconds(duration); + const afkDuration = 0; let language = Config.language; if (Config.mode === "quote") { language = Strings.removeLanguageSize(Config.language); @@ -1286,7 +1285,6 @@ export async function finish(difficultyFailed = false): Promise { Math.round(stats.time % 1) >= 0.5 ) { TestInput.pushErrorToHistory(); - TestInput.pushAfkToHistory(); } const ce = buildCompletedEvent(stats, []); @@ -1325,9 +1323,8 @@ 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); @@ -1685,7 +1682,6 @@ export function fail(reason: string): void { // input.pushHistory(); // corrected.pushHistory(); TestInput.pushErrorToHistory(); - TestInput.pushAfkToHistory(); void finish(true); } diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index d7b1915733c8..57573990629a 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -45,7 +45,6 @@ export function getStats(): unknown { end, start3, end3, - afkHistory: TestInput.afkHistory, errorHistory: TestInput.errorHistory, lastSecondNotRound, missedWords: TestInput.missedWords, @@ -123,22 +122,6 @@ export function setStart(s: number): void { start3 = new Date().getTime(); } -export function calculateAfkSeconds(testSeconds: number): number { - let extraAfk = 0; - if (testSeconds !== undefined) { - extraAfk = Math.round(testSeconds); - 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; } diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 11b444398a9d..fd977b128cd7 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -167,7 +167,6 @@ function checkIfFailed( ): boolean { if (timerDebug) console.time("fail conditions"); TestInput.pushErrorToHistory(); - TestInput.pushAfkToHistory(); if ( Config.minWpm === "custom" && wpmAndRaw.wpm < Config.minWpmCustomSpeed && From 1dae6e422b0e047a30b4ad1b4ba9351327d319f7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:23:19 +0200 Subject: [PATCH 09/28] missing after last yeet --- frontend/src/ts/test/events/stats.ts | 56 ++++++++++++++++++++++++++++ frontend/src/ts/test/result.ts | 5 ++- frontend/src/ts/test/test-logic.ts | 20 ++-------- 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index efda7750be1e..02ba48298cb6 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -556,6 +556,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", diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index e5fcaf183807..6e0e9e056123 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -62,6 +62,7 @@ import { currentQuote } from "./test-words"; import { qs, qsa } from "../utils/dom"; import { getTheme } from "../states/theme"; import { isTestInvalid } from "../states/test"; +import { getRawHistory, getWpmHistory } from "./events/stats"; let result: CompletedEvent; let minChartVal: number; @@ -110,7 +111,7 @@ async function updateChartData(): Promise { let labels = []; - for (let i = 1; i <= 0; i++) { + for (let i = 1; i <= getWpmHistory().length; i++) { if (TestStats.lastSecondNotRound && i === 0) { labels.push(Numbers.roundTo2(result.testDuration).toString()); } else { @@ -124,7 +125,7 @@ async function updateChartData(): Promise { ), ]; - const chartData2: number[] = []; + const chartData2: number[] = getRawHistory(); const valueWindow = Math.max(...result.chartData.burst) * 0.25; let smoothedBurst = Arrays.smoothWithValueWindow( diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 99009484121d..6a38c7f0a82a 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -781,15 +781,12 @@ function buildCompletedEvent( consistency = 0; } - const chartErr = []; - for (const error of TestInput.errorHistory) { - chartErr.push(error.count ?? 0); - } + const wpmHistory = getWpmHistory(); const chartData = { - wpm: [], + wpm: wpmHistory, burst: rawPerSecond, - err: chartErr, + err: getErrorCountHistory(), }; //wpm consistency @@ -1278,16 +1275,7 @@ export async function finish(difficultyFailed = false): Promise { 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 - ) { - TestInput.pushErrorToHistory(); - } - - const ce = buildCompletedEvent(stats, []); + const ce = buildCompletedEvent(stats, getRawPerSecond()); console.debug("Completed event object", ce); From 1e01096321e3783b83493ef987c0a23a84a85ab1 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:23:25 +0200 Subject: [PATCH 10/28] yeet error history --- .../src/ts/controllers/chart-controller.ts | 4 +-- frontend/src/ts/input/handlers/insert-text.ts | 2 -- frontend/src/ts/test/events/stats.ts | 26 +++++++++++++++ frontend/src/ts/test/test-input.ts | 32 ------------------- frontend/src/ts/test/test-logic.ts | 4 --- frontend/src/ts/test/test-stats.ts | 1 - frontend/src/ts/test/test-timer.ts | 1 - 7 files changed, 28 insertions(+), 42 deletions(-) diff --git a/frontend/src/ts/controllers/chart-controller.ts b/frontend/src/ts/controllers/chart-controller.ts index 572c5060ee24..d10d31dd0895 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 { getIncorrectWordIndexesForSecond } from "../test/events/stats"; export class ChartWithUpdateColors< TType extends ChartType = ChartType, @@ -275,7 +275,7 @@ export const result = new ChartWithUpdateColors< try { const keypressIndex = Math.round(parseFloat(ti.label)) - 1; const wordsToHighlight = - TestInput.errorHistory[keypressIndex]?.words; + getIncorrectWordIndexesForSecond(keypressIndex); const unique = [...new Set(wordsToHighlight)]; const firstHighlightWordIndex = unique[0]; diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index d3b77ce0ae7e..c69c7a492076 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -165,9 +165,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { Replay.addReplayEvent(correct ? "correctLetter" : "incorrectLetter", data); TestInput.incrementAccuracy(correct); WeakSpot.updateScore(data, correct); - TestInput.pushKeypressWord(wordIndex); if (!correct) { - TestInput.incrementKeypressErrors(); TestInput.pushMissedWord(TestWords.words.getCurrentText()); } if (Config.keymapMode === "react") { diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 02ba48298cb6..87d8036ffe31 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -488,6 +488,32 @@ 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 getErrorCountHistory(): number[] { const { counts } = countPerInterval( (e) => diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index 8a5269e4bda4..3481864241ea 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -1,11 +1,6 @@ import { lastElementFromArray } from "../utils/arrays"; import { getInputElementValue } from "../input/input-element"; -type ErrorHistoryObject = { - count: number; - words: number[]; -}; - class Input { current: string; private history: string[]; @@ -132,28 +127,6 @@ export let keyOverlap = { lastStartTime: -1, }; -export let errorHistory: ErrorHistoryObject[] = []; -let currentErrorHistory: ErrorHistoryObject = { - count: 0, - words: [], -}; - -export function incrementKeypressErrors(): void { - currentErrorHistory.count++; -} - -export function pushKeypressWord(wordIndex: number): void { - currentErrorHistory.words.push(wordIndex); -} - -export function pushErrorToHistory(): void { - errorHistory.push(currentErrorHistory); - currentErrorHistory = { - count: 0, - words: [], - }; -} - export function incrementAccuracy(correctincorrect: boolean): void { if (correctincorrect) { accuracy.correct++; @@ -171,11 +144,6 @@ export function pushMissedWord(word: string): void { } export function restart(): void { - errorHistory = []; - currentErrorHistory = { - count: 0, - words: [], - }; missedWords = Object.create(null) as MissedWordsType; accuracy = { correct: 0, diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 6a38c7f0a82a..62950afcfc05 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -273,7 +273,6 @@ export function restart(options = {} as RestartOptions): void { } if (Config.resultSaving) { - TestInput.pushErrorToHistory(); const testSeconds = TestStats.calculateTestSeconds(performance.now()); const afkseconds = 0; let tt = Numbers.roundTo2(testSeconds - afkseconds); @@ -1667,9 +1666,6 @@ async function saveResult( export function fail(reason: string): void { failReason = reason; - // input.pushHistory(); - // corrected.pushHistory(); - TestInput.pushErrorToHistory(); void finish(true); } diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 57573990629a..19f28e10983c 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -45,7 +45,6 @@ export function getStats(): unknown { end, start3, end3, - errorHistory: TestInput.errorHistory, lastSecondNotRound, missedWords: TestInput.missedWords, accuracy: TestInput.accuracy, diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index fd977b128cd7..6d0c2b31fcdb 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -166,7 +166,6 @@ function checkIfFailed( acc: number, ): boolean { if (timerDebug) console.time("fail conditions"); - TestInput.pushErrorToHistory(); if ( Config.minWpm === "custom" && wpmAndRaw.wpm < Config.minWpmCustomSpeed && From 5d3bd0766199036a2a8da01a8574491327908073 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:26:03 +0200 Subject: [PATCH 11/28] fix word highlight --- .../src/ts/controllers/chart-controller.ts | 5 ++--- frontend/src/ts/test/events/stats.ts | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/controllers/chart-controller.ts b/frontend/src/ts/controllers/chart-controller.ts index d10d31dd0895..2142abf340c4 100644 --- a/frontend/src/ts/controllers/chart-controller.ts +++ b/frontend/src/ts/controllers/chart-controller.ts @@ -64,7 +64,7 @@ import { typedKeys } from "../utils/misc"; import { getTheme } from "../states/theme"; import { Theme } from "../constants/themes"; import { createDebouncedEffectOn } from "../hooks/effects"; -import { getIncorrectWordIndexesForSecond } from "../test/events/stats"; +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 = - getIncorrectWordIndexesForSecond(keypressIndex); + const wordsToHighlight = getWordIndexesForSecond(keypressIndex); const unique = [...new Set(wordsToHighlight)]; const firstHighlightWordIndex = unique[0]; diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 87d8036ffe31..8fce0d898e12 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -514,6 +514,28 @@ export function getIncorrectWordIndexesForSecond(second: number): number[] { 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) => From 709e2ba866516c8af08f8e759ab853e3e0c03310 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:26:57 +0200 Subject: [PATCH 12/28] fix --- frontend/src/ts/test/test-logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 62950afcfc05..dce93c72cea9 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -274,7 +274,7 @@ export function restart(options = {} as RestartOptions): void { if (Config.resultSaving) { const testSeconds = TestStats.calculateTestSeconds(performance.now()); - const afkseconds = 0; + const afkseconds = getAfkDuration(); let tt = Numbers.roundTo2(testSeconds - afkseconds); if (tt < 0) tt = 0; const acc = Numbers.roundTo2(TestStats.calculateAccuracy()); From 1c616931ce7c56e99991837831b5f0e9338b4a5a Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:28:46 +0200 Subject: [PATCH 13/28] yeet comparison --- frontend/src/ts/test/test-logic.ts | 357 +---------------------------- 1 file changed, 3 insertions(+), 354 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index dce93c72cea9..b6a3fadb7512 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -85,11 +85,7 @@ 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, - logEventsDataToTheConsoleTable, -} from "./events/data"; +import { resetTestEvents, cleanupData } from "./events/data"; import { getKeypressDurations, getChars, @@ -767,340 +763,7 @@ export async function retrySavingResult(): Promise { await saveResult(completedEvent, true); } -function buildCompletedEvent( - stats: TestStats.Stats, - rawPerSecond: number[], -): Omit { - //consistency - const stddev = Numbers.stdDev(rawPerSecond); - const avg = Numbers.mean(rawPerSecond); - let consistency = Numbers.roundTo2(Numbers.kogasa(stddev / avg)); - - if (!consistency || isNaN(consistency)) { - consistency = 0; - } - - const wpmHistory = getWpmHistory(); - - const chartData = { - wpm: wpmHistory, - burst: rawPerSecond, - err: getErrorCountHistory(), - }; - - //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 = 0; - 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: [], - keyDuration: [], - keyOverlap: Numbers.roundTo2(TestInput.keyOverlap.total), - lastKeyToEnd: 0, - startToFirstKey: 0, - consistency: consistency, - wpmConsistency: wpmConsistency, - keyConsistency: 0, - 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; -} - -const ALWAYSREPORT = true; - -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, - ); - } - } - } 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) { - if (ALWAYSREPORT) { - showSuccessNotification("Completed events match", { important: true }); - } - } else { - if (ALWAYSREPORT) { - 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 @@ -1274,7 +937,7 @@ export async function finish(difficultyFailed = false): Promise { PaceCaret.setLastTestWpm(stats.wpm); - const ce = buildCompletedEvent(stats, getRawPerSecond()); + const ce = buildCompletedEvent(); console.debug("Completed event object", ce); @@ -1402,20 +1065,6 @@ export async function finish(difficultyFailed = false): Promise { // test is valid - if (ALWAYSREPORT) { - logEventsDataToTheConsoleTable(); - } - - if ( - (getAuthenticatedUser() !== null && - !dontSave && - !difficultyFailed && - Config.resultSaving) || - ALWAYSREPORT - ) { - compareCompletedEvents(ce); - } - if (TestState.isRepeated || difficultyFailed) { if (Config.resultSaving) { const testSeconds = completedEvent.testDuration; From 2a453413f6e9a04f99b5ce3e7985ecf660fa0e6e Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:36:20 +0200 Subject: [PATCH 14/28] fix --- frontend/src/ts/test/result.ts | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 6e0e9e056123..8f9941b85663 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -27,7 +27,6 @@ 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,7 +61,7 @@ import { currentQuote } from "./test-words"; import { qs, qsa } from "../utils/dom"; import { getTheme } from "../states/theme"; import { isTestInvalid } from "../states/test"; -import { getRawHistory, getWpmHistory } from "./events/stats"; +import { getRawHistory } from "./events/stats"; let result: CompletedEvent; let minChartVal: number; @@ -111,12 +110,12 @@ async function updateChartData(): Promise { let labels = []; - for (let i = 1; i <= getWpmHistory().length; i++) { - if (TestStats.lastSecondNotRound && i === 0) { - 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 = [ @@ -138,16 +137,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) { From fd3985418202a32f0df130196c895ca6b0e23fbd Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:37:19 +0200 Subject: [PATCH 15/28] yeet last second not round --- frontend/src/ts/test/test-logic.ts | 11 ----------- frontend/src/ts/test/test-stats.ts | 7 ------- 2 files changed, 18 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index b6a3fadb7512..7fc3c3944b26 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -923,18 +923,7 @@ export async function finish(difficultyFailed = false): Promise { // logEventsDataToTheConsoleTable(); - // 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); const ce = buildCompletedEvent(); diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 19f28e10983c..82f375c48389 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -36,7 +36,6 @@ export type Stats = { 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 = { @@ -45,7 +44,6 @@ export function getStats(): unknown { end, start3, end3, - lastSecondNotRound, missedWords: TestInput.missedWords, accuracy: TestInput.accuracy, keyOverlap: TestInput.keyOverlap, @@ -66,7 +64,6 @@ export function restart(): void { end2 = 0; start3 = 0; end3 = 0; - lastSecondNotRound = false; } export function calculateTestSeconds(now?: number): number { @@ -121,10 +118,6 @@ export function setStart(s: number): void { start3 = new Date().getTime(); } -export function setLastSecondNotRound(): void { - lastSecondNotRound = true; -} - export function calculateAccuracy(): number { const acc = (TestInput.accuracy.correct / From e61c110e7b0a9b4f588c8d3a49226dff23dede83 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:38:51 +0200 Subject: [PATCH 16/28] yeet stats --- frontend/src/ts/test/test-logic.ts | 4 +- frontend/src/ts/test/test-stats.ts | 237 ----------------------------- 2 files changed, 1 insertion(+), 240 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 7fc3c3944b26..099ae52998ec 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -923,10 +923,8 @@ export async function finish(difficultyFailed = false): Promise { // logEventsDataToTheConsoleTable(); - const stats = TestStats.calculateFinalStats(); - PaceCaret.setLastTestWpm(stats.wpm); - const ce = buildCompletedEvent(); + PaceCaret.setLastTestWpm(ce.wpm); console.debug("Completed event object", ce); diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 82f375c48389..77f88a2385d9 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -1,24 +1,7 @@ -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; @@ -76,36 +59,6 @@ export function calculateTestSeconds(now?: number): number { 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(); @@ -125,193 +78,3 @@ export function calculateAccuracy(): number { 100; return isNaN(acc) ? 100 : acc; } - -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; -} From 7fd6d17bc547d22fbb0c18bc6160acc85ac5108f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:39:38 +0200 Subject: [PATCH 17/28] yeet acc --- frontend/src/ts/test/test-logic.ts | 3 ++- frontend/src/ts/test/test-stats.ts | 8 -------- frontend/src/ts/test/test-timer.ts | 12 ++---------- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 099ae52998ec..03d25eedb9e0 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -100,6 +100,7 @@ import { getWpmHistory, getAfkDuration, forceReleaseAllKeys, + getCurrentAccuracy, } from "./events/stats"; import { calculateWpm } from "../utils/numbers"; @@ -273,7 +274,7 @@ export function restart(options = {} as RestartOptions): void { 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 }); } } diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 77f88a2385d9..c8722216cc11 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -70,11 +70,3 @@ export function setStart(s: number): void { start2 = Date.now(); start3 = new Date().getTime(); } - -export function calculateAccuracy(): number { - const acc = - (TestInput.accuracy.correct / - (TestInput.accuracy.correct + TestInput.accuracy.incorrect)) * - 100; - return isNaN(acc) ? 100 : acc; -} diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 6d0c2b31fcdb..39f0bcc7cd96 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -10,7 +10,6 @@ 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,7 +28,7 @@ import { clearLowFpsMode, setLowFpsMode } from "../anim"; import { createTimer } from "animejs"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; import { logTestEvent } from "./events/data"; -import { getCurrentWpmAndRaw } from "./events/stats"; +import { getCurrentAccuracy, getCurrentWpmAndRaw } from "./events/stats"; let lastLoop = 0; const newTimer = createTimer({ @@ -108,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") { @@ -246,7 +238,7 @@ function timerStep(): void { //calc Time.increment(); const wpmAndRaw = getCurrentWpmAndRaw(); - const acc = calculateAcc(); + const acc = getCurrentAccuracy(); //ui updates requestDebouncedAnimationFrame("test-timer.timerStep", () => { From 48a17b4addd99aab2e0f003afeea6ed4a56deddb Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:40:22 +0200 Subject: [PATCH 18/28] yeet test seconds --- frontend/src/ts/test/test-logic.ts | 3 ++- frontend/src/ts/test/test-stats.ts | 10 ---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 03d25eedb9e0..12342401ad28 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -101,6 +101,7 @@ import { getAfkDuration, forceReleaseAllKeys, getCurrentAccuracy, + getCurrentTestDurationMs, } from "./events/stats"; import { calculateWpm } from "../utils/numbers"; @@ -270,7 +271,7 @@ export function restart(options = {} as RestartOptions): void { } if (Config.resultSaving) { - const testSeconds = TestStats.calculateTestSeconds(performance.now()); + const testSeconds = getCurrentTestDurationMs(performance.now()) / 1000; const afkseconds = getAfkDuration(); let tt = Numbers.roundTo2(testSeconds - afkseconds); if (tt < 0) tt = 0; diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index c8722216cc11..113e67318262 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -49,16 +49,6 @@ export function restart(): void { end3 = 0; } -export function calculateTestSeconds(now?: number): number { - let duration = (end - start) / 1000; - - if (now !== undefined) { - duration = (now - start) / 1000; - } - - return duration; -} - export function setEnd(e: number): void { end = e; end2 = Date.now(); From 66bdfe362857910a95e2ca27fe4a20e148d6941d Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:46:03 +0200 Subject: [PATCH 19/28] fully yeet stats file --- frontend/__tests__/test/events/stats.spec.ts | 2 +- frontend/src/ts/commandline/lists.ts | 35 ++++++----- frontend/src/ts/index.ts | 2 - frontend/src/ts/test/events/stats.ts | 30 ++++++++++ frontend/src/ts/test/events/types.ts | 1 + frontend/src/ts/test/test-logic.ts | 9 +-- frontend/src/ts/test/test-stats.ts | 62 -------------------- frontend/src/ts/test/test-timer.ts | 5 +- 8 files changed, 55 insertions(+), 91 deletions(-) delete mode 100644 frontend/src/ts/test/test-stats.ts diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts index 3c7d03a55a32..ea23e582d634 100644 --- a/frontend/__tests__/test/events/stats.spec.ts +++ b/frontend/__tests__/test/events/stats.spec.ts @@ -112,7 +112,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, 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/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/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 8fce0d898e12..47f814e56466 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -246,6 +246,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, 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/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 12342401ad28..e749b2d9ba2e 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"; @@ -102,6 +101,7 @@ import { forceReleaseAllKeys, getCurrentAccuracy, getCurrentTestDurationMs, + getDateBasedTestDurationMs, } from "./events/stats"; import { calculateWpm } from "../utils/numbers"; @@ -160,7 +160,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; } @@ -190,7 +190,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; @@ -319,7 +318,6 @@ export function restart(options = {} as RestartOptions): void { resetTestEvents(); TestTimer.clear(); setIsTestInvalid(false); - TestStats.restart(); TestInput.restart(); TestInput.corrected.reset(); ShiftTracker.reset(); @@ -881,7 +879,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, @@ -970,7 +967,7 @@ export async function finish(difficultyFailed = false): Promise { let tooShort = false; //fail checks - const dateDur = (TestStats.end3 - TestStats.start3) / 1000; + const dateDur = getDateBasedTestDurationMs() / 1000; if ( Config.mode === "time" && !TestState.bailedOut && diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts deleted file mode 100644 index 113e67318262..000000000000 --- a/frontend/src/ts/test/test-stats.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as TestInput from "./test-input"; -import * as TestWords from "./test-words"; -import { getLastResult } from "../states/test"; - -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 function getStats(): unknown { - const ret = { - lastResult: getLastResult(), - start, - end, - start3, - end3, - missedWords: TestInput.missedWords, - accuracy: TestInput.accuracy, - keyOverlap: TestInput.keyOverlap, - wordsHistory: TestWords.words.list.slice( - 0, - TestInput.input.getHistory().length, - ), - inputHistory: TestInput.input.getHistory(), - }; - - return ret; -} - -export function restart(): void { - start = 0; - end = 0; - start2 = 0; - end2 = 0; - start3 = 0; - end3 = 0; -} - -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(); -} diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 39f0bcc7cd96..23161a62d9e4 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -6,7 +6,6 @@ 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"; @@ -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(), }); } } @@ -308,12 +308,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(), From fa4e20b595166fa3eb546a7e9f7c0e39ae497a29 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:50:31 +0200 Subject: [PATCH 20/28] yeet acc --- frontend/src/ts/test/result.ts | 10 ++++------ frontend/src/ts/test/test-input.ts | 16 ---------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 8f9941b85663..743d79da11b7 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -26,7 +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 TestUI from "./test-ui"; import * as TodayTracker from "./today-tracker"; import { configEvent } from "../events/config"; @@ -61,7 +60,7 @@ import { currentQuote } from "./test-words"; import { qs, qsa } from "../utils/dom"; import { getTheme } from "../states/theme"; import { isTestInvalid } from "../states/test"; -import { getRawHistory } from "./events/stats"; +import { getAccuracy, getRawHistory } from "./events/stats"; let result: CompletedEvent; let minChartVal: number; @@ -344,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( @@ -368,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 @@ -394,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 3481864241ea..c2cd2643414c 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -117,24 +117,12 @@ type MissedWordsType = Record; export let missedWords: MissedWordsType = Object.create( null, ) as MissedWordsType; -export let accuracy = { - correct: 0, - incorrect: 0, -}; export let keyOverlap = { total: 0, lastStartTime: -1, }; -export function incrementAccuracy(correctincorrect: boolean): void { - if (correctincorrect) { - accuracy.correct++; - } else { - accuracy.incorrect++; - } -} - export function pushMissedWord(word: string): void { if (!Object.keys(missedWords).includes(word)) { missedWords[word] = 1; @@ -145,8 +133,4 @@ export function pushMissedWord(word: string): void { export function restart(): void { missedWords = Object.create(null) as MissedWordsType; - accuracy = { - correct: 0, - incorrect: 0, - }; } From 2cf2bf8df85e9272ac88b6e5eba7bcfe6315c750 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:50:42 +0200 Subject: [PATCH 21/28] yeet --- frontend/src/ts/test/test-input.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index c2cd2643414c..c133cb699fb3 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -118,11 +118,6 @@ export let missedWords: MissedWordsType = Object.create( null, ) as MissedWordsType; -export let keyOverlap = { - total: 0, - lastStartTime: -1, -}; - export function pushMissedWord(word: string): void { if (!Object.keys(missedWords).includes(word)) { missedWords[word] = 1; From 297775f27f7d3b4f3b5e258242002d32a16e0f50 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:51:21 +0200 Subject: [PATCH 22/28] fixes --- frontend/__tests__/test/events/data.spec.ts | 2 +- frontend/src/ts/input/handlers/insert-text.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index c69c7a492076..0605cfeb6e71 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -163,7 +163,6 @@ export async function onInsertText(options: OnInsertTextParams): Promise { // general per keypress updates Replay.addReplayEvent(correct ? "correctLetter" : "incorrectLetter", data); - TestInput.incrementAccuracy(correct); WeakSpot.updateScore(data, correct); if (!correct) { TestInput.pushMissedWord(TestWords.words.getCurrentText()); From c9fa677281d656b49ff48f35e35d95f85094d27f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 20:55:42 +0200 Subject: [PATCH 23/28] yeet missed words --- frontend/src/ts/input/handlers/insert-text.ts | 3 --- frontend/src/ts/test/events/stats.ts | 23 +++++++++++++++++++ frontend/src/ts/test/practise-words.ts | 10 ++++---- frontend/src/ts/test/test-input.ts | 18 --------------- frontend/src/ts/test/test-logic.ts | 1 - frontend/src/ts/test/test-ui.ts | 8 +++++-- 6 files changed, 35 insertions(+), 28 deletions(-) diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index 0605cfeb6e71..bb89fdb7d654 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -164,9 +164,6 @@ export async function onInsertText(options: OnInsertTextParams): Promise { // general per keypress updates Replay.addReplayEvent(correct ? "correctLetter" : "incorrectLetter", data); WeakSpot.updateScore(data, correct); - if (!correct) { - TestInput.pushMissedWord(TestWords.words.getCurrentText()); - } if (Config.keymapMode === "react") { flash(data, correct); } diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 47f814e56466..519580ce8e7d 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -753,6 +753,29 @@ 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 const __testing = { getTimerBoundaries, }; diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index 9e209662486d..097fcbce04c9 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -9,7 +9,7 @@ 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 } from "./events/stats"; +import { getBurstHistory, getMissedWords } from "./events/stats"; type Before = { mode: Mode | null; @@ -38,11 +38,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]); } @@ -57,7 +59,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]); diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index c133cb699fb3..60ddb3e5b8b4 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -111,21 +111,3 @@ class Corrected { export const input = new Input(); export const corrected = new Corrected(); - -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 function pushMissedWord(word: string): void { - if (!Object.keys(missedWords).includes(word)) { - missedWords[word] = 1; - } else { - (missedWords[word] as number) += 1; - } -} - -export function restart(): void { - missedWords = Object.create(null) as MissedWordsType; -} diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index e749b2d9ba2e..e27e1705d840 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -318,7 +318,6 @@ export function restart(options = {} as RestartOptions): void { resetTestEvents(); TestTimer.clear(); setIsTestInvalid(false); - TestInput.restart(); TestInput.corrected.reset(); ShiftTracker.reset(); AltTracker.reset(); diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 05296b60b8ae..dd991f6c722a 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -68,7 +68,11 @@ import { import { getTheme } from "../states/theme"; import { skipBreakdownEvent } from "../states/header"; import { wordsHaveNewline } from "../states/test"; -import { getBurstHistory, getCurrentAccuracy } from "./events/stats"; +import { + getBurstHistory, + getCurrentAccuracy, + getMissedWords, +} from "./events/stats"; export const updateHintsPositionDebounced = Misc.debounceUntilResolved( updateHintsPosition, @@ -1962,7 +1966,7 @@ qs(".pageTest #copyMissedWordsListButton")?.on("click", async () => { if (Config.mode === "zen") { words = TestInput.input.getHistory().join(" "); } else { - words = Object.keys(TestInput.missedWords ?? {}).join(" "); + words = (Object.keys(getMissedWords()) ?? {}).join(" "); } await copyToClipboard(words); }); From 442ddb4e86c2b7d0f9e4283e047742488dd8adb0 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 30 May 2026 21:53:04 +0200 Subject: [PATCH 24/28] yeet correcred --- frontend/__tests__/test/events/stats.spec.ts | 279 ++++++++++++++++++ frontend/src/ts/input/handlers/insert-text.ts | 3 - .../src/ts/input/helpers/word-navigation.ts | 2 - frontend/src/ts/test/events/stats.ts | 40 +++ frontend/src/ts/test/test-input.ts | 96 +++--- frontend/src/ts/test/test-logic.ts | 10 +- frontend/src/ts/test/test-timer.ts | 1 - frontend/src/ts/test/test-ui.ts | 5 +- 8 files changed, 376 insertions(+), 60 deletions(-) diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts index ea23e582d634..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 { @@ -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/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index bb89fdb7d654..f474e4d06e30 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -167,9 +167,6 @@ export async function onInsertText(options: OnInsertTextParams): Promise { if (Config.keymapMode === "react") { flash(data, correct); } - 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 diff --git a/frontend/src/ts/input/helpers/word-navigation.ts b/frontend/src/ts/input/helpers/word-navigation.ts index c1f5e99077a0..de09a57d32f0 100644 --- a/frontend/src/ts/input/helpers/word-navigation.ts +++ b/frontend/src/ts/input/helpers/word-navigation.ts @@ -66,7 +66,6 @@ export async function goToNextWord({ Funbox.toggleScript(TestWords.words.getText(TestState.activeWordIndex + 1)); TestInput.input.pushHistory(); - TestInput.corrected.pushHistory(); const lastWord = TestState.activeWordIndex >= TestWords.words.length - 1; if (lastWord) { @@ -110,7 +109,6 @@ export function goToPreviousWord( const word = TestInput.input.popHistory(); TestState.decreaseActiveWordIndex(); - TestInput.corrected.popHistory(); Funbox.toggleScript(TestWords.words.getText(TestState.activeWordIndex)); diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 519580ce8e7d..0bb62a745931 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -776,6 +776,46 @@ export function getMissedWords(): Record { 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/test-input.ts b/frontend/src/ts/test/test-input.ts index 60ddb3e5b8b4..881ba08a3383 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -61,53 +61,53 @@ 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 = ""; - } -} +// 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 = ""; +// } +// } export const input = new Input(); -export const corrected = new Corrected(); +// export const corrected = new Corrected(); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index e27e1705d840..438dc20ca596 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -84,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, @@ -318,7 +322,6 @@ export function restart(options = {} as RestartOptions): void { resetTestEvents(); TestTimer.clear(); setIsTestInvalid(false); - TestInput.corrected.reset(); ShiftTracker.reset(); AltTracker.reset(); Caret.hide(); @@ -900,7 +903,6 @@ export async function finish(difficultyFailed = false): Promise { // 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()); } @@ -919,7 +921,7 @@ export async function finish(difficultyFailed = false): Promise { cleanupData(); - // logEventsDataToTheConsoleTable(); + logEventsDataToTheConsoleTable(); const ce = buildCompletedEvent(); PaceCaret.setLastTestWpm(ce.wpm); diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 23161a62d9e4..e3e6a87fd6dd 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -194,7 +194,6 @@ function checkIfTimeIsUp(): void { if (timer !== null) clearTimeout(timer); Caret.hide(); TestInput.input.pushHistory(); - TestInput.corrected.pushHistory(); SlowTimer.clear(); slowTimerCount = 0; timerEvent.dispatch({ key: "finish" }); diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index dd991f6c722a..b556abcf4d7c 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -70,6 +70,7 @@ import { skipBreakdownEvent } from "../states/header"; import { wordsHaveNewline } from "../states/test"; import { getBurstHistory, + getCorrectedWords, getCurrentAccuracy, getMissedWords, } from "./events/stats"; @@ -1317,11 +1318,11 @@ async function loadWordsHistory(): Promise { wordsContainer?.empty(); const burstHistory = getBurstHistory(); - + const correctedHistory = getCorrectedWords(); const inputHistoryLength = TestInput.input.getHistory().length; for (let i = 0; i < inputHistoryLength + 2; i++) { const input = TestInput.input.getHistory(i); - const corrected = TestInput.corrected.getHistory(i); + const corrected = correctedHistory[i]; const word = TestWords.words.getText(i) ?? ""; const koreanRegex = /[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/; From 36b525158d30e59d9151fda499b628f2c088df51 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 31 May 2026 00:15:19 +0200 Subject: [PATCH 25/28] input history --- frontend/src/ts/test/events/stats.ts | 12 ++++++++++++ frontend/src/ts/test/test-logic.ts | 2 ++ 2 files changed, 14 insertions(+) diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 0bb62a745931..7e9968e97323 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -438,6 +438,18 @@ export function getChars(countPartialLastWord = false): CharCounts { }; } +export function getInputHistory(): string[] { + 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; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 438dc20ca596..79c59bbd6d82 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -106,6 +106,7 @@ import { getCurrentAccuracy, getCurrentTestDurationMs, getDateBasedTestDurationMs, + getInputHistory, } from "./events/stats"; import { calculateWpm } from "../utils/numbers"; @@ -922,6 +923,7 @@ export async function finish(difficultyFailed = false): Promise { cleanupData(); logEventsDataToTheConsoleTable(); + console.log(getInputHistory()); const ce = buildCompletedEvent(); PaceCaret.setLastTestWpm(ce.wpm); From 9883c9b729bd6ea444d821fbabf92fbb68643676 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 31 May 2026 00:39:13 +0200 Subject: [PATCH 26/28] yeet input history --- .../src/ts/commandline/lists/result-screen.ts | 6 +- .../src/ts/input/handlers/before-delete.ts | 4 +- .../src/ts/input/helpers/word-navigation.ts | 6 +- frontend/src/ts/test/events/data.ts | 24 +++++++ frontend/src/ts/test/events/stats.ts | 10 ++- .../src/ts/test/funbox/funbox-functions.ts | 5 +- frontend/src/ts/test/practise-words.ts | 9 ++- frontend/src/ts/test/test-input.ts | 67 +++++++++---------- frontend/src/ts/test/test-logic.ts | 16 ++--- frontend/src/ts/test/test-timer.ts | 2 +- frontend/src/ts/test/test-ui.ts | 18 ++--- frontend/src/ts/test/timer-progress.ts | 5 +- 12 files changed, 103 insertions(+), 69 deletions(-) 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/input/handlers/before-delete.ts b/frontend/src/ts/input/handlers/before-delete.ts index 3dc7c77a1c73..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) { @@ -45,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/helpers/word-navigation.ts b/frontend/src/ts/input/helpers/word-navigation.ts index de09a57d32f0..2a5ab05dc118 100644 --- a/frontend/src/ts/input/helpers/word-navigation.ts +++ b/frontend/src/ts/input/helpers/word-navigation.ts @@ -15,7 +15,7 @@ import { showLoaderBar, hideLoaderBar } from "../../states/loader-bar"; import { setInputElementValue } from "../input-element"; import { setAwaitingNextWord } from "../state"; import { DeleteInputType } from "./input-type"; -import { getWordBurst } from "../../test/events/stats"; +import { getInputForWord, getWordBurst } from "../../test/events/stats"; type GoToNextWordParams = { correctInsert: boolean; @@ -65,7 +65,7 @@ export async function goToNextWord({ Funbox.toggleScript(TestWords.words.getText(TestState.activeWordIndex + 1)); - TestInput.input.pushHistory(); + // TestInput.input.pushHistory(); const lastWord = TestState.activeWordIndex >= TestWords.words.length - 1; if (lastWord) { @@ -107,7 +107,7 @@ export function goToPreviousWord( Replay.addReplayEvent("backWord"); - const word = TestInput.input.popHistory(); + const word = getInputForWord(TestState.activeWordIndex); TestState.decreaseActiveWordIndex(); Funbox.toggleScript(TestWords.words.getText(TestState.activeWordIndex)); diff --git a/frontend/src/ts/test/events/data.ts b/frontend/src/ts/test/events/data.ts index 01cf95e30aa8..0d4e4ac2a38d 100644 --- a/frontend/src/ts/test/events/data.ts +++ b/frontend/src/ts/test/events/data.ts @@ -310,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 7e9968e97323..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, @@ -373,7 +374,7 @@ function computeBurst(events: InputEvent[], now?: number): number { } export function getWordBurst(wordIndex: number, now?: number): number { - const events = getInputEventsPerWord().get(wordIndex) ?? []; + const events = getInputEventsForWord(wordIndex); return computeBurst(events, now); } @@ -438,7 +439,14 @@ export function getChars(countPartialLastWord = false): 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[] = []; 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 097fcbce04c9..35d57cf57a4b 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -4,12 +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, getMissedWords } from "./events/stats"; +import { + getBurstHistory, + getInputHistory, + getMissedWords, +} from "./events/stats"; type Before = { mode: Mode | null; @@ -91,7 +94,7 @@ 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(); diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index 881ba08a3383..f779b8ebeea1 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -1,24 +1,23 @@ -import { lastElementFromArray } from "../utils/arrays"; import { getInputElementValue } from "../input/input-element"; class Input { current: string; - private history: string[]; + // private history: string[]; koreanStatus: boolean; constructor() { this.current = ""; - this.history = []; + // this.history = []; this.koreanStatus = false; } reset(): void { this.current = ""; - this.history = []; + // this.history = []; } - resetHistory(): void { - this.history = []; - } + // resetHistory(): void { + // this.history = []; + // } setKoreanStatus(val: boolean): void { this.koreanStatus = val; @@ -28,33 +27,33 @@ class Input { 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); - } + // 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); + // } syncWithInputElement(): void { this.current = getInputElementValue().inputValue; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 79c59bbd6d82..ffa69262d441 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -432,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(); @@ -654,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; } @@ -668,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; } @@ -903,14 +903,14 @@ 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(); - 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()); } forceReleaseAllKeys(); @@ -1071,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; diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index e3e6a87fd6dd..32cb7e556716 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -193,7 +193,7 @@ function checkIfTimeIsUp(): void { //times up if (timer !== null) clearTimeout(timer); Caret.hide(); - TestInput.input.pushHistory(); + // TestInput.input.pushHistory(); SlowTimer.clear(); slowTimerCount = 0; timerEvent.dispatch({ key: "finish" }); diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index b556abcf4d7c..58b1d1dd4ba5 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -72,6 +72,7 @@ import { getBurstHistory, getCorrectedWords, getCurrentAccuracy, + getInputHistory, getMissedWords, } from "./events/stats"; @@ -498,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++) { @@ -514,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( `
`, ); @@ -1317,11 +1316,12 @@ async function loadWordsHistory(): Promise { const wordsContainer = qs("#resultWordsHistory .words"); wordsContainer?.empty(); + const inputHistory = getInputHistory(); const burstHistory = getBurstHistory(); const correctedHistory = getCorrectedWords(); - const inputHistoryLength = TestInput.input.getHistory().length; + const inputHistoryLength = inputHistory.length; for (let i = 0; i < inputHistoryLength + 2; i++) { - const input = TestInput.input.getHistory(i); + const input = inputHistory[i]; const corrected = correctedHistory[i]; const word = TestWords.words.getText(i) ?? ""; const koreanRegex = @@ -1952,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); @@ -1965,7 +1965,7 @@ 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(getMissedWords()) ?? {}).join(" "); } 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}
`; From e1a354073dc6e76495540c150de181d85e5b95ec Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 31 May 2026 00:39:41 +0200 Subject: [PATCH 27/28] yeet --- frontend/src/ts/test/test-input.ts | 81 ------------------------------ 1 file changed, 81 deletions(-) diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index f779b8ebeea1..418385951dee 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -15,10 +15,6 @@ class Input { // this.history = []; } - // resetHistory(): void { - // this.history = []; - // } - setKoreanStatus(val: boolean): void { this.koreanStatus = val; } @@ -27,86 +23,9 @@ class Input { 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); - // } - syncWithInputElement(): void { this.current = getInputElementValue().inputValue; } } -// 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 = ""; -// } -// } - export const input = new Input(); -// export const corrected = new Corrected(); From 358e88caf0a5e98195facd18d6cae4628aadd07b Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 31 May 2026 00:41:06 +0200 Subject: [PATCH 28/28] move korean state --- frontend/src/ts/test/test-input.ts | 10 ---------- frontend/src/ts/test/test-logic.ts | 4 ++-- frontend/src/ts/test/test-state.ts | 5 +++++ 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index 418385951dee..79a5aa7ddf87 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -3,11 +3,9 @@ import { getInputElementValue } from "../input/input-element"; class Input { current: string; // private history: string[]; - koreanStatus: boolean; constructor() { this.current = ""; // this.history = []; - this.koreanStatus = false; } reset(): void { @@ -15,14 +13,6 @@ class Input { // this.history = []; } - setKoreanStatus(val: boolean): void { - this.koreanStatus = val; - } - - getKoreanStatus(): boolean { - return this.koreanStatus; - } - syncWithInputElement(): void { this.current = getInputElementValue().inputValue; } diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index ffa69262d441..8bfbb070f4bb 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -332,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(""); @@ -593,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++) { 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;