-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
fix(test): recover pace caret after word generation gaps (@anuragkej) #7560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { __testing as ConfigTesting } from "../../src/ts/config"; | ||
| import * as PaceCaret from "../../src/ts/test/pace-caret"; | ||
| import * as TestState from "../../src/ts/test/test-state"; | ||
| import * as TestWords from "../../src/ts/test/test-words"; | ||
|
|
||
| describe("pace-caret", () => { | ||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| ConfigTesting.replaceConfig({ | ||
| paceCaret: "custom", | ||
| paceCaretCustomSpeed: 100, | ||
| blindMode: false, | ||
| }); | ||
| TestState.setActive(true); | ||
| TestState.setResultVisible(false); | ||
| TestWords.words.reset(); | ||
| PaceCaret.reset(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| PaceCaret.reset(); | ||
| TestWords.words.reset(); | ||
| TestState.setActive(false); | ||
| TestState.setResultVisible(false); | ||
| vi.clearAllTimers(); | ||
| vi.useRealTimers(); | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it("recovers when the pace caret reaches words that are not generated yet", async () => { | ||
| TestWords.words.push("alpha", 0); | ||
| await PaceCaret.init(); | ||
|
|
||
| const initialSettings = PaceCaret.settings; | ||
| expect(initialSettings).not.toBeNull(); | ||
| if (initialSettings === null) { | ||
| throw new Error("Pace caret settings were not initialized"); | ||
| } | ||
|
|
||
| const showMock = vi.spyOn(PaceCaret.caret, "show"); | ||
| const hideMock = vi.spyOn(PaceCaret.caret, "hide"); | ||
| vi.spyOn(PaceCaret.caret, "isHidden").mockReturnValue(true); | ||
| vi.spyOn(PaceCaret.caret, "goTo").mockImplementation(() => undefined); | ||
|
|
||
| const endOfFirstWord = TestWords.words.get(0)?.length ?? 1; | ||
| initialSettings.currentWordIndex = 0; | ||
| initialSettings.currentLetterIndex = endOfFirstWord; | ||
| initialSettings.correction = 1; | ||
|
|
||
| await PaceCaret.update(0); | ||
|
|
||
| expect(PaceCaret.settings).not.toBeNull(); | ||
| expect(PaceCaret.settings?.currentWordIndex).toBe(0); | ||
| expect(PaceCaret.settings?.currentLetterIndex).toBe(endOfFirstWord); | ||
| expect(PaceCaret.settings?.correction).toBe(1); | ||
| expect(hideMock).toHaveBeenCalled(); | ||
| expect(showMock).not.toHaveBeenCalled(); | ||
|
|
||
| if (PaceCaret.settings !== null) { | ||
| PaceCaret.settings.correction = 0; | ||
| } | ||
| TestWords.words.push("beta", 1); | ||
| await PaceCaret.update(0); | ||
|
|
||
| expect(PaceCaret.settings?.currentWordIndex).toBe(1); | ||
| expect(PaceCaret.settings?.currentLetterIndex).toBe(0); | ||
| expect(showMock).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it("stops retrying when no additional words can be generated", async () => { | ||
| ConfigTesting.replaceConfig({ | ||
| paceCaret: "custom", | ||
| paceCaretCustomSpeed: 100, | ||
| blindMode: false, | ||
| mode: "words", | ||
| words: 1, | ||
| }); | ||
| TestWords.words.push("alpha", 0); | ||
| await PaceCaret.init(); | ||
|
|
||
| const currentSettings = PaceCaret.settings; | ||
| expect(currentSettings).not.toBeNull(); | ||
| if (currentSettings === null) { | ||
| throw new Error("Pace caret settings were not initialized"); | ||
| } | ||
|
|
||
| vi.spyOn(PaceCaret.caret, "isHidden").mockReturnValue(true); | ||
| vi.spyOn(PaceCaret.caret, "goTo").mockImplementation(() => undefined); | ||
|
|
||
| const endOfFirstWord = TestWords.words.get(0)?.length ?? 1; | ||
| currentSettings.currentWordIndex = 0; | ||
| currentSettings.currentLetterIndex = endOfFirstWord; | ||
| currentSettings.correction = 1; | ||
|
|
||
| await PaceCaret.update(0); | ||
|
|
||
| expect(vi.getTimerCount()).toBe(0); | ||
| expect(PaceCaret.settings).toBeNull(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ import Config from "../config"; | |
| import * as DB from "../db"; | ||
| import * as Misc from "../utils/misc"; | ||
| import * as TestState from "./test-state"; | ||
| import * as CustomText from "./custom-text"; | ||
| import * as WordsGenerator from "./words-generator"; | ||
| import * as ConfigEvent from "../observables/config-event"; | ||
| import { getActiveFunboxes } from "./funbox/list"; | ||
| import { Caret } from "../utils/caret"; | ||
|
|
@@ -139,12 +141,26 @@ export async function update(expectedStepEnd: number): Promise<void> { | |
| return; | ||
| } | ||
|
|
||
| const nextExpectedStepEnd = | ||
| expectedStepEnd + (currentSettings.spc ?? 0) * 1000; | ||
|
|
||
| if (!incrementLetterIndex()) { | ||
| if (shouldRetryWhenWordsMayStillGenerate(currentSettings)) { | ||
| scheduleUpdate( | ||
| currentSettings, | ||
| nextExpectedStepEnd, | ||
| Math.max(16, (currentSettings.spc ?? 0) * 1000), | ||
| ); | ||
| } else { | ||
| settings = null; | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (caret.isHidden()) { | ||
| caret.show(); | ||
| } | ||
|
|
||
| incrementLetterIndex(); | ||
|
|
||
| try { | ||
| const now = performance.now(); | ||
| const absoluteStepEnd = startTimestamp + expectedStepEnd; | ||
|
|
@@ -162,24 +178,65 @@ export async function update(expectedStepEnd: number): Promise<void> { | |
| }, | ||
| }); | ||
|
|
||
| currentSettings.timeout = setTimeout( | ||
| () => { | ||
| if (settings !== currentSettings) return; | ||
| update(expectedStepEnd + (currentSettings.spc ?? 0) * 1000).catch( | ||
| () => { | ||
| if (settings === currentSettings) settings = null; | ||
| }, | ||
| ); | ||
| }, | ||
| Math.max(0, duration), | ||
| ); | ||
| scheduleUpdate(currentSettings, nextExpectedStepEnd, Math.max(0, duration)); | ||
| } catch (e) { | ||
| console.error(e); | ||
| caret.hide(); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| function scheduleUpdate( | ||
| currentSettings: Settings, | ||
| nextExpectedStepEnd: number, | ||
| delay: number, | ||
| ): void { | ||
| currentSettings.timeout = setTimeout(() => { | ||
| if (settings !== currentSettings) return; | ||
| update(nextExpectedStepEnd).catch(() => { | ||
| if (settings === currentSettings) settings = null; | ||
| }); | ||
| }, delay); | ||
| } | ||
|
|
||
| function shouldRetryWhenWordsMayStillGenerate( | ||
| currentSettings: Settings, | ||
| ): boolean { | ||
| if (settings !== currentSettings) return false; | ||
| return !areAllTestWordsGenerated(); | ||
| } | ||
|
|
||
| function areAllTestWordsGenerated(): boolean { | ||
| if (Config.mode === "words") { | ||
| return TestWords.words.length >= Config.words && Config.words > 0; | ||
| } | ||
|
|
||
| if (Config.mode === "quote") { | ||
|
Comment on lines
+209
to
+214
|
||
| return ( | ||
| TestWords.words.length >= (TestWords.currentQuote?.textSplit?.length ?? 0) | ||
| ); | ||
| } | ||
|
|
||
| if (Config.mode === "custom") { | ||
| const limitMode = CustomText.getLimitMode(); | ||
| const limitValue = CustomText.getLimitValue(); | ||
|
|
||
| if (limitMode === "word") { | ||
| return TestWords.words.length >= limitValue && limitValue !== 0; | ||
| } | ||
|
|
||
| if (limitMode === "section") { | ||
| return ( | ||
| WordsGenerator.sectionIndex >= limitValue && | ||
| WordsGenerator.currentSection.length === 0 && | ||
| limitValue !== 0 | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| export function reset(): void { | ||
| if (settings?.timeout !== null && settings?.timeout !== undefined) { | ||
| clearTimeout(settings.timeout); | ||
|
|
@@ -188,8 +245,12 @@ export function reset(): void { | |
| startTimestamp = 0; | ||
| } | ||
|
|
||
| function incrementLetterIndex(): void { | ||
| if (settings === null) return; | ||
| function incrementLetterIndex(): boolean { | ||
| if (settings === null) return false; | ||
|
|
||
| const previousWordIndex = settings.currentWordIndex; | ||
| const previousLetterIndex = settings.currentLetterIndex; | ||
| const previousCorrection = settings.correction; | ||
|
|
||
| try { | ||
| settings.currentLetterIndex++; | ||
|
|
@@ -228,13 +289,15 @@ function incrementLetterIndex(): void { | |
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| //out of words | ||
| settings = null; | ||
| console.log("pace caret out of words"); | ||
| } catch { | ||
| settings.currentWordIndex = previousWordIndex; | ||
| settings.currentLetterIndex = previousLetterIndex; | ||
| settings.correction = previousCorrection; | ||
| caret.hide(); | ||
| return; | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| export function handleSpace(correct: boolean, currentWord: string): void { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This recovery test doesn’t exercise the new timer-based retry path: it manually calls
PaceCaret.update(0)again after pushing a word, instead of letting the scheduled retry fire. To validate the fix end-to-end, assert a retry timer was scheduled after the first failure and then advance/run fake timers (and flush pending promises) to confirm the caret state recovers via the scheduled update.