diff --git a/frontend/src/ts/commandline/lists/result-screen.ts b/frontend/src/ts/commandline/lists/result-screen.ts index 1debee3ac804..320fcbf98467 100644 --- a/frontend/src/ts/commandline/lists/result-screen.ts +++ b/frontend/src/ts/commandline/lists/result-screen.ts @@ -5,7 +5,7 @@ import { showErrorNotification, showSuccessNotification, } from "../../states/notifications"; -import * as TestInput from "../../test/test-input"; +import { getInputHistory } from "../../test/test-input"; import * as TestState from "../../test/test-state"; import * as TestWords from "../../test/test-words"; import { Config } from "../../config/store"; @@ -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 85ddc1c7e55a..bf40582731aa 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/test-input"; export function onBeforeDelete(event: InputEvent): void { if (!TestState.isActive) { @@ -51,7 +51,7 @@ export function onBeforeDelete(event: InputEvent): void { const confidence = Config.confidenceMode; const previousWordCorrect = - (TestInput.input.get(TestState.activeWordIndex - 1) ?? "") === + (getInputForWord(TestState.activeWordIndex - 1) ?? "") === TestWords.words.getText(TestState.activeWordIndex - 1); if (confidence === "on" && inputIsEmpty && !previousWordCorrect) { diff --git a/frontend/src/ts/input/handlers/before-insert-text.ts b/frontend/src/ts/input/handlers/before-insert-text.ts index 3b96d6be403c..0c0efdb91681 100644 --- a/frontend/src/ts/input/handlers/before-insert-text.ts +++ b/frontend/src/ts/input/handlers/before-insert-text.ts @@ -1,5 +1,5 @@ import { Config } from "../../config/store"; -import * as TestInput from "../../test/test-input"; +import { getCurrentInput } from "../../test/test-input"; import * as TestState from "../../test/test-state"; import * as TestUI from "../../test/test-ui"; import * as TestWords from "../../test/test-words"; @@ -61,7 +61,7 @@ export function onBeforeInsertText(data: string): boolean { // block input if the word is too long const inputLimit = Config.mode === "zen" ? 30 : TestWords.words.getCurrentText().length + 20; - const overLimit = TestInput.input.current.length >= inputLimit; + const overLimit = getCurrentInput().length >= inputLimit; if (overLimit && (shouldInsertSpaceAsCharacter === true || !dataIsSpace)) { console.error("Hitting word limit"); return true; @@ -71,7 +71,7 @@ export function onBeforeInsertText(data: string): boolean { // this will not work for the first word of each line, but that has a low chance of happening const dataIsNotFalsy = data !== null && data !== ""; const inputIsLongerThanOrEqualToWord = - TestInput.input.current.length >= TestWords.words.getCurrentText().length; + getCurrentInput().length >= TestWords.words.getCurrentText().length; if ( !SlowTimer.get() && // don't do this check if slow timer is active @@ -91,7 +91,7 @@ export function onBeforeInsertText(data: string): boolean { ); const { top: topAfterAppend, height: heightAfterAppend } = TestUI.getActiveWordTopAndHeightWithDifferentData( - (pendingWordData ?? TestInput.input.current) + data, + (pendingWordData ?? getCurrentInput()) + data, ); if (topAfterAppend > TestUI.activeWordTop) { //word jumped to next line diff --git a/frontend/src/ts/input/handlers/delete.ts b/frontend/src/ts/input/handlers/delete.ts index fa244fbe5454..d177384e17cc 100644 --- a/frontend/src/ts/input/handlers/delete.ts +++ b/frontend/src/ts/input/handlers/delete.ts @@ -1,6 +1,7 @@ import * as TestUI from "../../test/test-ui"; import * as TestWords from "../../test/test-words"; import * as TestInput from "../../test/test-input"; +import { getCurrentInput } from "../../test/test-input"; import { getInputElementValue, setInputElementValue } from "../input-element"; import * as Replay from "../../test/replay"; @@ -13,20 +14,20 @@ import { activeWordIndex } from "../../test/test-state"; export function onDelete(inputType: DeleteInputType, now: number): void { const { realInputValue } = getInputElementValue(); - const inputBeforeDelete = TestInput.input.current; + const inputBeforeDelete = getCurrentInput(); const activeWordIndexBeforeDelete = activeWordIndex; TestInput.input.syncWithInputElement(); - const inputAfterDelete = TestInput.input.current; + const inputAfterDelete = getCurrentInput(); - Replay.addReplayEvent("setLetterIndex", TestInput.input.current.length); + Replay.addReplayEvent("setLetterIndex", getCurrentInput().length); TestInput.setCurrentNotAfk(); const beforeDeleteOnlyTabs = /^\t*$/.test(inputBeforeDelete); const allTabsCorrect = TestWords.words .getCurrentText() - .startsWith(TestInput.input.current); + .startsWith(getCurrentInput()); //special check for code languages if ( diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index 9610924ce769..d65a87999955 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -1,6 +1,7 @@ import * as TestUI from "../../test/test-ui"; import * as TestWords from "../../test/test-words"; import * as TestInput from "../../test/test-input"; +import { getCurrentInput } from "../../test/test-input"; import { getInputElementValue, replaceInputElementLastValueChar, @@ -86,8 +87,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { const charOverride = charOverrides.get(options.data); if ( charOverride !== undefined && - TestWords.words.getCurrentText()[TestInput.input.current.length] !== - options.data + TestWords.words.getCurrentText()[getCurrentInput().length] !== options.data ) { // replace the data with the override setInputElementValue( @@ -101,7 +101,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { } // input and target word - const testInput = TestInput.input.current; + const testInput = getCurrentInput(); const currentWord = TestWords.words.getCurrentText(); // if the character is visually equal, replace it with the target character @@ -215,7 +215,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { } // capture DOM before goToNextWord clears it for the new word - const inputValueAfterEvent = TestInput.input.current; + const inputValueAfterEvent = getCurrentInput(); // Log the event BEFORE goToNextWord so readers inside the navigation // (e.g. beforeTestWordChange's updateWordLetters, getWordBurst) see the @@ -264,7 +264,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { //this COULD be the next word because we are awaiting goToNextWord const nextWord = TestWords.words.getCurrentText(); const doesNextWordHaveTab = /^\t+/.test(nextWord); - const isCurrentCharTab = nextWord[TestInput.input.current.length] === "\t"; + const isCurrentCharTab = nextWord[getCurrentInput().length] === "\t"; //code mode - auto insert tabs if ( diff --git a/frontend/src/ts/input/helpers/validation.ts b/frontend/src/ts/input/helpers/validation.ts index 5ab38fe05725..58b7580a4fa3 100644 --- a/frontend/src/ts/input/helpers/validation.ts +++ b/frontend/src/ts/input/helpers/validation.ts @@ -5,7 +5,7 @@ import { isSpace } from "../../utils/strings"; * Check if the input data is correct * @param options - Options object * @param options.data - Input data - * @param options.inputValue - Current input value (use TestInput.input.current, not input element value) + * @param options.inputValue - Current input value (use getCurrentInput(), not input element value) * @param options.targetWord - Target word * @param options.correctShiftUsed - Whether the correct shift state was used. Null means disabled */ @@ -47,7 +47,7 @@ export function isCharCorrect(options: { * as a "control character" (moving to the next word) * @param options - Options object * @param options.data - Input data - * @param options.inputValue - Current input value (use TestInput.input.current, not input element value) + * @param options.inputValue - Current input value (use getCurrentInput(), not input element value) * @param options.targetWord - Target word * @returns Boolean if data is space, null if not */ diff --git a/frontend/src/ts/input/listeners/composition.ts b/frontend/src/ts/input/listeners/composition.ts index cec96ead8fad..685df7dd4431 100644 --- a/frontend/src/ts/input/listeners/composition.ts +++ b/frontend/src/ts/input/listeners/composition.ts @@ -3,6 +3,7 @@ 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 { getCurrentInput } from "../../test/test-input"; import { setLastInsertCompositionTextData } from "../state"; import * as CompositionDisplay from "../../elements/composition-display"; import { onInsertText } from "../handlers/insert-text"; @@ -25,7 +26,7 @@ inputEl.addEventListener("compositionstart", (event) => { if (!TestState.isActive) { TestLogic.startTest(now); } - if (TestInput.input.current.length === 0) { + if (getCurrentInput().length === 0) { TestInput.setBurstStart(now); } diff --git a/frontend/src/ts/input/listeners/input.ts b/frontend/src/ts/input/listeners/input.ts index 9cc59ce380a3..4eaecb26bbeb 100644 --- a/frontend/src/ts/input/listeners/input.ts +++ b/frontend/src/ts/input/listeners/input.ts @@ -9,7 +9,7 @@ import { import * as TestUI from "../../test/test-ui"; import { onBeforeInsertText } from "../handlers/before-insert-text"; import { onBeforeDelete } from "../handlers/before-delete"; -import * as TestInput from "../../test/test-input"; +import { getCurrentInput } from "../../test/test-input"; import * as TestWords from "../../test/test-words"; import * as CompositionState from "../../legacy-states/composition"; import * as TestState from "../../test/test-state"; @@ -127,7 +127,7 @@ inputEl.addEventListener("input", async (event) => { ) { const allWordsTyped = activeWordIndex >= TestWords.words.length - 1; const inputPlusComposition = - TestInput.input.current + (CompositionState.getData() ?? ""); + getCurrentInput() + (CompositionState.getData() ?? ""); const inputPlusCompositionIsCorrect = TestWords.words.getCurrentText() === inputPlusComposition; diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts index bbb380838266..cff1856c799b 100644 --- a/frontend/src/ts/test/caret.ts +++ b/frontend/src/ts/test/caret.ts @@ -1,10 +1,10 @@ import { Config } from "../config/store"; -import * as TestInput from "./test-input"; import * as TestState from "../test/test-state"; import { configEvent } from "../events/config"; import { Caret } from "../elements/caret"; import * as CompositionState from "../legacy-states/composition"; import { qsr } from "../utils/dom"; +import { getCurrentInput } from "./test-input"; export function stopAnimation(): void { caret.stopBlinking(); @@ -33,8 +33,7 @@ export function resetPosition(): void { export function updatePosition(noAnim = false): void { caret.goTo({ wordIndex: TestState.activeWordIndex, - letterIndex: - TestInput.input.current.length + CompositionState.getData().length, + letterIndex: getCurrentInput().length + CompositionState.getData().length, isLanguageRightToLeft: TestState.isLanguageRightToLeft, isDirectionReversed: TestState.isDirectionReversed, animate: Config.smoothCaret !== "off" && !noAnim, diff --git a/frontend/src/ts/test/events/data.ts b/frontend/src/ts/test/events/data.ts index 6b392b87a55f..3f72860e9479 100644 --- a/frontend/src/ts/test/events/data.ts +++ b/frontend/src/ts/test/events/data.ts @@ -325,18 +325,6 @@ export function getPressedKeys(): Map< return pressedKeys; } -export function getInputEventsForWord(wordIndex: number): InputEventNoMs[] { - const events = getAllTestEvents(); - const result: InputEventNoMs[] = []; - for (const event of events) { - if (event.type !== "input") continue; - if (event.data.wordIndex === 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 7dda6493050f..3b8a14f2aa47 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -1,7 +1,6 @@ import { getAllTestEvents, getInputEvents, - getInputEventsForWord, getInputEventsPerWord, getPressedKeys, logTestEvent, @@ -352,9 +351,16 @@ export function getChars(): CharCounts { ); } -export function getInputForWord(wordIndex: number): string { - const events = getInputEventsForWord(wordIndex); - return getInputFromDom(events).trimEnd(); +export function getInputHistory(): string[] { + const eventsPerWordIndex = getInputEventsPerWord(); + const history: string[] = []; + + for (const events of eventsPerWordIndex.values()) { + const simulatedInput = getInputFromDom(events); + history.push(simulatedInput); + } + + return history; } export function getAccuracy(): { diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 484fc3bb2053..25775d523657 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -14,7 +14,7 @@ import { } from "../../states/notifications"; import * as DDR from "../../utils/ddr"; import * as TestWords from "../test-words"; -import * as TestInput from "../test-input"; +import { getCurrentInput, getInputForWord } from "../test-input"; import * as LayoutfluidFunboxTimer from "./layoutfluid-funbox-timer"; import { highlight } from "../../events/keymap"; import * as MemoryTimer from "./memory-funbox-timer"; @@ -52,17 +52,18 @@ export type FunboxFunctions = { }; async function readAheadHandleKeydown(event: KeyboardEvent): Promise { - const inputCurrentChar = (TestInput.input.current ?? "").slice(-1); + const currentInput = getCurrentInput(); + const inputCurrentChar = (currentInput ?? "").slice(-1); const wordCurrentChar = TestWords.words .getCurrentText() - .slice(TestInput.input.current.length - 1, TestInput.input.current.length); + .slice(currentInput.length - 1, currentInput.length); const isCorrect = inputCurrentChar === wordCurrentChar; if ( event.key === "Backspace" && !isCorrect && - (TestInput.input.current !== "" || - TestInput.input.getHistory(TestState.activeWordIndex - 1) !== + (currentInput !== "" || + getInputForWord(TestState.activeWordIndex - 1) !== TestWords.words.getText(TestState.activeWordIndex - 1) || Config.freedomMode) ) { @@ -452,9 +453,7 @@ const list: Partial> = { } setTimeout(() => { highlight( - TestWords.words - .getCurrentText() - .charAt(TestInput.input.current.length), + TestWords.words.getCurrentText().charAt(getCurrentInput().length), ); }, 1); } diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index 1099d4afd7cf..c89bcce099af 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -5,6 +5,7 @@ import { Config } from "../config/store"; import { setConfig } from "../config/setters"; import * as CustomText from "./custom-text"; import * as TestInput from "./test-input"; +import { getMissedWords, getInputHistory } from "./test-input"; import { configEvent } from "../events/config"; import { Mode } from "@monkeytype/schemas/shared"; import { CustomTextSettings } from "@monkeytype/schemas/results"; @@ -37,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]); } @@ -56,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]); @@ -88,7 +91,7 @@ export function init( if (slow) { const typedWords = TestWords.words .getText() - .slice(0, TestInput.input.getHistory().length - 1); + .slice(0, getInputHistory().length - 1); sortableSlowWords = typedWords.map((e, i) => [ e, diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index d1b0cf76139f..19c45833c1b3 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -547,3 +547,23 @@ export function restart(): void { resetKeypressTimings(); } + +export function getCurrentInput(): string { + return input.current; +} + +export function getInputForWord(wordIndex: number): string | undefined { + return input.get(wordIndex); +} + +export function resetCurrentInput(): void { + input.current = ""; +} + +export function getMissedWords(): MissedWordsType { + return missedWords; +} + +export function getInputHistory(): string[] { + return input.getHistory(); +} diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index ac2da630d78b..3a7c4912a7cb 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -50,6 +50,11 @@ import { } from "../states/test"; import { restartTestEvent } from "../events/test"; import * as TestInput from "./test-input"; +import { + getCurrentInput, + resetCurrentInput, + getInputHistory, +} from "./test-input"; import * as TestWords from "./test-words"; import * as WordsGenerator from "./words-generator"; import * as TestState from "./test-state"; @@ -114,6 +119,7 @@ import { getAfkDuration, forceReleaseAllKeys, getKeypressesPerSecond, + getInputHistory as getEventsInputHistory, } from "./events/stats"; import { calculateWpm } from "../utils/numbers"; import { isDevEnvironment } from "../utils/env"; @@ -447,7 +453,7 @@ async function init(): Promise { TestWords.words.reset(); TestState.setActiveWordIndex(0); TestInput.input.resetHistory(); - TestInput.input.current = ""; + resetCurrentInput(); showLoaderBar(); const { data: language, error } = await tryCatch( @@ -990,7 +996,7 @@ function compareCompletedEvents( } else { if (TestWords.words.list.length <= 25) { notMatching.push( - `charStats (${diffs.join(", ")}) words '${TestWords.words.list.join("_")}' input '${TestInput.input.getHistory().join("_")}'`, + `charStats (${diffs.join(", ")}) words '${TestWords.words.list.join("_")}' input '${getInputHistory().join("_")}'`, ); } else { notMatching.push(`charStats (${diffs.join(", ")})`); @@ -1245,6 +1251,18 @@ function compareCompletedEvents( } } + { + const a = getInputHistory().join(" "); + const b = getEventsInputHistory().join(""); + if (a === b) { + console.debug(`Completed event match on input history:`, a); + } else { + notMatching.push(`input history (values differ)`); + mismatchedKeys.push("inputHistory"); + console.error(`Completed event mismatch on input history:`, a, b); + } + } + if (notMatching.length === 0) { if (ALWAYSREPORT) { showSuccessNotification("Completed events match", { important: true }); @@ -1317,7 +1335,7 @@ function compareCompletedEvents( difficulty: ce.difficulty, duration: ce.testDuration, funboxes: getActiveFunboxNames().join(","), - version: 19, + version: 20, data: { words: TestWords.words.list.join(" "), events: getAllTestEvents(), @@ -1473,16 +1491,16 @@ 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) { + if (getCurrentInput().length !== 0) { TestInput.input.pushHistory(); TestInput.corrected.pushHistory(); - Replay.replayGetWordsList(TestInput.input.getHistory()); + Replay.replayGetWordsList(getInputHistory()); } // in zen mode, ensure the replay words list reflects the typed input history // even if the current input was empty at finish (e.g., after submitting a word). if (Config.mode === "zen") { - Replay.replayGetWordsList(TestInput.input.getHistory()); + Replay.replayGetWordsList(getInputHistory()); } TestInput.forceKeyup(now); //this ensures that the last keypress(es) are registered @@ -1503,7 +1521,7 @@ 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) { + if (TestInput.burstHistory.length !== getInputHistory()?.length) { const burst = TestStats.calculateBurst(now); TestInput.pushBurstToHistory(burst); } @@ -1720,11 +1738,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-stats.ts b/frontend/src/ts/test/test-stats.ts index 0d2885905bc3..6ff0768bcb07 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -1,6 +1,7 @@ import Hangul from "hangul-js"; import { Config } from "../config/store"; import * as TestInput from "./test-input"; +import { getCurrentInput, getInputHistory } from "./test-input"; import * as TestWords from "./test-words"; import * as TestState from "./test-state"; import * as Numbers from "@monkeytype/util/numbers"; @@ -53,11 +54,8 @@ export function getStats(): unknown { accuracy: TestInput.accuracy, keypressTimings: TestInput.keypressTimings, keyOverlap: TestInput.keyOverlap, - wordsHistory: TestWords.words.list.slice( - 0, - TestInput.input.getHistory().length, - ), - inputHistory: TestInput.input.getHistory(), + wordsHistory: TestWords.words.list.slice(0, getInputHistory().length), + inputHistory: getInputHistory(), }; try { @@ -178,8 +176,8 @@ export function calculateBurst(endTime: number = performance.now()): number { if (timeToWrite <= 0) return 0; let wordLength: number; wordLength = !containsKorean - ? TestInput.input.current.length - : Hangul.disassemble(TestInput.input.current).length; + ? getCurrentInput().length + : Hangul.disassemble(getCurrentInput()).length; if (wordLength === 0) { wordLength = !containsKorean ? (TestInput.input.getHistoryLast()?.length ?? 0) @@ -206,20 +204,20 @@ export function removeAfkData(): void { TestInput.rawHistory.splice(testSeconds); } -function getInputWords(isTimedTest: boolean): string[] { +function getInputWords(): string[] { const containsKorean = TestState.koreanStatus; - let inputWords = [...TestInput.input.getHistory()]; + let inputWords = [...getInputHistory()]; if (TestState.isActive) { - inputWords.push(TestInput.input.current); + inputWords.push(getCurrentInput()); } if (containsKorean) { inputWords = inputWords.map((w) => Hangul.disassemble(w).join("")); } - for (let i = 0; i < inputWords.length - (isTimedTest ? 0 : 1); i++) { + for (let i = 0; i < inputWords.length - 1; i++) { if ( getLastChar(inputWords[i] as string) !== "\n" && !isFunboxActiveWithProperty("nospace") @@ -231,19 +229,17 @@ function getInputWords(isTimedTest: boolean): string[] { return inputWords; } -function getTargetWords(isTimedTest: boolean): string[] { +function getTargetWords(): string[] { const containsKorean = TestState.koreanStatus; let targetWords = [ - ...(Config.mode === "zen" - ? TestInput.input.getHistory() - : TestWords.words.list), + ...(Config.mode === "zen" ? getInputHistory() : TestWords.words.list), ]; if (TestState.isActive) { targetWords.push( Config.mode === "zen" - ? TestInput.input.current + ? getCurrentInput() : TestWords.words.getCurrentText(), ); } @@ -252,7 +248,7 @@ function getTargetWords(isTimedTest: boolean): string[] { targetWords = targetWords.map((w) => Hangul.disassemble(w).join("")); } - for (let i = 0; i < targetWords.length - (isTimedTest ? 0 : 1); i++) { + for (let i = 0; i < targetWords.length - 1; i++) { if ( getLastChar(targetWords[i] as string) !== "\n" && !isFunboxActiveWithProperty("nospace") @@ -271,19 +267,25 @@ function countChars(final = false): CharCount { let extraChars = 0; let missedChars = 0; + const inputWords = getInputWords(); + const targetWords = getTargetWords(); + const isTimedTest = Config.mode === "time" || - (Config.mode === "words" && Config.words === 0) || (Config.mode === "custom" && CustomText.getLimit().mode === "time"); - const inputWords = getInputWords(isTimedTest); - const targetWords = getTargetWords(isTimedTest); - for (let i = 0; i < inputWords.length; i++) { const inputWord = inputWords[i] as string; let targetWord = targetWords[i] as string; const isLastInputWord = i === inputWords.length - 1; + // getTargetWords appends a delimiter to every word except the last in the + // generated list; for the last input word (active in timed/mid-test, or + // the actual last word) drop that delimiter so overshoot counts as extra + if (isLastInputWord && targetWord.endsWith(" ")) { + targetWord = targetWord.slice(0, -1); + } + const { correctWord, allCorrect, incorrect, missed, extra } = countCharsUtils( inputWord, diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 3e4af969739e..87ddf50fd0fe 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -8,6 +8,7 @@ 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 { getCurrentInput } from "./test-input"; import * as TestWords from "./test-words"; import * as Monkey from "./monkey"; import * as Numbers from "@monkeytype/util/numbers"; @@ -160,9 +161,7 @@ function layoutfluid(): void { if (Config.keymapMode === "next") { setTimeout(() => { highlight( - TestWords.words - .getCurrentText() - .charAt(TestInput.input.current.length), + TestWords.words.getCurrentText().charAt(getCurrentInput().length), ); }, 1); } diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 84e472a94669..0e6c155d6b1b 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -7,6 +7,11 @@ import { Config } from "../config/store"; import { setConfig } from "../config/setters"; import * as TestWords from "./test-words"; import * as TestInput from "./test-input"; +import { + getCurrentInput, + getInputHistory, + getInputForWord, +} from "./test-input"; import * as CustomText from "./custom-text"; import * as Caret from "./caret"; import * as OutOfFocus from "./out-of-focus"; @@ -696,8 +701,8 @@ export function addWord( // // in case word addition took a long time and some input happened in the mean time // // we need to update word letters for that word // const inputHistory = [ - // ...TestInput.input.getHistory(), - // TestInput.input.current, + // ...getInputHistory(), + // getCurrentInput(), // ]; // const input = inputHistory[wordIndex]; // if (input !== undefined && input !== "") { @@ -1064,7 +1069,7 @@ export async function scrollTape(noAnimation = false): Promise { /* calculate current word width to add to #words margin */ let currentWordWidth = 0; - const inputLength = TestInput.input.current.length; + const inputLength = getCurrentInput().length; if (Config.tapeMode === "letter" && inputLength > 0) { const letters = activeWordEl.qsa("letter"); let lastPositiveLetterWidth = 0; @@ -1285,7 +1290,7 @@ function buildWordLettersHTML( }`; } } else { - if (inputCharacters[c] === TestInput.input.current) { + if (inputCharacters[c] === getCurrentInput()) { out += `${ wordCharacters[c] }`; @@ -1308,9 +1313,9 @@ async function loadWordsHistory(): Promise { const wordsContainer = qs("#resultWordsHistory .words"); wordsContainer?.empty(); - const inputHistoryLength = TestInput.input.getHistory().length; + const inputHistoryLength = getInputHistory().length; for (let i = 0; i < inputHistoryLength + 2; i++) { - const input = TestInput.input.getHistory(i); + const input = getInputForWord(i); const corrected = TestInput.corrected.getHistory(i); const word = TestWords.words.getText(i) ?? ""; const koreanRegex = @@ -1739,7 +1744,7 @@ function afterAnyTestInput( if (Config.keymapMode === "next") { highlight( - TestWords.words.getCurrentText().charAt(TestInput.input.current.length), + TestWords.words.getCurrentText().charAt(getCurrentInput().length), ); } @@ -1760,7 +1765,7 @@ export function afterTestTextInput( if (!increasedWordIndex) { void updateWordLetters({ - input: inputOverride ?? TestInput.input.current, + input: inputOverride ?? getCurrentInput(), wordIndex: TestState.activeWordIndex, compositionData: CompositionState.getData(), }); @@ -1771,7 +1776,7 @@ export function afterTestTextInput( export function afterTestCompositionUpdate(): void { void updateWordLetters({ - input: TestInput.input.current, + input: getCurrentInput(), wordIndex: TestState.activeWordIndex, compositionData: CompositionState.getData(), }); @@ -1781,7 +1786,7 @@ export function afterTestCompositionUpdate(): void { export function afterTestDelete(): void { void updateWordLetters({ - input: TestInput.input.current, + input: getCurrentInput(), wordIndex: TestState.activeWordIndex, compositionData: CompositionState.getData(), }); @@ -1811,7 +1816,7 @@ export function beforeTestWordChange( Config.strictSpace ) { void updateWordLetters({ - input: TestInput.input.current, + input: getCurrentInput(), wordIndex: TestState.activeWordIndex, compositionData: CompositionState.getData(), }); @@ -1932,11 +1937,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); @@ -1945,7 +1950,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(TestInput.missedWords ?? {}).join(" "); } @@ -2048,7 +2053,7 @@ configEvent.subscribe(({ key, newValue }) => { if (key === "highlightMode") { if (getActivePage() === "test") { void updateWordLetters({ - input: TestInput.input.current, + input: getCurrentInput(), wordIndex: TestState.activeWordIndex, compositionData: CompositionState.getData(), }); diff --git a/packages/contracts/src/results.ts b/packages/contracts/src/results.ts index b80b4b13fba1..af591d2746a5 100644 --- a/packages/contracts/src/results.ts +++ b/packages/contracts/src/results.ts @@ -75,7 +75,7 @@ export const ReportCompletedEventMismatchRequestSchema = z.object({ difficulty: DifficultySchema.optional(), duration: z.number().max(200).optional(), funboxes: z.string().max(100).optional(), - version: z.literal(19), + version: z.literal(20), data: z.object({ words: z.string().max(10000), events: z.array(z.record(z.unknown())),