Skip to content
Open
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
13 changes: 10 additions & 3 deletions frontend/__tests__/root/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,16 @@ describe("Config", () => {
vi.useFakeTimers();
mocks.forEach((it) => it.mockClear());

vi.mock("../../src/ts/test/test-state", () => ({
isActive: true,
}));
vi.mock("../../src/ts/test/test-state", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../src/ts/test/test-state")>();

return {
...actual,
isActive: true,
resetQuoteHistory: vi.fn(),
};
});

isConfigValueValidMock.mockReturnValue(true);
canSetConfigWithCurrentFunboxesMock.mockReturnValue(true);
Expand Down
27 changes: 19 additions & 8 deletions frontend/src/html/pages/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,25 @@

<div id="keymap" class="hidden"></div>

<button
id="restartTestButton"
aria-label="Restart Test"
data-balloon-pos="down"
class="text"
>
<i class="fas fa-fw fa-redo-alt"></i>
</button>
<div id="testActionButtons">
<button
id="previousTestButton"
aria-label="Previous Test"
data-balloon-pos="down"
class="text testActionButton hidden"
>
<i class="fas fa-fw fa-undo-alt"></i>
</button>

<button
id="restartTestButton"
aria-label="Restart Test"
data-balloon-pos="down"
class="text testActionButton"
>
<i class="fas fa-fw fa-redo-alt"></i>
</button>
</div>
<div id="liveStatsTextBottom" class="timerMain">
<div class="wrapper">
<div class="liveSpeed hidden">123</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/styles/media-queries-blue.scss
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
}
}
@media (pointer: coarse) and (max-width: 778px) {
#previousTestButton,
#restartTestButton {
display: block !important;
}
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/styles/test.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1321,11 +1321,21 @@
margin-left: 0.5em;
}

#restartTestButton {
#testActionButtons {
display: flex;
justify-content: center;
align-items: center;
gap: 0.75rem;
margin: 1rem auto 0;
}

.testActionButton {
font-size: 1rem;
margin: 1rem auto 0 auto;
display: flex;
align-items: center;
justify-content: center;
padding: 1em 2em;
margin: 0;
}

#compositionDisplay {
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/ts/commandline/lists/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { navigate } from "../../controllers/route-controller";
import { isAuthenticated } from "../../firebase";
import { toggleFullscreen } from "../../utils/misc";
import { Command } from "../types";
import { Config } from "../../config/store";
import * as TestState from "../../test/test-state";
import * as TestLogic from "../../test/test-logic";

const commands: Command[] = [
{
Expand Down Expand Up @@ -58,6 +61,15 @@ const commands: Command[] = [
toggleFullscreen();
},
},
{
id: "previousTest",
display: "Previous Test (Quotes)",
icon: "fa-undo-alt",
available: () => Config.mode === "quote" && TestState.quoteHistoryIndex > 0,
exec: (): void => {
TestLogic.restart({ isPrevious: true });
},
},
];

export default commands;
9 changes: 9 additions & 0 deletions frontend/src/ts/config/setters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ export function setConfig<T extends keyof ConfigSchemas.Config>(
}

Config[key] = value;

if (key === "mode" && previousValue === "quote" && value !== "quote") {
TestState.resetQuoteHistory();
}

if (key === "language" && previousValue !== value) {
TestState.resetQuoteHistory();
}

if (!options?.nosave) saveToLocalStorage(key, options?.nosave);

// @ts-expect-error i can't figure this out
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/ts/event-handlers/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import * as EditResultTagsModal from "../modals/edit-result-tags";
import * as MobileTestConfigModal from "../modals/mobile-test-config";
import * as CustomTestDurationModal from "../modals/custom-test-duration";
import * as TestWords from "../test/test-words";
import * as TestLogic from "../test/test-logic";
import * as TestState from "../test/test-state";
import * as TestUI from "../test/test-ui";
import {
showNoticeNotification,
showErrorNotification,
Expand Down Expand Up @@ -120,3 +123,10 @@ qs(".pageTest #dailyLeaderboardRank")?.on("click", async () => {
)}&goToUserPage=true`,
);
});

testPage?.onChild("click", "#previousTestButton", () => {
if (TestUI.resultCalculating) return;
if (Config.mode === "quote" && TestState.quoteHistoryIndex > 0) {
TestLogic.restart({ isPrevious: true });
}
});
13 changes: 8 additions & 5 deletions frontend/src/ts/test/test-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ type RestartOptions = {
practiseMissed?: boolean;
noAnim?: boolean;
isQuickRestart?: boolean;
isPrevious?: boolean;
};
Comment on lines 169 to 173
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restart({ isPrevious: true }) can be overridden by existing logic that forces options.withSameWordset = true when repeatQuotes === "typing" (and in repeated tests). In that case TestState.isRepeated short-circuits quote selection, so “Previous Test” won’t actually navigate back. Consider explicitly disabling withSameWordset when isPrevious is set (or otherwise ensuring the previous-quote path wins).

Copilot uses AI. Check for mistakes.

export function restart(options = {} as RestartOptions): void {
Expand Down Expand Up @@ -346,7 +347,7 @@ export function restart(options = {} as RestartOptions): void {
TestState.setPaceRepeat(repeatWithPace);
TestInitFailed.hide();
TestState.setTestInitSuccess(true);
const initResult = await init();
const initResult = await init(options.isPrevious ?? false);

if (!initResult) {
TestState.setTestRestarting(false);
Expand Down Expand Up @@ -380,7 +381,7 @@ let lastInitError: Error | null = null;
let showedLazyModeNotification: boolean = false;
let testReinitCount = 0;

async function init(): Promise<boolean> {
async function init(loadPreviousQuote = false): Promise<boolean> {
console.debug("Initializing test");
testReinitCount++;
if (testReinitCount > 3) {
Expand Down Expand Up @@ -413,7 +414,7 @@ async function init(): Promise<boolean> {
}

if (!language || language.name !== Config.language) {
return await init();
return await init(loadPreviousQuote);
}

if (getActivePage() === "test") {
Expand Down Expand Up @@ -512,7 +513,9 @@ async function init(): Promise<boolean> {
let generatedWords: string[] = [];
let generatedSectionIndexes: number[] = [];
try {
const gen = await WordsGenerator.generateWords(language);
const gen = await WordsGenerator.generateWords(language, {
loadPreviousQuote,
});
generatedWords = gen.words;
generatedSectionIndexes = gen.sectionIndexes;
wordsHaveTab = gen.hasTab;
Expand All @@ -537,7 +540,7 @@ async function init(): Promise<boolean> {
});
}

return await init();
return await init(loadPreviousQuote);
}

let hasNumbers = false;
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/ts/test/test-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export let isLanguageRightToLeft = false;
export let isDirectionReversed = false;
export let testRestarting = false;
export let resultVisible = false;
/** Max quotes remembered for "previous quote" navigation (per session). */
export const MAX_QUOTE_HISTORY_LENGTH = 50;

export const quoteHistory: number[] = [];
export let quoteHistoryIndex = -1;

export function setRepeated(tf: boolean): void {
isRepeated = tf;
Expand Down Expand Up @@ -82,3 +87,42 @@ export function setTestRestarting(val: boolean): void {
export function setResultVisible(val: boolean): void {
resultVisible = val;
}

/**
* Id of the quote that would load if we navigate back, without mutating history.
* Call {@link commitPreviousNavigation} only after that quote was resolved successfully.
*/
export function peekPreviousQuoteId(): number | null {
if (quoteHistoryIndex <= 0) {
return null;
}
const val = quoteHistory[quoteHistoryIndex - 1];
return val ?? null;
}

/** Apply a successful "previous quote" navigation (decrements the history cursor). */
export function commitPreviousNavigation(): void {
if (quoteHistoryIndex <= 0) {
return;
}
quoteHistoryIndex--;
}

export function pushQuoteToHistory(quoteId: number): void {
if (quoteHistoryIndex < quoteHistory.length - 1) {
quoteHistory.splice(quoteHistoryIndex + 1);
}

quoteHistory.push(quoteId);
quoteHistoryIndex++;

if (quoteHistory.length > MAX_QUOTE_HISTORY_LENGTH) {
quoteHistory.shift();
quoteHistoryIndex--;
}
}

export function resetQuoteHistory(): void {
quoteHistory.length = 0;
quoteHistoryIndex = -1;
}
15 changes: 15 additions & 0 deletions frontend/src/ts/test/test-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1935,6 +1935,7 @@ export function onTestRestart(source: "testPage" | "resultPage"): void {
void SoundController.clearAllSounds();
cancelPendingAnimationFramesStartingWith("test-ui");
showWords();
updatePreviousButtonVisibility();
}

export function onTestFinish(): void {
Expand Down Expand Up @@ -2090,4 +2091,18 @@ configEvent.subscribe(({ key, newValue }) => {
if (["tapeMode", "tapeMargin"].includes(key)) {
updateLiveStatsMargin();
}
if (key === "mode" || key === "language") {
updatePreviousButtonVisibility();
}
});

export function updatePreviousButtonVisibility(): void {
const prevBtn = document.getElementById("previousTestButton");
if (!prevBtn) return;

if (Config.mode === "quote" && TestState.quoteHistoryIndex > 0) {
prevBtn.classList.remove("hidden");
} else {
prevBtn.classList.add("hidden");
}
}
Loading
Loading