diff --git a/frontend/__tests__/input/handlers/insert-text.spec.ts b/frontend/__tests__/input/handlers/insert-text.spec.ts index 5b961698d3e5..9868c1469ca3 100644 --- a/frontend/__tests__/input/handlers/insert-text.spec.ts +++ b/frontend/__tests__/input/handlers/insert-text.spec.ts @@ -30,6 +30,8 @@ vi.mock("../../../src/ts/input/input-element", () => ({ const mockState = vi.hoisted(() => ({ activeWordIndex: 0, correctShiftUsed: true as boolean, + // words that have scrolled off the screen and been removed from the dom + wordsScrolledOff: new Set(), })); const nav = vi.hoisted(() => ({ @@ -94,6 +96,10 @@ vi.mock("../../../src/ts/test/custom-text", () => ({ // peripheral collaborators - none of them feed back into the events we assert vi.mock("../../../src/ts/test/test-ui", () => ({ afterTestTextInput: vi.fn(), + // words scrolled off the screen are removed from the dom + getWordElement: vi.fn((index: number) => + mockState.wordsScrolledOff.has(index) ? null : {}, + ), pendingWordData: new Map(), })); vi.mock("../../../src/ts/test/test-logic", () => ({ @@ -207,6 +213,7 @@ describe("onInsertText - delete on error", () => { TestWords.reset(); mockState.activeWordIndex = 0; mockState.correctShiftUsed = true; + mockState.wordsScrolledOff.clear(); setInput(""); replaceConfig({ mode: "words", @@ -343,6 +350,20 @@ describe("onInsertText - delete on error", () => { expect(deletesForWord(0)).toEqual([["deleteContentBackward", 1, ""]]); }); + it("does not regress into a word that scrolled off the screen", async () => { + replaceConfig({ deleteOnError: "letter_hard", stopOnError: "off" }); + pushWords("hello", "world"); + for (const char of "hello ") await type(char); + mockState.wordsScrolledOff.add(0); + + await type("x"); + + expect(nav.goToPreviousWord).not.toHaveBeenCalled(); + expect(mockState.activeWordIndex).toBe(1); + expect(deletesForWord(1)).toEqual([["deleteContentBackward", 1, ""]]); + expect(getInput()).toBe(""); + }); + it("does not regress on a mistake later in the word", async () => { replaceConfig({ deleteOnError: "letter_hard", stopOnError: "off" }); pushWords("hello", "world"); diff --git a/frontend/src/ts/input/handlers/before-insert-text.ts b/frontend/src/ts/input/handlers/before-insert-text.ts index 3f5bbe7adc1e..2bf264e8c25f 100644 --- a/frontend/src/ts/input/handlers/before-insert-text.ts +++ b/frontend/src/ts/input/handlers/before-insert-text.ts @@ -101,6 +101,7 @@ export function onBeforeInsertText(data: string): boolean { dataIsNotFalsy && !Config.blindMode && !Config.hideExtraLetters && + !Config.deleteOnError.includes("hard") && inputIsLongerThanOrEqualToWord && !goingToNextWord && Config.mode !== "zen" diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index 82b4ba637bf8..104f381c7822 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -109,7 +109,14 @@ function handleDeleteOnError(now: number): void { } //mistake on the first character of the word - the hard modes send you back - if (goBackAWord && inputLength <= 1 && getActiveWordIndex() > 0) { + //but only if the previous word is still in the dom (it might have scrolled + //off), same check as the one a normal backspace does in onBeforeDelete + if ( + goBackAWord && + inputLength <= 1 && + getActiveWordIndex() > 0 && + TestUI.getWordElement(getActiveWordIndex() - 1) !== null + ) { //pretend its a normal backspace, not insertText const inputType: DeleteInputType = deleteWholeWord ? "deleteWordBackward" diff --git a/package.json b/package.json index c38cf5fce706..77201f92a549 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monkeytype", - "version": "26.28.0", + "version": "26.32.0", "private": true, "license": "GPL-3.0", "type": "module",