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
4 changes: 2 additions & 2 deletions docs/specs/solid-monkeytype/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ Serial; each step needs the previous one. Verify each in the browser (`__mt.chec
| Done | ID | Size | Step | Blocked by | Notes |
|---|---|---|---|---|---|
| [x] | C1 | S · commit | Config setter writes the store **before** dispatching the config event (and before full-config-change finished) | none | Done in [bd2cae5](https://github.com/monkeytypegame/monkeytype/commit/bd2cae518): `setConfig`/`toggleFunbox` write store → persist → event; whole-config apply resets the store with `Config`, writes per key inside one `batch` (effects run once), all before finished. Leftover for C3: funbox `activate()` writes `Config.funbox` without the store |
| [ ] | C2 | S · commit each | Replace config-bus subscribers outside the test with effects on store keys: theme controller, sound controller, quotes controller, challenge controller, commandline theme list | C1 | One PR each, all parallel |
| [x] | C2 | S · commit each | Replace config-bus subscribers outside the test with effects on store keys: theme controller, sound controller, quotes controller, challenge controller, commandline theme list | C1 | Done in one PR, [#8526](https://github.com/monkeytypegame/monkeytype/pull/8526). Effects only run on a changed value: re-picking a setting no longer re-runs its reaction (challenge stays loaded, quote queue not reshuffled); theme pickers clear the random theme themselves. Theme reactions resolve one configured theme; picking a theme turns auto switch off via `overrideConfig`, so whole-config apply sets `autoSwitchTheme` last |
| [ ] | C3 | M · commit per area; PR for test modules | Move mutable `Config.x` reads to the store, by area: format singleton; commandline lists; controllers; input; test modules | C1 | Parallel by area; the test-module part follows T3/T6 where they overlap |
| [ ] | C4 | L · PR | Delete the mutable config object, the config event bus and the full-config-change events; whole-config apply becomes one batched reconcile (the `batch` already exists since C1); convert remaining test subscribers (pace caret, tts, practise words, test-logic) to effects | C2, C3, T6, L2 | Last step of C |
| [ ] | C4 | L · PR | Delete the mutable config object, the config event bus and the full-config-change events; whole-config apply becomes one batched reconcile (the `batch` already exists since C1); convert remaining test subscribers (pace caret, tts, practise words, test-logic) to effects | C2, C3, T6, L2 | Last step of C. Since C2 no subscriber outside the test listens for full-config-change; `autoSwitchTheme` sits last in `lastConfigsToApply` only because `overrideConfig` turns it off on a theme pick, and that entry can go once apply no longer runs overrides. Converted effects only fire on a changed value, not on re-picking the current one |
| [ ] | C5 | M · PR | Merge commandline metadata into config metadata; command icons become icon-component types | none | Can be L-track work |

## Track U: user data (parallel with T and C)
Expand Down
34 changes: 34 additions & 0 deletions frontend/__tests__/commandline/lists/themes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it, expect } from "vitest";
import { Config, setConfigStore } from "../../../src/ts/config/store";
import commands from "../../../src/ts/commandline/lists/themes";
import * as ThemeController from "../../../src/ts/controllers/theme-controller";
import { __testing } from "../../../src/ts/config/testing";
import { getTheme } from "../../../src/ts/states/theme";

describe("themes commandline list config effects", () => {
const firstTheme = (): string | undefined =>
commands[0]?.subgroup?.list[0]?.configValue as string | undefined;

it("puts new favorites first", () => {
Config.favThemes = ["nord"];
setConfigStore("favThemes", ["nord"]);
expect(firstTheme()).toBe("nord");

Config.favThemes = ["nord", "8008"];
setConfigStore("favThemes", ["nord", "8008"]);
expect(firstTheme()).toBe("8008");
});

it("drops the random theme when the configured theme is re-picked", async () => {
__testing.replaceConfig({ theme: "serika_dark", randomTheme: "on" });
await ThemeController.randomizeTheme();
expect(ThemeController.randomTheme).not.toBeNull();

commands[0]?.subgroup?.list
.find((command) => command.configValue === "serika_dark")
?.exec?.({});

expect(ThemeController.randomTheme).toBeNull();
expect(getTheme().name).toBe("serika_dark");
});
});
40 changes: 40 additions & 0 deletions frontend/__tests__/controllers/challenge-controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it, expect, beforeEach } from "vitest";
import { Config as ConfigType } from "@monkeytype/schemas/configs";
import { Config, setConfigStore } from "../../src/ts/config/store";
import { __testing } from "../../src/ts/config/testing";
import "../../src/ts/controllers/challenge-controller";
import {
getLoadedChallenge,
setLoadedChallenge,
} from "../../src/ts/states/test";

const { replaceConfig } = __testing;

function set<K extends keyof ConfigType>(key: K, value: ConfigType[K]): void {
Config[key] = value;
setConfigStore(key, value);
}

describe("challenge controller config effects", () => {
beforeEach(() => {
replaceConfig({});
setLoadedChallenge({ name: "oneHourWarrior" } as Parameters<
typeof setLoadedChallenge
>[0]);
});

it("clears the challenge on a test setting change", () => {
set("mode", "words");
expect(getLoadedChallenge()).toBeNull();
});

it("clears the challenge on a funbox change", () => {
set("funbox", ["nausea"]);
expect(getLoadedChallenge()).toBeNull();
});

it("keeps the challenge on other changes", () => {
set("soundVolume", 0.3);
expect(getLoadedChallenge()).not.toBeNull();
});
});
21 changes: 21 additions & 0 deletions frontend/__tests__/controllers/quotes-controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect, vi } from "vitest";
import { Config as ConfigType } from "@monkeytype/schemas/configs";
import { Config, setConfigStore } from "../../src/ts/config/store";
import quoteController from "../../src/ts/controllers/quotes-controller";

function set<K extends keyof ConfigType>(key: K, value: ConfigType[K]): void {
Config[key] = value;
setConfigStore(key, value);
}

describe("quotes controller config effects", () => {
const updateQuoteQueueMock = vi.spyOn(quoteController, "updateQuoteQueue");

it("updates the queue on quote length changes", () => {
set("quoteLength", [0, 1]);
expect(updateQuoteQueueMock).toHaveBeenLastCalledWith([0, 1]);

set("quoteLength", [0, 1, 2]);
expect(updateQuoteQueueMock).toHaveBeenLastCalledWith([0, 1, 2]);
});
});
60 changes: 60 additions & 0 deletions frontend/__tests__/controllers/sound-controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Config as ConfigType } from "@monkeytype/schemas/configs";
import { Config, setConfigStore } from "../../src/ts/config/store";
import "../../src/ts/controllers/sound-controller";

const { volumeMock, howlMock } = vi.hoisted(() => ({
volumeMock: vi.fn(),
howlMock: vi.fn(),
}));
vi.mock("howler", () => ({
Howler: { volume: volumeMock },
Howl: class {
constructor(options: unknown) {
howlMock(options);
}
seek(): void {
//
}
},
}));

function set<K extends keyof ConfigType>(key: K, value: ConfigType[K]): void {
Config[key] = value;
setConfigStore(key, value);
}

async function flush(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve));
}

describe("sound controller config effects", () => {
beforeEach(() => {
volumeMock.mockClear();
howlMock.mockClear();
});

it("does nothing at boot", async () => {
await flush();
expect(volumeMock).not.toHaveBeenCalled();
expect(howlMock).not.toHaveBeenCalled();
});

it("loads the click sounds when turned on", async () => {
set("playSoundOnClick", "2");
await flush();
expect(howlMock).toHaveBeenCalled();
});

it("loads nothing when turned off", async () => {
set("playSoundOnClick", "off");
await flush();
expect(howlMock).not.toHaveBeenCalled();
});

it("sets the volume", async () => {
set("soundVolume", 0.3);
await flush();
expect(volumeMock).toHaveBeenCalledWith(0.3);
});
});
67 changes: 67 additions & 0 deletions frontend/__tests__/controllers/theme-controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, it, expect, beforeEach } from "vitest";
import { Config as ConfigType } from "@monkeytype/schemas/configs";
import * as ThemeController from "../../src/ts/controllers/theme-controller";
import { Config, setConfigStore } from "../../src/ts/config/store";
import { __testing } from "../../src/ts/config/testing";
import { getTheme } from "../../src/ts/states/theme";
import { getThemeIndicator, setThemeIndicator } from "../../src/ts/states/core";

const { replaceConfig } = __testing;

function set<K extends keyof ConfigType>(key: K, value: ConfigType[K]): void {
Config[key] = value;
setConfigStore(key, value);
}

describe("theme controller config effects", () => {
beforeEach(() => {
replaceConfig({ theme: "serika_dark" });
});

it("applies the theme", () => {
set("theme", "8008");
expect(getTheme().name).toBe("8008");
});

it("applies the custom theme and its colors", () => {
set("customTheme", true);
expect(getTheme().name).toBe("custom");

const colors = [
...Config.customThemeColors,
] as ConfigType["customThemeColors"];
colors[0] = "#123456";
set("customThemeColors", colors);
expect(getTheme().bg).toBe("#123456");
});

it("applies the light theme on auto switch", () => {
set("themeLight", "nord_light");
set("autoSwitchTheme", true);
expect(getTheme().name).toBe("nord_light");

set("autoSwitchTheme", false);
expect(getTheme().name).toBe("serika_dark");
});

it("keeps the random theme over a custom theme", async () => {
set("customTheme", true);
set("randomTheme", "on");

await ThemeController.randomizeTheme();
await new Promise((resolve) => setTimeout(resolve));

expect(ThemeController.randomTheme).not.toBeNull();
expect(getTheme().name).toBe(ThemeController.randomTheme);
expect(Config.customTheme).toBe(false);
});

it("updates the indicator on favorite changes", () => {
setThemeIndicator({ text: "", isFavorite: false });
set("favThemes", ["serika_dark"]);
expect(getThemeIndicator()).toEqual({
text: "serika dark",
isFavorite: true,
});
});
});
45 changes: 45 additions & 0 deletions frontend/__tests__/root/config-metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,51 @@ describe("ConfigMeta", () => {
given: { customTheme: true },
expected: { customTheme: false },
},
{
value: "8008",
given: { autoSwitchTheme: true },
expected: { autoSwitchTheme: false },
},
{
value: "8008",
given: { customTheme: true, autoSwitchTheme: true },
expected: { customTheme: false, autoSwitchTheme: false },
},
],
customTheme: [
{
value: true,
given: { autoSwitchTheme: true },
expected: { autoSwitchTheme: false },
},
{
value: false,
given: { customTheme: true, autoSwitchTheme: true },
expected: { autoSwitchTheme: false },
},
{
value: false,
given: { customTheme: false, autoSwitchTheme: true },
expected: { autoSwitchTheme: true },
},
],
customThemeColors: [
{
value: [
"#000",
"#111",
"#222",
"#333",
"#444",
"#555",
"#666",
"#777",
"#888",
"#999",
],
given: { autoSwitchTheme: true },
expected: { autoSwitchTheme: false },
},
],
keymapLayout: [
{
Expand Down
38 changes: 38 additions & 0 deletions frontend/__tests__/root/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,35 @@ describe("Config", () => {
);
expect(miscReloadAfterMock).toHaveBeenCalledWith(3);
});

it("notifies once when a theme pick turns off custom theme and auto switch", () => {
//GIVEN
replaceConfig({ customTheme: true, autoSwitchTheme: true });

//WHEN
Config.setConfig("theme", "8008");

//THEN
expect(getConfig()).toMatchObject({
customTheme: false,
autoSwitchTheme: false,
});
expect(notificationAddMock).toHaveBeenCalledExactlyOnceWith(
"Auto switch theme disabled",
);
});

it("keeps auto switch on when an internal write leaves custom theme off", () => {
//GIVEN
replaceConfig({ customTheme: false, autoSwitchTheme: true });

//WHEN (randomizeTheme)
Config.setConfig("customTheme", false, { nosave: true });

//THEN
expect(getConfig().autoSwitchTheme).toBe(true);
expect(notificationAddMock).not.toHaveBeenCalled();
});
});

describe("apply", () => {
Expand Down Expand Up @@ -527,6 +556,15 @@ describe("Config", () => {
expect(getConfigStore.funbox).toEqual([]);
});

it("applyConfig keeps auto switch theme next to a theme", async () => {
await Lifecycle.applyConfig({
theme: "8008",
customTheme: true,
autoSwitchTheme: true,
});
expect(getConfig().autoSwitchTheme).toBe(true);
});

it("applyConfig runs store effects once", async () => {
replaceConfig({ mode: "time", punctuation: false, numbers: false });
const runs: unknown[] = [];
Expand Down
1 change: 1 addition & 0 deletions frontend/src/ts/commandline/lists/custom-themes-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function update(): void {
ThemeController.preview("custom", theme.colors);
},
exec: (): void => {
void ThemeController.clearRandom();
setConfig("customTheme", true);
setConfig("customThemeColors", theme.colors);
},
Expand Down
Loading
Loading