mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-01-15 03:44:31 +08:00
Merge branch 'master' into feature/track-user-email-verification
This commit is contained in:
commit
157b280533
6 changed files with 1134 additions and 1058 deletions
278
frontend/__tests__/root/config-metadata.spec.ts
Normal file
278
frontend/__tests__/root/config-metadata.spec.ts
Normal 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 ?? {});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
635
frontend/src/ts/config-metadata.ts
Normal file
635
frontend/src/ts/config-metadata.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue