Merge branch 'master' into feature/track-user-email-verification

This commit is contained in:
Christian Fehmer 2025-07-24 11:01:49 +02:00 committed by GitHub
commit 157b280533
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1134 additions and 1058 deletions

View file

@ -0,0 +1,278 @@
import { configMetadata } from "../../src/ts/config-metadata";
import * as Config from "../../src/ts/config";
import { ConfigKey, Config as ConfigType } from "@monkeytype/schemas/configs";
const { replaceConfig, getConfig } = Config.__testing;
type TestsByConfig<T> = Partial<{
[K in keyof ConfigType]: (T & { value: ConfigType[K] })[];
}>;
describe("ConfigMeta", () => {
afterAll(() => {
replaceConfig({});
vi.resetModules();
});
it("should have changeRequiresRestart defined", () => {
const configsRequiringRestarts = Object.entries(configMetadata)
.filter(([_key, value]) => value.changeRequiresRestart === true)
.map(([key]) => key)
.sort();
expect(configsRequiringRestarts).toEqual(
[
"punctuation",
"numbers",
"words",
"time",
"mode",
"quoteLength",
"language",
"difficulty",
"minWpmCustomSpeed",
"minWpm",
"minAcc",
"minAccCustom",
"minBurst",
"minBurstCustomSpeed",
"britishEnglish",
"funbox",
"customLayoutfluid",
"strictSpace",
"stopOnError",
"lazyMode",
"layout",
"codeUnindentOnBackspace",
].sort()
);
});
it("should have triggerResize defined", () => {
const configsWithTriggeResize = Object.entries(configMetadata)
.filter(([_key, value]) => value.triggerResize === true)
.map(([key]) => key)
.sort();
expect(configsWithTriggeResize).toEqual(
["fontSize", "keymapSize", "maxLineWidth", "tapeMode"].sort()
);
});
describe("overrideValue", () => {
const testCases: TestsByConfig<{
given?: Partial<ConfigType>;
expected: Partial<ConfigType>;
}> = {
punctuation: [
{ value: true, expected: { punctuation: true } },
{
value: true,
given: { mode: "quote" },
expected: { punctuation: false },
},
],
numbers: [
{ value: true, expected: { numbers: true } },
{
value: true,
given: { mode: "quote" },
expected: { numbers: false },
},
],
customLayoutfluid: [
{
value: ["qwerty", "qwerty", "qwertz"],
expected: { customLayoutfluid: ["qwerty", "qwertz"] },
},
],
customPolyglot: [
{
value: ["english", "polish", "english"],
expected: { customPolyglot: ["english", "polish"] },
},
],
keymapSize: [
{ value: 1, expected: { keymapSize: 1 } },
{ value: 1.234, expected: { keymapSize: 1.2 } },
{ value: 0.4, expected: { keymapSize: 0.5 } },
{ value: 3.6, expected: { keymapSize: 3.5 } },
],
customBackground: [
{
value: " https://example.com/test.jpg ",
expected: { customBackground: "https://example.com/test.jpg" },
},
],
accountChart: [
{
value: ["on", "off", "off", "off"],
expected: { accountChart: ["on", "off", "off", "off"] },
},
{
value: ["off", "off", "off", "off"],
given: { accountChart: ["on", "off", "off", "off"] },
expected: { accountChart: ["off", "on", "off", "off"] },
},
{
value: ["off", "off", "on", "on"],
given: { accountChart: ["off", "on", "off", "off"] },
expected: { accountChart: ["on", "off", "on", "on"] },
},
],
};
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given expect=$expected`,
({ key, value, given, expected }) => {
//GIVEN
replaceConfig(given ?? {});
//WHEN
Config.genericSet(key, value as any);
//THEN
expect(getConfig()).toMatchObject(expected);
}
);
});
describe("isBlocked", () => {
const testCases: TestsByConfig<{
given?: Partial<ConfigType>;
fail?: true;
}> = {
funbox: [
{
value: "gibberish" as any,
given: { mode: "quote" },
fail: true,
},
],
showAllLines: [
{ value: true, given: { tapeMode: "off" } },
{ value: false, given: { tapeMode: "word" } },
{ value: true, given: { tapeMode: "word" }, fail: true },
],
};
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given fail=$fail`,
({ key, value, given, fail }) => {
//GIVEN
replaceConfig(given ?? {});
//WHEN
const applied = Config.genericSet(key, value as any);
//THEN
expect(applied).toEqual(!fail);
}
);
});
describe("overrideConfig", () => {
const testCases: TestsByConfig<{
given: Partial<ConfigType>;
expected?: Partial<ConfigType>;
}> = {
mode: [
{ value: "time", given: { numbers: true, punctuation: true } },
{
value: "custom",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
{
value: "quote",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
{
value: "zen",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
],
numbers: [{ value: false, given: { mode: "quote" } }],
freedomMode: [
{
value: false,
given: { confidenceMode: "on" },
expected: { confidenceMode: "on" },
},
{
value: true,
given: { confidenceMode: "on" },
expected: { confidenceMode: "off" },
},
],
stopOnError: [
{
value: "off",
given: { confidenceMode: "on" },
expected: { confidenceMode: "on" },
},
{
value: "word",
given: { confidenceMode: "on" },
expected: { confidenceMode: "off" },
},
],
confidenceMode: [
{
value: "off",
given: { freedomMode: true, stopOnError: "word" },
expected: { freedomMode: true, stopOnError: "word" },
},
{
value: "on",
given: { freedomMode: true, stopOnError: "word" },
expected: { freedomMode: false, stopOnError: "off" },
},
],
tapeMode: [
{
value: "off",
given: { showAllLines: true },
expected: { showAllLines: true },
},
{
value: "letter",
given: { showAllLines: true },
expected: { showAllLines: false },
},
],
theme: [
{
value: "8008",
given: { customTheme: true },
expected: { customTheme: false },
},
],
};
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given expected=$expected`,
({ key, value, given, expected }) => {
//GIVEN
replaceConfig(given);
//WHEN
Config.genericSet(key, value as any);
//THEN
expect(getConfig()).toMatchObject(expected ?? {});
}
);
});
});

View file

@ -4,7 +4,7 @@ import {
CustomThemeColors,
FunboxName,
ConfigKey,
Config as ConfigType,
CaretStyleSchema,
} from "@monkeytype/schemas/configs";
import { randomBytes } from "crypto";
import { vi } from "vitest";
@ -15,66 +15,60 @@ import * as DB from "../../src/ts/db";
import * as AccountButton from "../../src/ts/elements/account-button";
import * as Notifications from "../../src/ts/elements/notifications";
type TestsByConfig<T> = Partial<{
[K in keyof ConfigType]: (T & { value: ConfigType[K] })[];
}>;
const { configMetadata, replaceConfig, getConfig } = Config.__testing;
const { replaceConfig, getConfig } = Config.__testing;
describe("Config", () => {
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");
beforeEach(() => isDevEnvironmentMock.mockReset());
describe("test with mocks", () => {
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");
const canSetConfigWithCurrentFunboxesMock = vi.spyOn(
FunboxValidation,
"canSetConfigWithCurrentFunboxes"
);
const isConfigValueValidMock = vi.spyOn(
ConfigValidation,
"isConfigValueValid"
);
const dispatchConfigEventMock = vi.spyOn(ConfigEvent, "dispatch");
const dbSaveConfigMock = vi.spyOn(DB, "saveConfig");
const accountButtonLoadingMock = vi.spyOn(AccountButton, "loading");
const notificationAddMock = vi.spyOn(Notifications, "add");
const miscReloadAfterMock = vi.spyOn(Misc, "reloadAfter");
const miscTriggerResizeMock = vi.spyOn(Misc, "triggerResize");
const mocks = [
canSetConfigWithCurrentFunboxesMock,
isConfigValueValidMock,
dispatchConfigEventMock,
dbSaveConfigMock,
accountButtonLoadingMock,
notificationAddMock,
miscReloadAfterMock,
miscTriggerResizeMock,
];
beforeEach(async () => {
vi.useFakeTimers();
mocks.forEach((it) => it.mockReset());
vi.mock("../../src/ts/test/test-state", () => ({
isActive: true,
}));
isConfigValueValidMock.mockReturnValue(true);
canSetConfigWithCurrentFunboxesMock.mockReturnValue(true);
dbSaveConfigMock.mockResolvedValue();
describe("configMeta", () => {
afterAll(() => {
replaceConfig({});
vi.resetModules();
});
it("should have changeRequiresRestart defined", () => {
const configsRequiringRestarts = Object.entries(configMetadata)
.filter(([_key, value]) => value.changeRequiresRestart === true)
.map(([key]) => key)
.sort();
expect(configsRequiringRestarts).toEqual(
[
"punctuation",
"numbers",
"words",
"time",
"mode",
"quoteLength",
"language",
"difficulty",
"minWpmCustomSpeed",
"minWpm",
"minAcc",
"minAccCustom",
"minBurst",
"minBurstCustomSpeed",
"britishEnglish",
"funbox",
"customLayoutfluid",
"strictSpace",
"stopOnError",
"lazyMode",
"layout",
"codeUnindentOnBackspace",
].sort()
);
});
it("should have triggerResize defined", () => {
const configsWithTriggeResize = Object.entries(configMetadata)
.filter(([_key, value]) => value.triggerResize === true)
.map(([key]) => key)
.sort();
expect(configsWithTriggeResize).toEqual(
["fontSize", "keymapSize", "maxLineWidth", "tapeMode"].sort()
);
afterAll(() => {
mocks.forEach((it) => it.mockRestore());
vi.useRealTimers();
});
beforeEach(() => isDevEnvironmentMock.mockReset());
it("should throw if config key in not found in metadata", () => {
expect(() => {
Config.genericSet("nonExistentKey" as ConfigKey, true);
@ -83,417 +77,198 @@ describe("Config", () => {
);
});
describe("overrideValue", () => {
const testCases: TestsByConfig<{
given?: Partial<ConfigType>;
expected: Partial<ConfigType>;
}> = {
punctuation: [
{ value: true, expected: { punctuation: true } },
{
value: true,
given: { mode: "quote" },
expected: { punctuation: false },
},
],
numbers: [
{ value: true, expected: { numbers: true } },
{
value: true,
given: { mode: "quote" },
expected: { numbers: false },
},
],
customLayoutfluid: [
{
value: ["qwerty", "qwerty", "qwertz"],
expected: { customLayoutfluid: ["qwerty", "qwertz"] },
},
],
customPolyglot: [
{
value: ["english", "polish", "english"],
expected: { customPolyglot: ["english", "polish"] },
},
],
keymapSize: [
{ value: 1, expected: { keymapSize: 1 } },
{ value: 1.234, expected: { keymapSize: 1.2 } },
{ value: 0.4, expected: { keymapSize: 0.5 } },
{ value: 3.6, expected: { keymapSize: 3.5 } },
],
customBackground: [
{
value: " https://example.com/test.jpg ",
expected: { customBackground: "https://example.com/test.jpg" },
},
],
accountChart: [
{
value: ["on", "off", "off", "off"],
expected: { accountChart: ["on", "off", "off", "off"] },
},
{
value: ["off", "off", "off", "off"],
given: { accountChart: ["on", "off", "off", "off"] },
expected: { accountChart: ["off", "on", "off", "off"] },
},
{
value: ["off", "off", "on", "on"],
given: { accountChart: ["off", "on", "off", "off"] },
expected: { accountChart: ["on", "off", "on", "on"] },
},
],
};
it("fails if test is active and funbox no_quit", () => {
//GIVEN
replaceConfig({ funbox: ["no_quit"], numbers: false });
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given expect=$expected`,
({ key, value, given, expected }) => {
//GIVEN
replaceConfig(given ?? {});
//WHEN
expect(Config.genericSet("numbers", true, true)).toBe(false);
//WHEN
Config.genericSet(key, value as any);
//THEN
expect(getConfig()).toMatchObject(expected);
//THEN
expect(notificationAddMock).toHaveBeenCalledWith(
"No quit funbox is active. Please finish the test.",
0,
{
important: true,
}
);
});
describe("isBlocked", () => {
const testCases: TestsByConfig<{
given?: Partial<ConfigType>;
fail?: true;
}> = {
funbox: [
{
value: "gibberish" as any,
given: { mode: "quote" },
fail: true,
},
],
showAllLines: [
{ value: true, given: { tapeMode: "off" } },
{ value: false, given: { tapeMode: "word" } },
{ value: true, given: { tapeMode: "word" }, fail: true },
],
};
//TODO isBlocked
it("should fail if config is blocked", () => {
//GIVEN
replaceConfig({ tapeMode: "letter" });
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given fail=$fail`,
({ key, value, given, fail }) => {
//GIVEN
replaceConfig(given ?? {});
//WHEN / THEN
expect(Config.genericSet("showAllLines", true)).toBe(false);
});
//WHEN
const applied = Config.genericSet(key, value as any);
it("should use overrideValue", () => {
//WHEN
Config.genericSet("customLayoutfluid", ["3l", "ABNT2", "3l"]);
//THEN
expect(applied).toEqual(!fail);
}
//THEN
expect(getConfig().customLayoutfluid).toEqual(["3l", "ABNT2"]);
});
it("fails if config is invalid", () => {
//GIVEN
isConfigValueValidMock.mockReturnValue(false);
//WHEN / THEN
expect(Config.genericSet("caretStyle", "banana" as any)).toBe(false);
expect(isConfigValueValidMock).toHaveBeenCalledWith(
"caret style",
"banana",
CaretStyleSchema
);
});
describe("overrideConfig", () => {
const testCases: TestsByConfig<{
given: Partial<ConfigType>;
expected?: Partial<ConfigType>;
}> = {
mode: [
{ value: "time", given: { numbers: true, punctuation: true } },
{
value: "custom",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
{
value: "quote",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
{
value: "zen",
given: { numbers: true, punctuation: true },
expected: { numbers: false, punctuation: false },
},
],
numbers: [{ value: false, given: { mode: "quote" } }],
freedomMode: [
{
value: false,
given: { confidenceMode: "on" },
expected: { confidenceMode: "on" },
},
{
value: true,
given: { confidenceMode: "on" },
expected: { confidenceMode: "off" },
},
],
stopOnError: [
{
value: "off",
given: { confidenceMode: "on" },
expected: { confidenceMode: "on" },
},
{
value: "word",
given: { confidenceMode: "on" },
expected: { confidenceMode: "off" },
},
],
confidenceMode: [
{
value: "off",
given: { freedomMode: true, stopOnError: "word" },
expected: { freedomMode: true, stopOnError: "word" },
},
{
value: "on",
given: { freedomMode: true, stopOnError: "word" },
expected: { freedomMode: false, stopOnError: "off" },
},
],
tapeMode: [
{
value: "off",
given: { showAllLines: true },
expected: { showAllLines: true },
},
{
value: "letter",
given: { showAllLines: true },
expected: { showAllLines: false },
},
],
theme: [
{
value: "8008",
given: { customTheme: true },
expected: { customTheme: false },
},
],
};
it("cannot set if funbox disallows", () => {
//GIVEN
canSetConfigWithCurrentFunboxesMock.mockReturnValue(false);
it.for(
Object.entries(testCases).flatMap(([key, value]) =>
value.flatMap((it) => ({ key: key as ConfigKey, ...it }))
)
)(
`$key value=$value given=$given expected=$expected`,
({ key, value, given, expected }) => {
//GIVEN
replaceConfig(given);
//WHEN / THEN
expect(Config.genericSet("numbers", true)).toBe(false);
});
//WHEN
Config.genericSet(key, value as any);
it("sets overrideConfigs", () => {
//GIVEN
replaceConfig({
confidenceMode: "off",
freedomMode: false, //already set correctly
stopOnError: "letter", //should get updated
});
//THEN
expect(getConfig()).toMatchObject(expected ?? {});
}
//WHEN
Config.genericSet("confidenceMode", "max");
//THEN
expect(dispatchConfigEventMock).not.toHaveBeenCalledWith(
"freedomMode",
false,
true,
true
);
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"stopOnError",
"off",
true,
"letter"
);
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"confidenceMode",
"max",
false,
"off"
);
});
describe("test with mocks", () => {
const canSetConfigWithCurrentFunboxesMock = vi.spyOn(
FunboxValidation,
"canSetConfigWithCurrentFunboxes"
it("saves to localstorage if nosave=false", async () => {
//GIVEN
replaceConfig({ numbers: false });
//WHEN
Config.genericSet("numbers", true);
//THEN
//wait for debounce
await vi.advanceTimersByTimeAsync(2500);
//show loading
expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(1, true);
//save
expect(dbSaveConfigMock).toHaveBeenCalledWith({ numbers: true });
//hide loading
expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(2, false);
//send event
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"saveToLocalStorage",
expect.stringContaining("numbers")
);
const isConfigValueValidMock = vi.spyOn(
ConfigValidation,
"isConfigValueValid"
});
it("does not save to localstorage if nosave=true", async () => {
//GIVEN
replaceConfig({ numbers: false });
//WHEN
Config.genericSet("numbers", true, true);
//THEN
//wait for debounce
await vi.advanceTimersByTimeAsync(2500);
expect(accountButtonLoadingMock).not.toHaveBeenCalled();
expect(dbSaveConfigMock).not.toHaveBeenCalled();
expect(dispatchConfigEventMock).not.toHaveBeenCalledWith(
"saveToLocalStorage",
expect.any(String)
);
const dispatchConfigEventMock = vi.spyOn(ConfigEvent, "dispatch");
const dbSaveConfigMock = vi.spyOn(DB, "saveConfig");
const accountButtonLoadingMock = vi.spyOn(AccountButton, "loading");
const notificationAddMock = vi.spyOn(Notifications, "add");
const miscReloadAfterMock = vi.spyOn(Misc, "reloadAfter");
});
const mocks = [
canSetConfigWithCurrentFunboxesMock,
isConfigValueValidMock,
dispatchConfigEventMock,
dbSaveConfigMock,
accountButtonLoadingMock,
notificationAddMock,
miscReloadAfterMock,
];
it("dispatches event on set", () => {
//GIVEN
replaceConfig({ numbers: false });
beforeEach(async () => {
vi.useFakeTimers();
mocks.forEach((it) => it.mockReset());
//WHEN
Config.genericSet("numbers", true, true);
vi.mock("../../src/ts/test/test-state", () => ({
isActive: true,
}));
//THEN
isConfigValueValidMock.mockReturnValue(true);
canSetConfigWithCurrentFunboxesMock.mockReturnValue(true);
dbSaveConfigMock.mockResolvedValue();
});
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"numbers",
true,
true,
false
);
});
afterAll(() => {
mocks.forEach((it) => it.mockRestore());
vi.useRealTimers();
});
it("triggers resize if property is set", () => {
///WHEN
Config.genericSet("maxLineWidth", 50, false);
it("cannot set if funbox disallows", () => {
//GIVEN
canSetConfigWithCurrentFunboxesMock.mockReturnValue(false);
expect(miscTriggerResizeMock).toHaveBeenCalled();
});
//WHEN / THEN
expect(Config.genericSet("numbers", true)).toBe(false);
});
it("does not triggers resize if property is not set", () => {
///WHEN
Config.genericSet("startGraphsAtZero", true, false);
it("fails if config is invalid", () => {
//GIVEN
isConfigValueValidMock.mockReturnValue(false);
expect(miscTriggerResizeMock).not.toHaveBeenCalled();
});
//WHEN / THEN
expect(Config.genericSet("numbers", "off" as any)).toBe(false);
});
it("does not triggers resize if property on nosave", () => {
///WHEN
Config.genericSet("maxLineWidth", 50, true);
it("dispatches event on set", () => {
//GIVEN
replaceConfig({ numbers: false });
expect(miscTriggerResizeMock).not.toHaveBeenCalled();
});
//WHEN
Config.genericSet("numbers", true, true);
it("calls afterSet", () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
replaceConfig({ ads: "off" });
//THEN
//WHEN
Config.genericSet("ads", "sellout");
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"numbers",
true,
true,
false
);
});
it("saves to localstorage if nosave=false", async () => {
//GIVEN
replaceConfig({ numbers: false });
//WHEN
Config.genericSet("numbers", true);
//THEN
//wait for debounce
await vi.advanceTimersByTimeAsync(2500);
//show loading
expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(1, true);
//save
expect(dbSaveConfigMock).toHaveBeenCalledWith({ numbers: true });
//hide loading
expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(2, false);
//send event
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"saveToLocalStorage",
expect.stringContaining("numbers")
);
});
it("does not save to localstorage if nosave=true", async () => {
//GIVEN
replaceConfig({ numbers: false });
//WHEN
Config.genericSet("numbers", true, true);
//THEN
//wait for debounce
await vi.advanceTimersByTimeAsync(2500);
expect(accountButtonLoadingMock).not.toHaveBeenCalled();
expect(dbSaveConfigMock).not.toHaveBeenCalled();
expect(dispatchConfigEventMock).not.toHaveBeenCalledWith(
"saveToLocalStorage",
expect.any(String)
);
});
it("calls afterSet", () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
replaceConfig({ ads: "off" });
//WHEN
Config.genericSet("ads", "sellout");
//THEN
expect(notificationAddMock).toHaveBeenCalledWith(
"Ad settings changed. Refreshing...",
0
);
expect(miscReloadAfterMock).toHaveBeenCalledWith(3);
});
it("fails if test is active and funbox no_quit", () => {
//GIVEN
replaceConfig({ funbox: ["no_quit"], numbers: false });
//WHEN
expect(Config.genericSet("numbers", true, true)).toBe(false);
//THEN
expect(notificationAddMock).toHaveBeenCalledWith(
"No quit funbox is active. Please finish the test.",
0,
{
important: true,
}
);
});
it("sends configEvents for overrideConfigs", () => {
//GIVEN
replaceConfig({
confidenceMode: "off",
freedomMode: true,
stopOnError: "letter",
});
//WHEN
Config.genericSet("confidenceMode", "max");
//THEN
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"freedomMode",
false,
true,
true
);
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"stopOnError",
"off",
true,
"letter"
);
expect(dispatchConfigEventMock).toHaveBeenCalledWith(
"confidenceMode",
"max",
false,
"off"
);
});
//THEN
expect(notificationAddMock).toHaveBeenCalledWith(
"Ad settings changed. Refreshing...",
0
);
expect(miscReloadAfterMock).toHaveBeenCalledWith(3);
});
});
//TODO move the rest to schema/tests or remove after removing the setX functions from Config
it("setMode", () => {
expect(Config.setMode("zen")).toBe(true);
expect(Config.setMode("invalid" as any)).toBe(false);

View file

@ -0,0 +1,635 @@
import * as DB from "./db";
import * as Notifications from "./elements/notifications";
import { isAuthenticated } from "./firebase";
import { canSetFunboxWithConfig } from "./test/funbox/funbox-validation";
import { isDevEnvironment, reloadAfter } from "./utils/misc";
import * as ConfigSchemas from "@monkeytype/schemas/configs";
import { roundTo1 } from "@monkeytype/util/numbers";
// type SetBlock = {
// [K in keyof ConfigSchemas.Config]?: ConfigSchemas.Config[K][];
// };
// type RequiredConfig = {
// [K in keyof ConfigSchemas.Config]?: ConfigSchemas.Config[K];
// };
export type ConfigMetadata = {
[K in keyof ConfigSchemas.Config]: {
/**
* Optional display string for the config key.
*/
displayString?: string;
/**
* Should the config change trigger a resize event? handled in ui.ts:108
*/
triggerResize?: true;
/**
* Is a test restart required after this config change?
*/
changeRequiresRestart: boolean;
/**
* Optional function that checks if the config value is blocked from being set.
* Returns true if setting the config value should be blocked.
* @param options - The options object containing the value being set and the current config.
*/
isBlocked?: (options: {
value: ConfigSchemas.Config[K];
currentConfig: Readonly<ConfigSchemas.Config>;
}) => boolean;
/**
* Optional function to override the value before setting it.
* Returns the modified value.
* @param options - The options object containing the value being set, the current value, and the current config.
* @returns The modified value to be set for the config key.
*/
overrideValue?: (options: {
value: ConfigSchemas.Config[K];
currentValue: ConfigSchemas.Config[K];
currentConfig: Readonly<ConfigSchemas.Config>;
}) => ConfigSchemas.Config[K];
/**
* Optional function to override other config values before this one is set.
* Returns an object with the config keys and their new values.
* @param options - The options object containing the value being set and the current config.
*/
overrideConfig?: (options: {
value: ConfigSchemas.Config[K];
currentConfig: Readonly<ConfigSchemas.Config>;
}) => Partial<ConfigSchemas.Config>;
/**
* Optional function that is called after the config value is set.
* It can be used to perform additional actions, like reloading the page.
* @param options - The options object containing the nosave flag and the current config.
*/
afterSet?: (options: {
nosave: boolean;
currentConfig: Readonly<ConfigSchemas.Config>;
}) => void;
};
};
//todo:
// maybe have generic set somehow handle test restarting
// maybe add config group to each metadata object? all though its already defined in ConfigGroupsLiteral
export const configMetadata: ConfigMetadata = {
// test
punctuation: {
changeRequiresRestart: true,
overrideValue: ({ value, currentConfig }) => {
if (currentConfig.mode === "quote") {
return false;
}
return value;
},
},
numbers: {
changeRequiresRestart: true,
overrideValue: ({ value, currentConfig }) => {
if (currentConfig.mode === "quote") {
return false;
}
return value;
},
},
words: {
displayString: "word count",
changeRequiresRestart: true,
},
time: {
changeRequiresRestart: true,
displayString: "time",
},
mode: {
changeRequiresRestart: true,
overrideConfig: ({ value }) => {
if (value === "custom" || value === "quote" || value === "zen") {
return {
numbers: false,
punctuation: false,
};
}
return {};
},
afterSet: ({ currentConfig }) => {
if (currentConfig.mode === "zen" && currentConfig.paceCaret !== "off") {
Notifications.add(`Pace caret will not work with zen mode.`, 0);
}
},
},
quoteLength: {
displayString: "quote length",
changeRequiresRestart: true,
},
language: {
displayString: "language",
changeRequiresRestart: true,
},
burstHeatmap: {
displayString: "burst heatmap",
changeRequiresRestart: false,
},
// behavior
difficulty: {
changeRequiresRestart: true,
},
quickRestart: {
displayString: "quick restart",
changeRequiresRestart: false,
},
repeatQuotes: {
displayString: "repeat quotes",
changeRequiresRestart: false,
},
blindMode: {
displayString: "blind mode",
changeRequiresRestart: false,
},
alwaysShowWordsHistory: {
displayString: "always show words history",
changeRequiresRestart: false,
},
singleListCommandLine: {
displayString: "single list command line",
changeRequiresRestart: false,
},
minWpm: {
displayString: "min speed",
changeRequiresRestart: true,
},
minWpmCustomSpeed: {
displayString: "min speed custom",
changeRequiresRestart: true,
},
minAcc: {
displayString: "min accuracy",
changeRequiresRestart: true,
},
minAccCustom: {
displayString: "min accuracy custom",
changeRequiresRestart: true,
},
minBurst: {
displayString: "min burst",
changeRequiresRestart: true,
},
minBurstCustomSpeed: {
displayString: "min burst custom speed",
changeRequiresRestart: true,
},
britishEnglish: {
displayString: "british english",
changeRequiresRestart: true,
},
funbox: {
changeRequiresRestart: true,
isBlocked: ({ value, currentConfig }) => {
for (const funbox of currentConfig.funbox) {
if (!canSetFunboxWithConfig(funbox, currentConfig)) {
Notifications.add(
`${value}" cannot be enabled with the current config`,
0
);
return true;
}
}
return false;
},
},
customLayoutfluid: {
displayString: "custom layoutfluid",
changeRequiresRestart: true,
overrideValue: ({ value }) => {
return Array.from(new Set(value));
},
},
customPolyglot: {
displayString: "custom polyglot",
changeRequiresRestart: false,
overrideValue: ({ value }) => {
return Array.from(new Set(value));
},
},
// input
freedomMode: {
changeRequiresRestart: false,
displayString: "freedom mode",
overrideConfig: ({ value }) => {
if (value) {
return {
confidenceMode: "off",
};
}
return {};
},
},
strictSpace: {
displayString: "strict space",
changeRequiresRestart: true,
},
oppositeShiftMode: {
displayString: "opposite shift mode",
changeRequiresRestart: false,
},
stopOnError: {
displayString: "stop on error",
changeRequiresRestart: true,
overrideConfig: ({ value }) => {
if (value !== "off") {
return {
confidenceMode: "off",
};
}
return {};
},
},
confidenceMode: {
displayString: "confidence mode",
changeRequiresRestart: false,
overrideConfig: ({ value }) => {
if (value !== "off") {
return {
freedomMode: false,
stopOnError: "off",
};
}
return {};
},
},
quickEnd: {
displayString: "quick end",
changeRequiresRestart: false,
},
indicateTypos: {
displayString: "indicate typos",
changeRequiresRestart: false,
},
hideExtraLetters: {
displayString: "hide extra letters",
changeRequiresRestart: false,
},
lazyMode: {
displayString: "lazy mode",
changeRequiresRestart: true,
},
layout: {
displayString: "layout",
changeRequiresRestart: true,
},
codeUnindentOnBackspace: {
displayString: "code unindent on backspace",
changeRequiresRestart: true,
},
// sound
soundVolume: {
displayString: "sound volume",
changeRequiresRestart: false,
},
playSoundOnClick: {
displayString: "play sound on click",
changeRequiresRestart: false,
},
playSoundOnError: {
displayString: "play sound on error",
changeRequiresRestart: false,
},
playTimeWarning: {
displayString: "play time warning",
changeRequiresRestart: false,
},
// caret
smoothCaret: {
displayString: "smooth caret",
changeRequiresRestart: false,
},
caretStyle: {
displayString: "caret style",
changeRequiresRestart: false,
},
paceCaret: {
displayString: "pace caret",
changeRequiresRestart: false,
isBlocked: ({ value }) => {
if (document.readyState === "complete") {
if ((value === "pb" || value === "tagPb") && !isAuthenticated()) {
Notifications.add(
`Pace caret "pb" and "tag pb" are unavailable without an account`,
0
);
return true;
}
}
return false;
},
},
paceCaretCustomSpeed: {
displayString: "pace caret custom speed",
changeRequiresRestart: false,
},
paceCaretStyle: {
displayString: "pace caret style",
changeRequiresRestart: false,
},
repeatedPace: {
displayString: "repeated pace",
changeRequiresRestart: false,
},
// appearance
timerStyle: {
displayString: "timer style",
changeRequiresRestart: false,
},
liveSpeedStyle: {
displayString: "live speed style",
changeRequiresRestart: false,
},
liveAccStyle: {
displayString: "live accuracy style",
changeRequiresRestart: false,
},
liveBurstStyle: {
displayString: "live burst style",
changeRequiresRestart: false,
},
timerColor: {
displayString: "timer color",
changeRequiresRestart: false,
},
timerOpacity: {
displayString: "timer opacity",
changeRequiresRestart: false,
},
highlightMode: {
displayString: "highlight mode",
changeRequiresRestart: false,
},
tapeMode: {
triggerResize: true,
changeRequiresRestart: false,
displayString: "tape mode",
overrideConfig: ({ value }) => {
if (value !== "off") {
return {
showAllLines: false,
};
}
return {};
},
},
tapeMargin: {
displayString: "tape margin",
changeRequiresRestart: false,
overrideValue: ({ value }) => {
//TODO move to migration after settings validation
if (value < 10) {
value = 10;
}
if (value > 90) {
value = 90;
}
return value;
},
},
smoothLineScroll: {
displayString: "smooth line scroll",
changeRequiresRestart: false,
},
showAllLines: {
changeRequiresRestart: false,
displayString: "show all lines",
isBlocked: ({ value, currentConfig }) => {
if (value && currentConfig.tapeMode !== "off") {
Notifications.add("Show all lines doesn't support tape mode.", 0);
return true;
}
return false;
},
},
alwaysShowDecimalPlaces: {
displayString: "always show decimal places",
changeRequiresRestart: false,
},
typingSpeedUnit: {
displayString: "typing speed unit",
changeRequiresRestart: false,
},
startGraphsAtZero: {
displayString: "start graphs at zero",
changeRequiresRestart: false,
},
maxLineWidth: {
changeRequiresRestart: false,
triggerResize: true,
displayString: "max line width",
overrideValue: ({ value }) => {
//TODO move to migration after settings validation
if (value < 20 && value !== 0) {
value = 20;
}
if (value > 1000) {
value = 1000;
}
return value;
},
},
fontSize: {
changeRequiresRestart: false,
triggerResize: true,
displayString: "font size",
overrideValue: ({ value }) => {
//TODO move to migration after settings validation
if (value < 0) {
value = 1;
}
return value;
},
},
fontFamily: {
displayString: "font family",
changeRequiresRestart: false,
},
keymapMode: {
displayString: "keymap mode",
changeRequiresRestart: false,
},
keymapLayout: {
displayString: "keymap layout",
changeRequiresRestart: false,
},
keymapStyle: {
displayString: "keymap style",
changeRequiresRestart: false,
},
keymapLegendStyle: {
displayString: "keymap legend style",
changeRequiresRestart: false,
},
keymapShowTopRow: {
displayString: "keymap show top row",
changeRequiresRestart: false,
},
keymapSize: {
triggerResize: true,
changeRequiresRestart: false,
displayString: "keymap size",
overrideValue: ({ value }) => {
if (value < 0.5) value = 0.5;
if (value > 3.5) value = 3.5;
return roundTo1(value);
},
},
// theme
flipTestColors: {
displayString: "flip test colors",
changeRequiresRestart: false,
},
colorfulMode: {
displayString: "colorful mode",
changeRequiresRestart: false,
},
customBackground: {
displayString: "custom background",
changeRequiresRestart: false,
overrideValue: ({ value }) => {
return value.trim();
},
},
customBackgroundSize: {
displayString: "custom background size",
changeRequiresRestart: false,
},
customBackgroundFilter: {
displayString: "custom background filter",
changeRequiresRestart: false,
},
autoSwitchTheme: {
displayString: "auto switch theme",
changeRequiresRestart: false,
},
themeLight: {
displayString: "theme light",
changeRequiresRestart: false,
},
themeDark: {
displayString: "theme dark",
changeRequiresRestart: false,
},
randomTheme: {
changeRequiresRestart: false,
displayString: "random theme",
isBlocked: ({ value }) => {
if (value === "custom") {
const snapshot = DB.getSnapshot();
if (!isAuthenticated()) {
Notifications.add(
"Random theme 'custom' is unavailable without an account",
0
);
return true;
}
if (!snapshot) {
Notifications.add(
"Random theme 'custom' requires a snapshot to be set",
0
);
return true;
}
if (snapshot?.customThemes?.length === 0) {
Notifications.add(
"Random theme 'custom' requires at least one custom theme to be saved",
0
);
return true;
}
}
return false;
},
},
favThemes: {
displayString: "favorite themes",
changeRequiresRestart: false,
},
theme: {
changeRequiresRestart: false,
overrideConfig: () => {
return {
customTheme: false,
};
},
},
customTheme: {
displayString: "custom theme",
changeRequiresRestart: false,
},
customThemeColors: {
displayString: "custom theme colors",
changeRequiresRestart: false,
},
// hide elements
showKeyTips: {
displayString: "show key tips",
changeRequiresRestart: false,
},
showOutOfFocusWarning: {
displayString: "show out of focus warning",
changeRequiresRestart: false,
},
capsLockWarning: {
displayString: "caps lock warning",
changeRequiresRestart: false,
},
showAverage: {
displayString: "show average",
changeRequiresRestart: false,
},
// other (hidden)
accountChart: {
displayString: "account chart",
changeRequiresRestart: false,
overrideValue: ({ value, currentValue }) => {
// if both speed and accuracy are off, set opposite to on
// i dedicate this fix to AshesOfAFallen and our 2 collective brain cells
if (value[0] === "off" && value[1] === "off") {
const changedIndex = value[0] === currentValue[0] ? 0 : 1;
value[changedIndex] = "on";
}
return value;
},
},
monkey: {
displayString: "monkey",
changeRequiresRestart: false,
},
monkeyPowerLevel: {
displayString: "monkey power level",
changeRequiresRestart: false,
},
// ads
ads: {
changeRequiresRestart: false,
isBlocked: ({ value }) => {
if (value !== "off" && isDevEnvironment()) {
Notifications.add("Ads are disabled in development mode.", 0);
return true;
}
return false;
},
afterSet: ({ nosave }) => {
if (!nosave && !isDevEnvironment()) {
reloadAfter(3);
Notifications.add("Ad settings changed. Refreshing...", 0);
}
},
},
};

View file

@ -2,7 +2,6 @@ import * as DB from "./db";
import * as Notifications from "./elements/notifications";
import { isConfigValueValid } from "./config-validation";
import * as ConfigEvent from "./observables/config-event";
import { isAuthenticated } from "./firebase";
import * as AccountButton from "./elements/account-button";
import { debounce } from "throttle-debounce";
import {
@ -11,10 +10,9 @@ import {
} from "./test/funbox/funbox-validation";
import {
createErrorMessage,
isDevEnvironment,
isObject,
promiseWithResolvers,
reloadAfter,
triggerResize,
typedKeys,
} from "./utils/misc";
import * as ConfigSchemas from "@monkeytype/schemas/configs";
@ -23,11 +21,11 @@ import { Mode } from "@monkeytype/schemas/shared";
import { Language } from "@monkeytype/schemas/languages";
import { LocalStorageWithSchema } from "./utils/local-storage-with-schema";
import { migrateConfig } from "./utils/config";
import { roundTo1 } from "@monkeytype/util/numbers";
import { getDefaultConfig } from "./constants/default-config";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { ZodSchema } from "zod";
import * as TestState from "./test/test-state";
import { ConfigMetadata, configMetadata } from "./config-metadata";
const configLS = new LocalStorageWithSchema({
key: "config",
@ -110,626 +108,6 @@ function isConfigChangeBlocked(): boolean {
return false;
}
// type SetBlock = {
// [K in keyof ConfigSchemas.Config]?: ConfigSchemas.Config[K][];
// };
// type RequiredConfig = {
// [K in keyof ConfigSchemas.Config]?: ConfigSchemas.Config[K];
// };
export type ConfigMetadata = {
[K in keyof ConfigSchemas.Config]: {
/**
* Optional display string for the config key.
*/
displayString?: string;
/**
* Should the config change trigger a resize event? handled in ui.ts:108
*/
triggerResize?: true;
/**
* Is a test restart required after this config change?
*/
changeRequiresRestart: boolean;
/**
* Optional function that checks if the config value is blocked from being set.
* Returns true if setting the config value should be blocked.
* @param value - The value being set for the config key.
*/
isBlocked?: (value: ConfigSchemas.Config[K]) => boolean;
/**
* Optional function to override the value before setting it.
* Returns the modified value.
* @param value - The value being set for the config key.
* @param currentValue - The current value of the config key.
*/
overrideValue?: (
value: ConfigSchemas.Config[K],
currentValue: ConfigSchemas.Config[K]
) => ConfigSchemas.Config[K];
/**
* Optional function to override other config values before this one is set.
* Returns an object with the config keys and their new values.
* @param value - The value being set for the config key.
*/
overrideConfig?: (
value: ConfigSchemas.Config[K]
) => Partial<ConfigSchemas.Config>;
/**
* Optional function that is called after the config value is set.
* It can be used to perform additional actions, like reloading the page.
* @param nosave - If true, the change is not saved to localStorage or database.
*/
afterSet?: (nosave: boolean) => void;
};
};
//todo:
// maybe have generic set somehow handle test restarting
// maybe add config group to each metadata object? all though its already defined in ConfigGroupsLiteral
const configMetadata: ConfigMetadata = {
// test
punctuation: {
changeRequiresRestart: true,
overrideValue: (value) => {
if (config.mode === "quote") {
return false;
}
return value;
},
},
numbers: {
changeRequiresRestart: true,
overrideValue: (value) => {
if (config.mode === "quote") {
return false;
}
return value;
},
},
words: {
displayString: "word count",
changeRequiresRestart: true,
},
time: {
changeRequiresRestart: true,
displayString: "time",
},
mode: {
changeRequiresRestart: true,
overrideConfig: (value) => {
if (value === "custom" || value === "quote" || value === "zen") {
return {
numbers: false,
punctuation: false,
};
}
return {};
},
afterSet: () => {
if (config.mode === "zen" && config.paceCaret !== "off") {
Notifications.add(`Pace caret will not work with zen mode.`, 0);
}
},
},
quoteLength: {
displayString: "quote length",
changeRequiresRestart: true,
},
language: {
displayString: "language",
changeRequiresRestart: true,
},
burstHeatmap: {
displayString: "burst heatmap",
changeRequiresRestart: false,
},
// behavior
difficulty: {
changeRequiresRestart: true,
},
quickRestart: {
displayString: "quick restart",
changeRequiresRestart: false,
},
repeatQuotes: {
displayString: "repeat quotes",
changeRequiresRestart: false,
},
blindMode: {
displayString: "blind mode",
changeRequiresRestart: false,
},
alwaysShowWordsHistory: {
displayString: "always show words history",
changeRequiresRestart: false,
},
singleListCommandLine: {
displayString: "single list command line",
changeRequiresRestart: false,
},
minWpm: {
displayString: "min speed",
changeRequiresRestart: true,
},
minWpmCustomSpeed: {
displayString: "min speed custom",
changeRequiresRestart: true,
},
minAcc: {
displayString: "min accuracy",
changeRequiresRestart: true,
},
minAccCustom: {
displayString: "min accuracy custom",
changeRequiresRestart: true,
},
minBurst: {
displayString: "min burst",
changeRequiresRestart: true,
},
minBurstCustomSpeed: {
displayString: "min burst custom speed",
changeRequiresRestart: true,
},
britishEnglish: {
displayString: "british english",
changeRequiresRestart: true,
},
funbox: {
changeRequiresRestart: true,
isBlocked: (value) => {
for (const funbox of config.funbox) {
if (!canSetFunboxWithConfig(funbox, config)) {
Notifications.add(
`${value}" cannot be enabled with the current config`,
0
);
return true;
}
}
return false;
},
},
customLayoutfluid: {
displayString: "custom layoutfluid",
changeRequiresRestart: true,
overrideValue: (value) => {
return Array.from(new Set(value));
},
},
customPolyglot: {
displayString: "custom polyglot",
changeRequiresRestart: false,
overrideValue: (value) => {
return Array.from(new Set(value));
},
},
// input
freedomMode: {
changeRequiresRestart: false,
displayString: "freedom mode",
overrideConfig: (value) => {
if (value) {
return {
confidenceMode: "off",
};
}
return {};
},
},
strictSpace: {
displayString: "strict space",
changeRequiresRestart: true,
},
oppositeShiftMode: {
displayString: "opposite shift mode",
changeRequiresRestart: false,
},
stopOnError: {
displayString: "stop on error",
changeRequiresRestart: true,
overrideConfig: (value) => {
if (value !== "off") {
return {
confidenceMode: "off",
};
}
return {};
},
},
confidenceMode: {
displayString: "confidence mode",
changeRequiresRestart: false,
overrideConfig: (value) => {
if (value !== "off") {
return {
freedomMode: false,
stopOnError: "off",
};
}
return {};
},
},
quickEnd: {
displayString: "quick end",
changeRequiresRestart: false,
},
indicateTypos: {
displayString: "indicate typos",
changeRequiresRestart: false,
},
hideExtraLetters: {
displayString: "hide extra letters",
changeRequiresRestart: false,
},
lazyMode: {
displayString: "lazy mode",
changeRequiresRestart: true,
},
layout: {
displayString: "layout",
changeRequiresRestart: true,
},
codeUnindentOnBackspace: {
displayString: "code unindent on backspace",
changeRequiresRestart: true,
},
// sound
soundVolume: {
displayString: "sound volume",
changeRequiresRestart: false,
},
playSoundOnClick: {
displayString: "play sound on click",
changeRequiresRestart: false,
},
playSoundOnError: {
displayString: "play sound on error",
changeRequiresRestart: false,
},
playTimeWarning: {
displayString: "play time warning",
changeRequiresRestart: false,
},
// caret
smoothCaret: {
displayString: "smooth caret",
changeRequiresRestart: false,
},
caretStyle: {
displayString: "caret style",
changeRequiresRestart: false,
},
paceCaret: {
displayString: "pace caret",
changeRequiresRestart: false,
isBlocked: (value) => {
if (document.readyState === "complete") {
if ((value === "pb" || value === "tagPb") && !isAuthenticated()) {
Notifications.add(
`Pace caret "pb" and "tag pb" are unavailable without an account`,
0
);
return true;
}
}
return false;
},
},
paceCaretCustomSpeed: {
displayString: "pace caret custom speed",
changeRequiresRestart: false,
},
paceCaretStyle: {
displayString: "pace caret style",
changeRequiresRestart: false,
},
repeatedPace: {
displayString: "repeated pace",
changeRequiresRestart: false,
},
// appearance
timerStyle: {
displayString: "timer style",
changeRequiresRestart: false,
},
liveSpeedStyle: {
displayString: "live speed style",
changeRequiresRestart: false,
},
liveAccStyle: {
displayString: "live accuracy style",
changeRequiresRestart: false,
},
liveBurstStyle: {
displayString: "live burst style",
changeRequiresRestart: false,
},
timerColor: {
displayString: "timer color",
changeRequiresRestart: false,
},
timerOpacity: {
displayString: "timer opacity",
changeRequiresRestart: false,
},
highlightMode: {
displayString: "highlight mode",
changeRequiresRestart: false,
},
tapeMode: {
triggerResize: true,
changeRequiresRestart: false,
displayString: "tape mode",
overrideConfig: (value) => {
if (value !== "off") {
return {
showAllLines: false,
};
}
return {};
},
},
tapeMargin: {
displayString: "tape margin",
changeRequiresRestart: false,
overrideValue: (value) => {
//TODO move to migration after settings validation
if (value < 10) {
value = 10;
}
if (value > 90) {
value = 90;
}
return value;
},
},
smoothLineScroll: {
displayString: "smooth line scroll",
changeRequiresRestart: false,
},
showAllLines: {
changeRequiresRestart: false,
displayString: "show all lines",
isBlocked: (value) => {
if (value && config.tapeMode !== "off") {
Notifications.add("Show all lines doesn't support tape mode.", 0);
return true;
}
return false;
},
},
alwaysShowDecimalPlaces: {
displayString: "always show decimal places",
changeRequiresRestart: false,
},
typingSpeedUnit: {
displayString: "typing speed unit",
changeRequiresRestart: false,
},
startGraphsAtZero: {
displayString: "start graphs at zero",
changeRequiresRestart: false,
},
maxLineWidth: {
changeRequiresRestart: false,
triggerResize: true,
displayString: "max line width",
overrideValue: (value) => {
//TODO move to migration after settings validation
if (value < 20 && value !== 0) {
value = 20;
}
if (value > 1000) {
value = 1000;
}
return value;
},
},
fontSize: {
changeRequiresRestart: false,
triggerResize: true,
displayString: "font size",
overrideValue: (value) => {
//TODO move to migration after settings validation
if (value < 0) {
value = 1;
}
return value;
},
},
fontFamily: {
displayString: "font family",
changeRequiresRestart: false,
},
keymapMode: {
displayString: "keymap mode",
changeRequiresRestart: false,
},
keymapLayout: {
displayString: "keymap layout",
changeRequiresRestart: false,
},
keymapStyle: {
displayString: "keymap style",
changeRequiresRestart: false,
},
keymapLegendStyle: {
displayString: "keymap legend style",
changeRequiresRestart: false,
},
keymapShowTopRow: {
displayString: "keymap show top row",
changeRequiresRestart: false,
},
keymapSize: {
triggerResize: true,
changeRequiresRestart: false,
displayString: "keymap size",
overrideValue: (value) => {
if (value < 0.5) value = 0.5;
if (value > 3.5) value = 3.5;
return roundTo1(value);
},
},
// theme
flipTestColors: {
displayString: "flip test colors",
changeRequiresRestart: false,
},
colorfulMode: {
displayString: "colorful mode",
changeRequiresRestart: false,
},
customBackground: {
displayString: "custom background",
changeRequiresRestart: false,
overrideValue: (value) => {
return value.trim();
},
},
customBackgroundSize: {
displayString: "custom background size",
changeRequiresRestart: false,
},
customBackgroundFilter: {
displayString: "custom background filter",
changeRequiresRestart: false,
},
autoSwitchTheme: {
displayString: "auto switch theme",
changeRequiresRestart: false,
},
themeLight: {
displayString: "theme light",
changeRequiresRestart: false,
},
themeDark: {
displayString: "theme dark",
changeRequiresRestart: false,
},
randomTheme: {
changeRequiresRestart: false,
displayString: "random theme",
isBlocked: (value) => {
if (value === "custom") {
const snapshot = DB.getSnapshot();
if (!isAuthenticated()) {
Notifications.add(
"Random theme 'custom' is unavailable without an account",
0
);
return true;
}
if (!snapshot) {
Notifications.add(
"Random theme 'custom' requires a snapshot to be set",
0
);
return true;
}
if (snapshot?.customThemes?.length === 0) {
Notifications.add(
"Random theme 'custom' requires at least one custom theme to be saved",
0
);
return true;
}
}
return false;
},
},
favThemes: {
displayString: "favorite themes",
changeRequiresRestart: false,
},
theme: {
changeRequiresRestart: false,
overrideConfig: () => {
return {
customTheme: false,
};
},
},
customTheme: {
displayString: "custom theme",
changeRequiresRestart: false,
},
customThemeColors: {
displayString: "custom theme colors",
changeRequiresRestart: false,
},
// hide elements
showKeyTips: {
displayString: "show key tips",
changeRequiresRestart: false,
},
showOutOfFocusWarning: {
displayString: "show out of focus warning",
changeRequiresRestart: false,
},
capsLockWarning: {
displayString: "caps lock warning",
changeRequiresRestart: false,
},
showAverage: {
displayString: "show average",
changeRequiresRestart: false,
},
// other (hidden)
accountChart: {
displayString: "account chart",
changeRequiresRestart: false,
overrideValue: (value, currentValue) => {
// if both speed and accuracy are off, set opposite to on
// i dedicate this fix to AshesOfAFallen and our 2 collective brain cells
if (value[0] === "off" && value[1] === "off") {
const changedIndex = value[0] === currentValue[0] ? 0 : 1;
value[changedIndex] = "on";
}
return value;
},
},
monkey: {
displayString: "monkey",
changeRequiresRestart: false,
},
monkeyPowerLevel: {
displayString: "monkey power level",
changeRequiresRestart: false,
},
// ads
ads: {
changeRequiresRestart: false,
isBlocked: (value) => {
if (value !== "off" && isDevEnvironment()) {
Notifications.add("Ads are disabled in development mode.", 0);
return true;
}
return false;
},
afterSet: (nosave) => {
if (!nosave && !isDevEnvironment()) {
reloadAfter(3);
Notifications.add("Ad settings changed. Refreshing...", 0);
}
},
},
};
export function genericSet<T extends keyof ConfigSchemas.Config>(
key: T,
value: ConfigSchemas.Config[T],
@ -772,12 +150,16 @@ export function genericSet<T extends keyof ConfigSchemas.Config>(
// }
// }
if (metadata.isBlocked?.(value)) {
if (metadata.isBlocked?.({ value, currentConfig: config })) {
return false;
}
if (metadata.overrideValue) {
value = metadata.overrideValue(value, config[key]);
value = metadata.overrideValue({
value,
currentValue: config[key],
currentConfig: config,
});
}
const schema = ConfigSchemas.ConfigSchema.shape[key] as ZodSchema;
@ -791,7 +173,10 @@ export function genericSet<T extends keyof ConfigSchemas.Config>(
}
if (metadata.overrideConfig) {
const targetConfig = metadata.overrideConfig(value);
const targetConfig = metadata.overrideConfig({
value,
currentConfig: config,
});
for (const targetKey of typedKeys(targetConfig)) {
const targetValue = targetConfig[
@ -816,10 +201,10 @@ export function genericSet<T extends keyof ConfigSchemas.Config>(
ConfigEvent.dispatch(key, value, nosave, previousValue);
if (metadata.triggerResize && !nosave) {
$(window).trigger("resize");
triggerResize();
}
metadata.afterSet?.(nosave || false);
metadata.afterSet?.({ nosave: nosave || false, currentConfig: config });
return true;
}

View file

@ -760,4 +760,7 @@ export function sanitize<T extends z.ZodTypeAny>(
) as z.infer<T>;
}
export function triggerResize(): void {
$(window).trigger("resize");
}
// DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES

View file

@ -133,7 +133,7 @@ const list: Record<FunboxName, FunboxMetadata> = {
layout_mirror: {
description: "Mirror the keyboard layout",
canGetPb: true,
difficultyLevel: 1,
difficultyLevel: 3,
properties: ["changesLayout"],
frontendFunctions: ["applyConfig", "rememberSettings"],
name: "layout_mirror",