Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 35 additions & 17 deletions frontend/__tests__/input/helpers/validation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterAll } from "vitest";
import {
isCharCorrect,
isWordCorrect,
shouldInsertSpaceCharacter,
} from "../../../src/ts/input/helpers/validation";
import { __testing } from "../../../src/ts/config/testing";
Expand Down Expand Up @@ -75,6 +76,29 @@ describe("isCharCorrect", () => {
});

describe("Space Handling", () => {
it.each([
["returns false in the middle of a word", " ", "wor", "word", false],
["returns false at the start of a word", " ", "", "word", false],
[
"returns false when longer than a word",
" ",
"wordwordword",
"word",
false,
],
])("%s", (_desc, char, input, word, expected) => {
expect(
isCharCorrect({
data: char,
inputValue: input,
targetWord: word,
correctShiftUsed: true,
}),
).toBe(expected);
});
});

describe("Space Handling at the end of a word", () => {
it.each([
["returns true at the end of a correct word", " ", "word", "word", true],
[
Expand All @@ -84,18 +108,23 @@ describe("isCharCorrect", () => {
"word",
false,
],
["returns false in the middle of a word", " ", "wor", "word", false],
["returns false at the start of a word", " ", "", "word", false],
[
"returns false when longer than a word",
" ",
"wordwordword",
"returns true when committing a word with a newline",
"\n",
"word",
"word\n",
true,
],
[
"returns false when committing an incorrect word with a newline",
"\n",
"xord",
"word\n",
false,
],
])("%s", (_desc, char, input, word, expected) => {
expect(
isCharCorrect({
isWordCorrect({
data: char,
inputValue: input,
targetWord: word,
Expand Down Expand Up @@ -124,17 +153,6 @@ describe("isCharCorrect", () => {
},
);
});

it("throws error if data is undefined", () => {
expect(() =>
isCharCorrect({
data: undefined as any,
inputValue: "val",
targetWord: "word",
correctShiftUsed: true,
}),
).toThrow();
});
});

describe("shouldInsertSpaceCharacter", () => {
Expand Down
40 changes: 30 additions & 10 deletions frontend/src/ts/input/handlers/insert-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { goToNextWord } from "../helpers/word-navigation";
import { onBeforeInsertText } from "./before-insert-text";
import {
isCharCorrect,
isWordCorrect,
shouldInsertSpaceCharacter,
} from "../helpers/validation";
import { getCurrentInput, logTestEvent } from "../../test/events/data";
Expand Down Expand Up @@ -134,7 +135,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise<void> {
data,
currentWord[(testInput + data).length - 1] ?? "",
);
const correct =
const charCorrect =
funboxCorrect ??
isCharCorrect({
data,
Expand All @@ -143,6 +144,26 @@ export async function onInsertText(options: OnInsertTextParams): Promise<void> {
correctShiftUsed,
});

// word navigation check
const noSpaceForce =
isFunboxActiveWithProperty("nospace") &&
(testInput + data).length === TestWords.words.getCurrentText().length;
// 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;

// handing cases where last char needs to be removed
// this is here and not in beforeInsertText because we want to penalize for incorrect spaces
// like accuracy, keypress errors, and missed words
Expand All @@ -155,7 +176,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise<void> {
removeLastChar = true;
}

if (!isSpace(data) && correctShiftUsed === false) {
if (!charIsSpace && correctShiftUsed === false) {
removeLastChar = true;
visualInputOverride = undefined;
incrementIncorrectShiftsInARow();
Expand All @@ -169,13 +190,8 @@ export async function onInsertText(options: OnInsertTextParams): Promise<void> {
resetIncorrectShiftsInARow();
}

// word navigation check
const noSpaceForce =
isFunboxActiveWithProperty("nospace") &&
(testInput + data).length === TestWords.words.getCurrentText().length;
const shouldGoToNextWord =
!removeLastChar &&
(((charIsSpace || charIsNewline) && !shouldInsertSpace) || noSpaceForce);
// stop-on-error and opposite shift mode can block navigation, so this is derived after removeLastChar
const shouldGoToNextWord = goingToNextWord && !removeLastChar;

if (Config.keymapMode === "react") {
flash(data, correct);
Expand Down Expand Up @@ -209,12 +225,16 @@ export async function onInsertText(options: OnInsertTextParams): Promise<void> {
// 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: correct,
correctInsert: commitCorrect,
isCompositionEnding: isCompositionEnding === true,
zenNewline: charIsNewline && Config.mode === "zen",
now,
Expand Down
35 changes: 22 additions & 13 deletions frontend/src/ts/input/helpers/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,37 @@ export function isCharCorrect(options: {
const { data, inputValue, targetWord, correctShiftUsed } = options;

if (Config.mode === "zen") return true;

if (correctShiftUsed === false) return false;

if (data === undefined) {
throw new Error("Failed to check if char is correct - data is undefined");
}

if (isSpace(data)) {
return inputValue === targetWord;
}

const targetChar = targetWord[inputValue.length];

if (targetChar === undefined) {
return false;
}

if (data === targetChar) {
return true;
}
return data === targetChar;
}

/**
* Check if the input data is correct
* @param options - Options object
* @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
*/
export function isWordCorrect(options: {
data: string;
inputValue: string;
targetWord: string;
correctShiftUsed: boolean | null; //null means disabled
}): boolean {
const { data, inputValue, targetWord, correctShiftUsed } = options;

if (Config.mode === "zen") return true;
if (correctShiftUsed === false) return false;

return false;
const finalInputValue = inputValue + (isSpace(data) ? "" : data);
return finalInputValue === targetWord;
}

/**
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/ts/test/events/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,26 @@ export function getCurrentInput(): string {
return getInputFromDom(getEventsForWord(getAllTestEvents(), activeWordIndex));
}

// Like getCurrentInput, but strips the trailing space when the last input
// event committed the word (advanced to the next word) rather than inserting
// a literal space. The committing space is a word separator, not part of the
// word's input, so the UI must not render it. Newline commits are kept
// since the newline is visible content.
export function getCurrentInputForDisplay(): string {
const input = getCurrentInput();
const last = inputEvents[inputEvents.length - 1];
if (
last !== undefined &&
last.data.wordIndex === activeWordIndex &&
"commitsWord" in last.data &&
last.data.commitsWord === true &&
last.data.data === " "
) {
return input.slice(0, -1);
}
return input;
}

export function getInputForWord(wordIndex: number): string {
return getInputFromDom(
getEventsForWord(getAllTestEvents(), wordIndex),
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/ts/test/test-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { Config } from "../config/store";
import { setConfig } from "../config/setters";
import * as TestWords from "./test-words";
import { getCurrentInput } from "./events/data";
import { getCurrentInput, getCurrentInputForDisplay } from "./events/data";
import { getLiveCachedAccuracy } from "./events/live-cache";
import * as CustomText from "./custom-text";
import * as Caret from "./caret";
Expand Down Expand Up @@ -1789,7 +1789,7 @@ export function afterTestTextInput(

if (!increasedWordIndex) {
void updateWordLetters({
input: inputOverride ?? getCurrentInput(),
input: inputOverride ?? getCurrentInputForDisplay(),
wordIndex: TestState.activeWordIndex,
compositionData: CompositionState.getData(),
});
Expand All @@ -1800,7 +1800,7 @@ export function afterTestTextInput(

export function afterTestCompositionUpdate(): void {
void updateWordLetters({
input: getCurrentInput(),
input: getCurrentInputForDisplay(),
wordIndex: TestState.activeWordIndex,
compositionData: CompositionState.getData(),
});
Expand All @@ -1810,7 +1810,7 @@ export function afterTestCompositionUpdate(): void {

export function afterTestDelete(): void {
void updateWordLetters({
input: getCurrentInput(),
input: getCurrentInputForDisplay(),
wordIndex: TestState.activeWordIndex,
compositionData: CompositionState.getData(),
});
Expand Down Expand Up @@ -1839,7 +1839,7 @@ export function beforeTestWordChange(
forceUpdateActiveWordLetters
) {
void updateWordLetters({
input: getCurrentInput().trimEnd(),
input: getCurrentInputForDisplay(),
wordIndex: TestState.activeWordIndex,
compositionData: CompositionState.getData(),
});
Expand Down Expand Up @@ -2078,7 +2078,7 @@ configEvent.subscribe(({ key, newValue }) => {
if (key === "highlightMode") {
if (getActivePage() === "test") {
void updateWordLetters({
input: getCurrentInput(),
input: getCurrentInputForDisplay(),
wordIndex: TestState.activeWordIndex,
compositionData: CompositionState.getData(),
});
Expand Down
Loading