diff --git a/frontend/__tests__/input/helpers/fail-or-finish.spec.ts b/frontend/__tests__/input/helpers/fail-or-finish.spec.ts index 0d9faaaaf21f..3e10cb0d8a53 100644 --- a/frontend/__tests__/input/helpers/fail-or-finish.spec.ts +++ b/frontend/__tests__/input/helpers/fail-or-finish.spec.ts @@ -139,7 +139,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "zen mode, master - never fails", config: { mode: "zen", difficulty: "master" }, correct: false, - spaceOrNewline: true, + isCommitChar: true, input: "hello", expected: false, }, @@ -147,7 +147,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "zen mode - never fails", config: { mode: "zen", difficulty: "normal" }, correct: false, - spaceOrNewline: true, + isCommitChar: true, input: "hello", expected: false, }, @@ -156,7 +156,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "normal typing incorrect- never fails", config: { difficulty: "normal" }, correct: false, - spaceOrNewline: false, + isCommitChar: false, input: "hello", expected: false, }, @@ -164,7 +164,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "normal typing space incorrect - never fails", config: { difficulty: "normal" }, correct: false, - spaceOrNewline: true, + isCommitChar: true, input: "hello", expected: false, }, @@ -172,7 +172,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "normal typing correct - never fails", config: { difficulty: "normal" }, correct: true, - spaceOrNewline: false, + isCommitChar: false, input: "hello", expected: false, }, @@ -180,7 +180,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "normal typing space correct - never fails", config: { difficulty: "normal" }, correct: true, - spaceOrNewline: true, + isCommitChar: true, input: "hello", expected: false, }, @@ -189,7 +189,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "expert - fail if incorrect space", config: { difficulty: "expert" }, correct: false, - spaceOrNewline: true, + isCommitChar: true, input: "he", expected: true, }, @@ -197,7 +197,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "expert - dont fail if space is the first character", config: { difficulty: "expert" }, correct: false, - spaceOrNewline: true, + isCommitChar: true, input: " ", expected: false, }, @@ -205,7 +205,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "expert: - dont fail if just typing", config: { difficulty: "expert" }, correct: false, - spaceOrNewline: false, + isCommitChar: false, input: "h", expected: false, }, @@ -213,7 +213,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "expert: - dont fail if just typing", config: { difficulty: "expert" }, correct: true, - spaceOrNewline: false, + isCommitChar: false, input: "h", expected: false, }, @@ -222,7 +222,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "master - fail if incorrect char", config: { difficulty: "master" }, correct: false, - spaceOrNewline: false, + isCommitChar: false, input: "h", expected: true, }, @@ -230,7 +230,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "master - fail if incorrect first space", config: { difficulty: "master" }, correct: true, - spaceOrNewline: true, + isCommitChar: true, input: " ", expected: false, }, @@ -238,7 +238,7 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "master - dont fail if correct char", config: { difficulty: "master" }, correct: true, - spaceOrNewline: false, + isCommitChar: false, input: "a", expected: false, }, @@ -246,16 +246,16 @@ describe("checkIfFailedDueToDifficulty", () => { desc: "master - dont fail if correct space", config: { difficulty: "master" }, correct: true, - spaceOrNewline: true, + isCommitChar: true, input: " ", expected: false, }, - ])("$desc", ({ config, correct, spaceOrNewline, input, expected }) => { + ])("$desc", ({ config, correct, isCommitChar, input, expected }) => { replaceConfig(config as any); const result = checkIfFailedDueToDifficulty({ testInputWithData: input, correct, - spaceOrNewline, + isCommitChar, }); expect(result).toBe(expected); }); diff --git a/frontend/__tests__/input/helpers/validation.spec.ts b/frontend/__tests__/input/helpers/validation.spec.ts index 681394cb863e..39d400403459 100644 --- a/frontend/__tests__/input/helpers/validation.spec.ts +++ b/frontend/__tests__/input/helpers/validation.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterAll } from "vitest"; import { isCharCorrect, isWordCorrect, - shouldInsertSpaceCharacter, + shouldJumpToNextWord, } from "../../../src/ts/input/helpers/validation"; import { __testing } from "../../../src/ts/config/testing"; import * as FunboxList from "../../../src/ts/test/funbox/list"; @@ -13,6 +13,7 @@ const { replaceConfig } = __testing; // Mock dependencies vi.mock("../../../src/ts/test/funbox/list", () => ({ findSingleActiveFunboxWithFunction: vi.fn(), + isFunboxActiveWithProperty: vi.fn(), })); vi.mock("../../../src/ts/utils/strings", async () => { @@ -155,7 +156,7 @@ describe("isCharCorrect", () => { }); }); -describe("shouldInsertSpaceCharacter", () => { +describe("shouldJumpToNextWord", () => { beforeEach(() => { replaceConfig({ mode: "time", @@ -169,20 +170,10 @@ describe("shouldInsertSpaceCharacter", () => { replaceConfig({}); }); - it("returns null if data is not a space", () => { - expect( - shouldInsertSpaceCharacter({ - data: "a", - inputValue: "test", - targetWord: "test", - }), - ).toBe(null); - }); - it("returns false in zen mode", () => { replaceConfig({ mode: "zen" }); expect( - shouldInsertSpaceCharacter({ + shouldJumpToNextWord({ data: " ", inputValue: "test", targetWord: "test", @@ -202,7 +193,7 @@ describe("shouldInsertSpaceCharacter", () => { strictSpace: false, difficulty: "normal", }, - expected: false, + expected: true, }, { desc: "submit incorrect word (stopOnError off)", @@ -213,11 +204,11 @@ describe("shouldInsertSpaceCharacter", () => { strictSpace: false, difficulty: "normal", }, - expected: false, + expected: true, }, // Stop on error { - desc: "insert space if incorrect (stopOnError letter)", + desc: "not submit if incorrect (stopOnError letter)", inputValue: "hel", targetWord: "hello", config: { @@ -225,10 +216,10 @@ describe("shouldInsertSpaceCharacter", () => { strictSpace: false, difficulty: "normal", }, - expected: true, + expected: false, }, { - desc: "insert space if incorrect (stopOnError word)", + desc: "not submit if incorrect (stopOnError word)", inputValue: "hel", targetWord: "hello", config: { @@ -236,7 +227,7 @@ describe("shouldInsertSpaceCharacter", () => { strictSpace: false, difficulty: "normal", }, - expected: true, + expected: false, }, { desc: "submit if correct (stopOnError letter)", @@ -247,11 +238,11 @@ describe("shouldInsertSpaceCharacter", () => { strictSpace: false, difficulty: "normal", }, - expected: false, + expected: true, }, // Strict space / Difficulty { - desc: "insert space if empty input (strictSpace on)", + desc: "not submit if empty input (strictSpace on)", inputValue: "", targetWord: "hello", config: { @@ -259,10 +250,10 @@ describe("shouldInsertSpaceCharacter", () => { strictSpace: true, difficulty: "normal", }, - expected: true, + expected: false, }, { - desc: "insert space if empty input (difficulty not normal - expert or master)", + desc: "not submit if empty input (difficulty not normal - expert or master)", inputValue: "", targetWord: "hello", config: { @@ -270,7 +261,7 @@ describe("shouldInsertSpaceCharacter", () => { strictSpace: false, difficulty: "expert", }, - expected: true, + expected: false, }, { desc: "submit if not empty input (strictSpace on)", @@ -281,12 +272,12 @@ describe("shouldInsertSpaceCharacter", () => { strictSpace: true, difficulty: "normal", }, - expected: false, + expected: true, }, ])("$desc", ({ inputValue, targetWord, config, expected }) => { replaceConfig(config as any); expect( - shouldInsertSpaceCharacter({ + shouldJumpToNextWord({ data: " ", inputValue, targetWord, diff --git a/frontend/src/ts/input/handlers/before-insert-text.ts b/frontend/src/ts/input/handlers/before-insert-text.ts index 3c5f56419e6e..ded765fc89c4 100644 --- a/frontend/src/ts/input/handlers/before-insert-text.ts +++ b/frontend/src/ts/input/handlers/before-insert-text.ts @@ -7,7 +7,7 @@ import { isFunboxActiveWithProperty } from "../../test/funbox/list"; import { isSpace } from "../../utils/strings"; import { getInputElementValue } from "../input-element"; import { isAwaitingNextWord } from "../state"; -import { shouldInsertSpaceCharacter } from "../helpers/validation"; +import { shouldJumpToNextWord } from "../helpers/validation"; import * as SlowTimer from "../../legacy-states/slow-timer"; import { wordsHaveNewline } from "../../states/test"; @@ -31,11 +31,6 @@ export function onBeforeInsertText(data: string): boolean { const { inputValue } = getInputElementValue(); const dataIsSpace = isSpace(data); - const shouldInsertSpaceAsCharacter = shouldInsertSpaceCharacter({ - data, - inputValue, - targetWord: TestWords.words.getCurrentText(), - }); //prevent space from being inserted if input is empty //allow if strict space is enabled @@ -58,11 +53,17 @@ export function onBeforeInsertText(data: string): boolean { return true; } + const shouldGoToNextWord = shouldJumpToNextWord({ + data, + inputValue, + targetWord: TestWords.words.getCurrentText(), + }); + // block input if the word is too long const inputLimit = Config.mode === "zen" ? 30 : TestWords.words.getCurrentText().length + 20; const overLimit = getCurrentInput().length >= inputLimit; - if (overLimit && (shouldInsertSpaceAsCharacter === true || !dataIsSpace)) { + if (overLimit && !shouldGoToNextWord) { console.error("Hitting word limit"); return true; } @@ -79,7 +80,7 @@ export function onBeforeInsertText(data: string): boolean { !Config.blindMode && !Config.hideExtraLetters && inputIsLongerThanOrEqualToWord && - (shouldInsertSpaceAsCharacter === true || !dataIsSpace) && + !shouldGoToNextWord && Config.mode !== "zen" ) { // make sure to only check this when really necessary diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index 70ae26173981..ab33aced985c 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -11,13 +11,10 @@ import { checkIfFailedDueToMinBurst, checkIfFinished, } from "../helpers/fail-or-finish"; -import { areCharactersVisuallyEqual, isSpace } from "../../utils/strings"; +import { areCharactersVisuallyEqual } from "../../utils/strings"; import * as TestState from "../../test/test-state"; import * as TestLogic from "../../test/test-logic"; -import { - findSingleActiveFunboxWithFunction, - isFunboxActiveWithProperty, -} from "../../test/funbox/list"; +import { findSingleActiveFunboxWithFunction } from "../../test/funbox/list"; import { Config } from "../../config/store"; import { flash } from "../../events/keymap"; import * as WeakSpot from "../../test/weak-spot"; @@ -34,7 +31,8 @@ import { onBeforeInsertText } from "./before-insert-text"; import { isCharCorrect, isWordCorrect, - shouldInsertSpaceCharacter, + shouldJumpToNextWord, + isCommitCharacter, } from "../helpers/validation"; import { getCurrentInput, logTestEvent } from "../../test/events/data"; @@ -117,14 +115,6 @@ export async function onInsertText(options: OnInsertTextParams): Promise { const lastInMultiOrSingle = lastInMultiIndex === true || lastInMultiIndex === undefined; const wordIndex = TestState.activeWordIndex; - const charIsSpace = isSpace(data); - const charIsNewline = data === "\n"; - const shouldInsertSpace = - shouldInsertSpaceCharacter({ - data, - inputValue: testInput, - targetWord: currentWord, - }) === true; const correctShiftUsed = Config.oppositeShiftMode === "off" ? null : isCorrectShiftUsed(); @@ -135,34 +125,35 @@ export async function onInsertText(options: OnInsertTextParams): Promise { data, currentWord[(testInput + data).length - 1] ?? "", ); - const charCorrect = - funboxCorrect ?? - isCharCorrect({ - data, - inputValue: testInput, - targetWord: currentWord, - correctShiftUsed, - }); - // word navigation check - const noSpaceForce = - isFunboxActiveWithProperty("nospace") && - (testInput + data).length === TestWords.words.getCurrentText().length; + const isCommitChar = isCommitCharacter({ data, inputValue: testInput }); + + const wordCorrect = isWordCorrect({ + data, + inputValue: testInput, + targetWord: currentWord, + correctShiftUsed, + }); + // does this input try to move to the next word (before removeLastChar can block it) - const goingToNextWord = - ((charIsSpace || charIsNewline) && !shouldInsertSpace) || noSpaceForce; - - // when moving to the next word, correctness is word-level (a correct word-completing - // space has charCorrect === false, so charCorrect can't be used below) - const correct = goingToNextWord - ? (funboxCorrect ?? - isWordCorrect({ - data, - inputValue: testInput, - targetWord: currentWord, - correctShiftUsed, - })) - : charCorrect; + const goingToNextWord = shouldJumpToNextWord({ + data, + inputValue: testInput, + targetWord: currentWord, + isCommitChar, + wordCorrect, + }); + + const correct = + funboxCorrect ?? + (goingToNextWord || wordCorrect + ? wordCorrect + : isCharCorrect({ + data, + inputValue: testInput, + targetWord: currentWord, + correctShiftUsed, + })); // handing cases where last char needs to be removed // this is here and not in beforeInsertText because we want to penalize for incorrect spaces @@ -176,7 +167,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { removeLastChar = true; } - if (!charIsSpace && correctShiftUsed === false) { + if (correctShiftUsed === false) { removeLastChar = true; visualInputOverride = undefined; incrementIncorrectShiftsInARow(); @@ -225,18 +216,14 @@ export async function onInsertText(options: OnInsertTextParams): Promise { // this needs to be called after event logging WeakSpot.updateScore(data, correct); - const commitCorrect = noSpaceForce - ? testInput + data === currentWord - : correct; - // going to next word let increasedWordIndex: null | boolean = null; let lastBurst: null | number = null; if (shouldGoToNextWord) { const result = await goToNextWord({ - correctInsert: commitCorrect, + correctInsert: correct, isCompositionEnding: isCompositionEnding === true, - zenNewline: charIsNewline && Config.mode === "zen", + zenNewline: data === "\n" && Config.mode === "zen", now, }); lastBurst = result.lastBurst; @@ -245,10 +232,6 @@ export async function onInsertText(options: OnInsertTextParams): Promise { /* Probably a good place to explain what the heck is going on with all these space related variables: - - spaceOrNewLine: did the user input a space or a new line? - - shouldInsertSpace: should space be treated as a character, or should it move us to the next word - monkeytype doesnt actually have space characters in words, so we need this distinction - and also moving to the next word might get blocked by things like stop on error - shouldGoToNextWord: IF input is space and we DONT insert a space CHARACTER, we will TRY to go to the next word - increasedWordIndex: the only reason this is here because on the last word we dont move to the next word */ @@ -274,8 +257,13 @@ export async function onInsertText(options: OnInsertTextParams): Promise { if ( checkIfFailedDueToDifficulty({ testInputWithData: testInput + data, - correct, - spaceOrNewline: charIsSpace || charIsNewline, + // We need to use `wordCorrect` here instead of `correct`, because when stop on + // error = word and nospace is enabled and difficulty = expert, submitting an incorrect + // word will do letter comparison when calculating `correct` (as we aren't moving + // to the next word because of stop on error), but we always want to do word + // comparison for expert mode. + correct: wordCorrect, + isCommitChar, }) ) { TestLogic.fail("difficulty"); diff --git a/frontend/src/ts/input/helpers/fail-or-finish.ts b/frontend/src/ts/input/helpers/fail-or-finish.ts index 6c096a42c10b..22f6f2f551fc 100644 --- a/frontend/src/ts/input/helpers/fail-or-finish.ts +++ b/frontend/src/ts/input/helpers/fail-or-finish.ts @@ -38,23 +38,21 @@ export function checkIfFailedDueToMinBurst(options: { * @param options - Options object * @param options.testInputWithData - Current test input result (after adding data) * @param options.correct - Was the last input correct - * @param options.spaceOrNewline - Is the input a space or newline + * @param options.isCommitChar - Whether the entered character commits the current word */ export function checkIfFailedDueToDifficulty(options: { testInputWithData: string; correct: boolean; - spaceOrNewline: boolean; + isCommitChar: boolean; }): boolean { - const { testInputWithData, correct, spaceOrNewline } = options; - // Using space or newline instead of shouldInsertSpace or increasedWordIndex - // because we want expert mode to fail no matter if confidence or stop on error is on + const { testInputWithData, correct, isCommitChar } = options; if (Config.mode === "zen") return false; const shouldFailDueToExpert = Config.difficulty === "expert" && !correct && - spaceOrNewline && + isCommitChar && testInputWithData.length > 1; const shouldFailDueToMaster = Config.difficulty === "master" && !correct; diff --git a/frontend/src/ts/input/helpers/validation.ts b/frontend/src/ts/input/helpers/validation.ts index a2712335b3b2..7f187f7e4fc4 100644 --- a/frontend/src/ts/input/helpers/validation.ts +++ b/frontend/src/ts/input/helpers/validation.ts @@ -1,5 +1,7 @@ import { Config } from "../../config/store"; import { isSpace } from "../../utils/strings"; +import { isFunboxActiveWithProperty } from "../../test/funbox/list"; +import * as TestWords from "../../test/test-words"; /** * Check if the input data is correct @@ -32,6 +34,7 @@ export function isCharCorrect(options: { /** * Check if the input data is correct * @param options - Options object + * @param options.data - Input data * @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 @@ -52,35 +55,74 @@ export function isWordCorrect(options: { } /** - * Determines if a space character should be inserted as a character, or act - * as a "control character" (moving to the next word) + * Determines if a character should commit the current word + * @param options - Options object + * @param options.data - Input data + * @param options.inputValue - Current input value (use getCurrentInput(), not input element value) + */ +export function isCommitCharacter(options: { + data: string; + inputValue: string; +}): boolean { + const { data, inputValue } = options; + + const charIsSpace = isSpace(data); + const charIsNewline = data === "\n"; + const noSpaceForce = + isFunboxActiveWithProperty("nospace") && + (inputValue + data).length === TestWords.words.getCurrentText().length; + + return charIsSpace || charIsNewline || noSpaceForce; +} + +/** + * Determines if we should move on to the next word or not. * @param options - Options object * @param options.data - Input data * @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 + * @param options.isCommitChar - Whether this character commits the current word + * @param options.wordCorrect - Whether the current word is correct */ -export function shouldInsertSpaceCharacter(options: { +export function shouldJumpToNextWord(options: { data: string; inputValue: string; targetWord: string; -}): boolean | null { - const { data, inputValue, targetWord } = options; - if (!isSpace(data)) { - return null; - } + isCommitChar?: boolean; + wordCorrect?: boolean; +}): boolean { + const { + data, + inputValue, + targetWord, + isCommitChar = isCommitCharacter({ data, inputValue }), + wordCorrect = isWordCorrect({ + data, + inputValue, + targetWord, + correctShiftUsed: null, + }), + } = options; + if (Config.mode === "zen") { return false; } - const correctSoFar = `${targetWord} `.startsWith(`${inputValue} `); + const stopOnErrorLetterAndIncorrect = - Config.stopOnError === "letter" && !correctSoFar; + Config.stopOnError === "letter" && !wordCorrect; const stopOnErrorWordAndIncorrect = - Config.stopOnError === "word" && !correctSoFar; + Config.stopOnError === "word" && !wordCorrect; const strictSpace = + isSpace(data) && inputValue.length === 0 && (Config.strictSpace || Config.difficulty !== "normal"); + return ( - stopOnErrorLetterAndIncorrect || stopOnErrorWordAndIncorrect || strictSpace + isCommitChar && + !( + stopOnErrorLetterAndIncorrect || + stopOnErrorWordAndIncorrect || + strictSpace + ) ); }