mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-01-11 09:55:13 +08:00
refactor(config): config metadata (@miodec) (#6753)
Create config metadata object Move all the special code on config change to config listeners Create a generic set function which will work with the metadata object to update any config key Update all setters to use the generic set. (Later probably only use the generic settter and remove all the specific ones) Also orders config groups and config schema. --------- Co-authored-by: Christian Fehmer <cfe@sexy-developer.com>
This commit is contained in:
parent
db9e54f794
commit
92790f3682
22 changed files with 1737 additions and 1733 deletions
|
|
@ -100,8 +100,8 @@ describe("ConfigController", () => {
|
|||
expect(body).toStrictEqual({
|
||||
message: "Invalid request data schema",
|
||||
validationErrors: [
|
||||
`"autoSwitchTheme" Expected boolean, received string`,
|
||||
`"confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
|
||||
`"autoSwitchTheme" Expected boolean, received string`,
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -249,8 +249,8 @@ describe("PresetController", () => {
|
|||
expect(body).toStrictEqual({
|
||||
message: "Invalid request data schema",
|
||||
validationErrors: [
|
||||
`"config.autoSwitchTheme" Expected boolean, received string`,
|
||||
`"config.confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
|
||||
`"config.autoSwitchTheme" Expected boolean, received string`,
|
||||
`"config" Unrecognized key(s) in object: 'extra'`,
|
||||
`Unrecognized key(s) in object: '_id', 'extra'`,
|
||||
],
|
||||
|
|
@ -427,9 +427,9 @@ describe("PresetController", () => {
|
|||
expect(body).toStrictEqual({
|
||||
message: "Invalid request data schema",
|
||||
validationErrors: [
|
||||
`"settingGroups.0" Invalid enum value. Expected 'test' | 'behavior' | 'input' | 'sound' | 'caret' | 'appearance' | 'theme' | 'hideElements' | 'ads' | 'hidden', received 'mappers'`,
|
||||
`"config.autoSwitchTheme" Expected boolean, received string`,
|
||||
`"settingGroups.0" Invalid enum value. Expected 'test' | 'behavior' | 'input' | 'sound' | 'caret' | 'appearance' | 'theme' | 'hideElements' | 'hidden' | 'ads', received 'mappers'`,
|
||||
`"config.confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
|
||||
`"config.autoSwitchTheme" Expected boolean, received string`,
|
||||
`"config" Unrecognized key(s) in object: 'extra'`,
|
||||
`Unrecognized key(s) in object: 'extra'`,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,12 +1,499 @@
|
|||
import * as Config from "../../src/ts/config";
|
||||
|
||||
import * as Misc from "../../src/ts/utils/misc";
|
||||
import {
|
||||
CustomThemeColors,
|
||||
FunboxName,
|
||||
ConfigKey,
|
||||
Config as ConfigType,
|
||||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { randomBytes } from "crypto";
|
||||
import { vi } from "vitest";
|
||||
import * as FunboxValidation from "../../src/ts/test/funbox/funbox-validation";
|
||||
import * as ConfigValidation from "../../src/ts/config-validation";
|
||||
import * as ConfigEvent from "../../src/ts/observables/config-event";
|
||||
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;
|
||||
|
||||
describe("Config", () => {
|
||||
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");
|
||||
beforeEach(() => isDevEnvironmentMock.mockReset());
|
||||
|
||||
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()
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if config key in not found in metadata", () => {
|
||||
expect(() => {
|
||||
Config.genericSet("nonExistentKey" as ConfigKey, true);
|
||||
}).toThrowError(
|
||||
`Config metadata for key "nonExistentKey" is not defined.`
|
||||
);
|
||||
});
|
||||
|
||||
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 ?? {});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("test with mocks", () => {
|
||||
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 mocks = [
|
||||
canSetConfigWithCurrentFunboxesMock,
|
||||
isConfigValueValidMock,
|
||||
dispatchConfigEventMock,
|
||||
dbSaveConfigMock,
|
||||
accountButtonLoadingMock,
|
||||
notificationAddMock,
|
||||
miscReloadAfterMock,
|
||||
];
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mocks.forEach((it) => it.mockRestore());
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("cannot set if funbox disallows", () => {
|
||||
//GIVEN
|
||||
canSetConfigWithCurrentFunboxesMock.mockReturnValue(false);
|
||||
|
||||
//WHEN / THEN
|
||||
expect(Config.genericSet("numbers", true)).toBe(false);
|
||||
});
|
||||
|
||||
it("fails if config is invalid", () => {
|
||||
//GIVEN
|
||||
isConfigValueValidMock.mockReturnValue(false);
|
||||
|
||||
//WHEN / THEN
|
||||
expect(Config.genericSet("numbers", "off" as any)).toBe(false);
|
||||
});
|
||||
|
||||
it("dispatches event on set", () => {
|
||||
//GIVEN
|
||||
replaceConfig({ numbers: false });
|
||||
|
||||
//WHEN
|
||||
Config.genericSet("numbers", true, true);
|
||||
|
||||
//THEN
|
||||
|
||||
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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("setMode", () => {
|
||||
expect(Config.setMode("zen")).toBe(true);
|
||||
expect(Config.setMode("invalid" as any)).toBe(false);
|
||||
|
|
@ -33,7 +520,7 @@ describe("Config", () => {
|
|||
it("setAccountChart", () => {
|
||||
expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true);
|
||||
//arrays not having 4 values will get [on, on, on, on] as default
|
||||
expect(Config.setAccountChart(["on", "off"] as any)).toBe(true);
|
||||
expect(Config.setAccountChart(["on", "off"] as any)).toBe(false);
|
||||
expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe(
|
||||
false
|
||||
);
|
||||
|
|
@ -199,13 +686,13 @@ describe("Config", () => {
|
|||
|
||||
//invalid values being "auto-fixed"
|
||||
expect(Config.setKeymapSize(0)).toBe(true);
|
||||
expect(Config.default.keymapSize).toBe(0.5);
|
||||
expect(getConfig().keymapSize).toBe(0.5);
|
||||
expect(Config.setKeymapSize(4)).toBe(true);
|
||||
expect(Config.default.keymapSize).toBe(3.5);
|
||||
expect(getConfig().keymapSize).toBe(3.5);
|
||||
expect(Config.setKeymapSize(1.25)).toBe(true);
|
||||
expect(Config.default.keymapSize).toBe(1.3);
|
||||
expect(getConfig().keymapSize).toBe(1.3);
|
||||
expect(Config.setKeymapSize(1.24)).toBe(true);
|
||||
expect(Config.default.keymapSize).toBe(1.2);
|
||||
expect(getConfig().keymapSize).toBe(1.2);
|
||||
});
|
||||
it("setCustomBackgroundSize", () => {
|
||||
expect(Config.setCustomBackgroundSize("contain")).toBe(true);
|
||||
|
|
@ -214,8 +701,10 @@ describe("Config", () => {
|
|||
});
|
||||
it("setCustomBackgroundFilter", () => {
|
||||
expect(Config.setCustomBackgroundFilter([0, 1, 2, 3])).toBe(true);
|
||||
//gets converted
|
||||
expect(Config.setCustomBackgroundFilter([0, 1, 2, 3, 4] as any)).toBe(true);
|
||||
|
||||
expect(Config.setCustomBackgroundFilter([0, 1, 2, 3, 4] as any)).toBe(
|
||||
false
|
||||
);
|
||||
expect(Config.setCustomBackgroundFilter([] as any)).toBe(false);
|
||||
expect(Config.setCustomBackgroundFilter(["invalid"] as any)).toBe(false);
|
||||
expect(Config.setCustomBackgroundFilter([1, 2, 3, 4, 5, 6] as any)).toBe(
|
||||
|
|
@ -231,9 +720,7 @@ describe("Config", () => {
|
|||
it("setCustomThemeColors", () => {
|
||||
expect(Config.setCustomThemeColors(customThemeColors(10))).toBe(true);
|
||||
|
||||
//gets converted
|
||||
expect(Config.setCustomThemeColors(customThemeColors(9))).toBe(true);
|
||||
|
||||
expect(Config.setCustomThemeColors(customThemeColors(9))).toBe(false);
|
||||
expect(Config.setCustomThemeColors([] as any)).toBe(false);
|
||||
expect(Config.setCustomThemeColors(["invalid"] as any)).toBe(false);
|
||||
expect(Config.setCustomThemeColors(customThemeColors(5))).toBe(false);
|
||||
|
|
@ -258,7 +745,7 @@ describe("Config", () => {
|
|||
});
|
||||
it("setAccountChart", () => {
|
||||
expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true);
|
||||
expect(Config.setAccountChart(["on", "off"] as any)).toBe(true);
|
||||
expect(Config.setAccountChart(["on", "off"] as any)).toBe(false);
|
||||
expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe(
|
||||
false
|
||||
);
|
||||
|
|
@ -358,8 +845,6 @@ describe("Config", () => {
|
|||
expect(Config.setMinAccCustom(0)).toBe(true);
|
||||
expect(Config.setMinAccCustom(1)).toBe(true);
|
||||
expect(Config.setMinAccCustom(11.11)).toBe(true);
|
||||
//gets converted
|
||||
expect(Config.setMinAccCustom(120)).toBe(true);
|
||||
|
||||
expect(Config.setMinAccCustom("invalid" as any)).toBe(false);
|
||||
expect(Config.setMinAccCustom(-1)).toBe(false);
|
||||
|
|
@ -376,19 +861,12 @@ describe("Config", () => {
|
|||
expect(Config.setTimeConfig(0)).toBe(true);
|
||||
expect(Config.setTimeConfig(1)).toBe(true);
|
||||
|
||||
//gets converted
|
||||
expect(Config.setTimeConfig("invalid" as any)).toBe(true);
|
||||
expect(Config.setTimeConfig(-1)).toBe(true);
|
||||
|
||||
expect(Config.setTimeConfig(11.11)).toBe(false);
|
||||
});
|
||||
it("setWordCount", () => {
|
||||
expect(Config.setWordCount(0)).toBe(true);
|
||||
expect(Config.setWordCount(1)).toBe(true);
|
||||
|
||||
//gets converted
|
||||
expect(Config.setWordCount(-1)).toBe(true);
|
||||
|
||||
expect(Config.setWordCount("invalid" as any)).toBe(false);
|
||||
expect(Config.setWordCount(11.11)).toBe(false);
|
||||
});
|
||||
|
|
@ -486,12 +964,14 @@ describe("Config", () => {
|
|||
expect(Config.setCustomBackground("invalid")).toBe(false);
|
||||
});
|
||||
it("setQuoteLength", () => {
|
||||
expect(Config.setQuoteLength(0)).toBe(true);
|
||||
expect(Config.setQuoteLength(-3)).toBe(true);
|
||||
expect(Config.setQuoteLength(3)).toBe(true);
|
||||
expect(Config.setQuoteLength([0])).toBe(true);
|
||||
expect(Config.setQuoteLength([-3])).toBe(true);
|
||||
expect(Config.setQuoteLength([3])).toBe(true);
|
||||
|
||||
expect(Config.setQuoteLength(-4 as any)).toBe(false);
|
||||
expect(Config.setQuoteLength(4 as any)).toBe(false);
|
||||
expect(Config.setQuoteLength(3 as any)).toBe(false);
|
||||
expect(Config.setQuoteLength(2 as any)).toBe(false);
|
||||
|
||||
expect(Config.setQuoteLength([0, -3, 2])).toBe(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ describe("url-handler", () => {
|
|||
|
||||
//THEN
|
||||
expect(setModeMock).toHaveBeenCalledWith("quote", true);
|
||||
expect(setQuoteLengthMock).toHaveBeenCalledWith(-2, false);
|
||||
expect(setQuoteLengthMock).toHaveBeenCalledWith([-2], false);
|
||||
expect(setSelectedQuoteIdMock).toHaveBeenCalledWith(512);
|
||||
expect(restartTestMock).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const commands: Command[] = [
|
|||
configValue: [0, 1, 2, 3],
|
||||
exec: (): void => {
|
||||
UpdateConfig.setMode("quote");
|
||||
UpdateConfig.setQuoteLength([0, 1, 2, 3]);
|
||||
UpdateConfig.setQuoteLengthAll();
|
||||
TestLogic.restart();
|
||||
},
|
||||
},
|
||||
|
|
@ -30,7 +30,7 @@ const commands: Command[] = [
|
|||
configValueMode: "include",
|
||||
exec: (): void => {
|
||||
UpdateConfig.setMode("quote");
|
||||
UpdateConfig.setQuoteLength(0);
|
||||
UpdateConfig.setQuoteLength([0]);
|
||||
TestLogic.restart();
|
||||
},
|
||||
},
|
||||
|
|
@ -41,7 +41,7 @@ const commands: Command[] = [
|
|||
configValueMode: "include",
|
||||
exec: (): void => {
|
||||
UpdateConfig.setMode("quote");
|
||||
UpdateConfig.setQuoteLength(1);
|
||||
UpdateConfig.setQuoteLength([1]);
|
||||
TestLogic.restart();
|
||||
},
|
||||
},
|
||||
|
|
@ -52,7 +52,7 @@ const commands: Command[] = [
|
|||
configValueMode: "include",
|
||||
exec: (): void => {
|
||||
UpdateConfig.setMode("quote");
|
||||
UpdateConfig.setQuoteLength(2);
|
||||
UpdateConfig.setQuoteLength([2]);
|
||||
TestLogic.restart();
|
||||
},
|
||||
},
|
||||
|
|
@ -63,7 +63,7 @@ const commands: Command[] = [
|
|||
configValueMode: "include",
|
||||
exec: (): void => {
|
||||
UpdateConfig.setMode("quote");
|
||||
UpdateConfig.setQuoteLength(3);
|
||||
UpdateConfig.setQuoteLength([3]);
|
||||
TestLogic.restart();
|
||||
},
|
||||
},
|
||||
|
|
@ -77,7 +77,7 @@ const commands: Command[] = [
|
|||
},
|
||||
exec: (): void => {
|
||||
UpdateConfig.setMode("quote");
|
||||
UpdateConfig.setQuoteLength(-3);
|
||||
UpdateConfig.setQuoteLength([-3]);
|
||||
TestLogic.restart();
|
||||
},
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -415,7 +415,36 @@ window
|
|||
}
|
||||
});
|
||||
|
||||
let ignoreConfigEvent = false;
|
||||
|
||||
ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
|
||||
if (eventKey === "fullConfigChange") {
|
||||
ignoreConfigEvent = true;
|
||||
}
|
||||
if (eventKey === "fullConfigChangeFinished") {
|
||||
ignoreConfigEvent = false;
|
||||
|
||||
await clearRandom();
|
||||
await clearPreview(false);
|
||||
if (Config.autoSwitchTheme) {
|
||||
if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) {
|
||||
await set(Config.themeDark, true);
|
||||
} else {
|
||||
await set(Config.themeLight, true);
|
||||
}
|
||||
} else {
|
||||
if (Config.customTheme) {
|
||||
await set("custom");
|
||||
} else {
|
||||
await set(Config.theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is here to prevent calling set / preview multiple times during a full config loading
|
||||
// once the full config is loaded, we can apply everything once
|
||||
if (ignoreConfigEvent) return;
|
||||
|
||||
if (eventKey === "randomTheme") {
|
||||
void changeThemeList();
|
||||
}
|
||||
|
|
@ -430,23 +459,6 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
|
|||
await clearPreview(false);
|
||||
await set(eventValue as string);
|
||||
}
|
||||
if (eventKey === "setThemes") {
|
||||
await clearRandom();
|
||||
await clearPreview(false);
|
||||
if (Config.autoSwitchTheme) {
|
||||
if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) {
|
||||
await set(Config.themeDark, true);
|
||||
} else {
|
||||
await set(Config.themeLight, true);
|
||||
}
|
||||
} else {
|
||||
if (eventValue as boolean) {
|
||||
await set("custom");
|
||||
} else {
|
||||
await set(Config.theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (eventKey === "randomTheme" && eventValue === "off") await clearRandom();
|
||||
if (eventKey === "customBackground") applyCustomBackground();
|
||||
if (eventKey === "customBackgroundSize") applyCustomBackgroundSize();
|
||||
|
|
|
|||
|
|
@ -930,7 +930,7 @@ export async function updateLbMemory<M extends Mode>(
|
|||
}
|
||||
}
|
||||
|
||||
export async function saveConfig(config: Config): Promise<void> {
|
||||
export async function saveConfig(config: Partial<Config>): Promise<void> {
|
||||
if (isAuthenticated()) {
|
||||
const response = await Ape.configs.save({ body: config });
|
||||
if (response.status !== 200) {
|
||||
|
|
|
|||
|
|
@ -595,8 +595,40 @@ ConfigEvent.subscribe((eventKey, newValue) => {
|
|||
void refresh();
|
||||
}
|
||||
if (eventKey === "keymapMode") {
|
||||
$(".activeKey").removeClass("activeKey");
|
||||
$(".keymapKey").attr("style", "");
|
||||
newValue === "off" ? hide() : show();
|
||||
}
|
||||
if (eventKey === "keymapSize") {
|
||||
$("#keymap").css("zoom", newValue as string);
|
||||
}
|
||||
if (eventKey === "keymapLegendStyle") {
|
||||
let style = newValue as string;
|
||||
|
||||
// Remove existing styles
|
||||
const keymapLegendStyles = ["lowercase", "uppercase", "blank", "dynamic"];
|
||||
keymapLegendStyles.forEach((name) => {
|
||||
$(".keymapLegendStyle").removeClass(name);
|
||||
});
|
||||
|
||||
style = style || "lowercase";
|
||||
|
||||
// Mutate the keymap in the DOM, if it exists.
|
||||
// 1. Remove everything
|
||||
$(".keymapKey > .letter").css("display", "");
|
||||
$(".keymapKey > .letter").css("text-transform", "");
|
||||
|
||||
// 2. Append special styles onto the DOM elements
|
||||
if (style === "uppercase") {
|
||||
$(".keymapKey > .letter").css("text-transform", "capitalize");
|
||||
}
|
||||
if (style === "blank") {
|
||||
$(".keymapKey > .letter").css("display", "none");
|
||||
}
|
||||
|
||||
// Update and save to cookie for persistence
|
||||
$(".keymapLegendStyle").addClass(style);
|
||||
}
|
||||
});
|
||||
|
||||
KeymapEvent.subscribe((mode, key, correct) => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import * as CustomTestDurationPopup from "./custom-test-duration";
|
|||
import * as QuoteSearchModal from "./quote-search";
|
||||
import * as CustomTextPopup from "./custom-text";
|
||||
import AnimatedModal from "../utils/animated-modal";
|
||||
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
|
||||
import {
|
||||
QuoteLength,
|
||||
QuoteLengthConfig,
|
||||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { Mode } from "@monkeytype/contracts/schemas/shared";
|
||||
|
||||
function update(): void {
|
||||
|
|
@ -126,22 +129,25 @@ async function setup(modalEl: HTMLElement): Promise<void> {
|
|||
for (const button of quoteGroupButtons) {
|
||||
button.addEventListener("click", (e) => {
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
const len = parseInt(target.getAttribute("data-quoteLength") ?? "0", 10);
|
||||
const len = parseInt(
|
||||
target.getAttribute("data-quoteLength") ?? "0",
|
||||
10
|
||||
) as QuoteLength;
|
||||
|
||||
if (len === -2) {
|
||||
void QuoteSearchModal.show({
|
||||
modalChain: modal,
|
||||
});
|
||||
} else {
|
||||
let newVal: number[] | number = len;
|
||||
if (len === -1) {
|
||||
newVal = [0, 1, 2, 3];
|
||||
let arr: QuoteLengthConfig = [];
|
||||
|
||||
if ((e as MouseEvent).shiftKey) {
|
||||
arr = [...Config.quoteLength, len];
|
||||
} else {
|
||||
arr = [len];
|
||||
}
|
||||
UpdateConfig.setQuoteLength(
|
||||
newVal as QuoteLength | QuoteLength[],
|
||||
false,
|
||||
(e as MouseEvent).shiftKey
|
||||
);
|
||||
|
||||
UpdateConfig.setQuoteLength(arr, false);
|
||||
ManualRestart.set();
|
||||
TestLogic.restart();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import * as TestState from "../test/test-state";
|
|||
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
|
||||
import * as TestLogic from "../test/test-logic";
|
||||
import { createErrorMessage } from "../utils/misc";
|
||||
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
|
||||
|
||||
const searchServiceCache: Record<string, SearchService<Quote>> = {};
|
||||
|
||||
|
|
@ -326,7 +325,7 @@ function apply(val: number): void {
|
|||
);
|
||||
}
|
||||
if (val !== null && !isNaN(val) && val >= 0) {
|
||||
UpdateConfig.setQuoteLength(-2 as QuoteLength, false);
|
||||
UpdateConfig.setQuoteLength([-2], false);
|
||||
TestState.setSelectedQuoteId(val);
|
||||
ManualRestart.set();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { findLineByLeastSquares } from "../utils/numbers";
|
|||
import defaultResultFilters from "../constants/default-result-filters";
|
||||
import { SnapshotResult } from "../constants/default-snapshot";
|
||||
import Ape from "../ape";
|
||||
import { AccountChart } from "@monkeytype/contracts/schemas/configs";
|
||||
|
||||
let filterDebug = false;
|
||||
//toggle filterdebug
|
||||
|
|
@ -1122,25 +1123,25 @@ function sortAndRefreshHistory(
|
|||
}
|
||||
|
||||
$(".pageAccount button.toggleResultsOnChart").on("click", () => {
|
||||
const newValue = Config.accountChart;
|
||||
const newValue = [...Config.accountChart] as AccountChart;
|
||||
newValue[0] = newValue[0] === "on" ? "off" : "on";
|
||||
UpdateConfig.setAccountChart(newValue);
|
||||
});
|
||||
|
||||
$(".pageAccount button.toggleAccuracyOnChart").on("click", () => {
|
||||
const newValue = Config.accountChart;
|
||||
const newValue = [...Config.accountChart] as AccountChart;
|
||||
newValue[1] = newValue[1] === "on" ? "off" : "on";
|
||||
UpdateConfig.setAccountChart(newValue);
|
||||
});
|
||||
|
||||
$(".pageAccount button.toggleAverage10OnChart").on("click", () => {
|
||||
const newValue = Config.accountChart;
|
||||
const newValue = [...Config.accountChart] as AccountChart;
|
||||
newValue[2] = newValue[2] === "on" ? "off" : "on";
|
||||
UpdateConfig.setAccountChart(newValue);
|
||||
});
|
||||
|
||||
$(".pageAccount button.toggleAverage100OnChart").on("click", () => {
|
||||
const newValue = Config.accountChart;
|
||||
const newValue = [...Config.accountChart] as AccountChart;
|
||||
newValue[3] = newValue[3] === "on" ? "off" : "on";
|
||||
UpdateConfig.setAccountChart(newValue);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -270,11 +270,26 @@ export async function updatePosition(noAnim = false): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
function updateStyle(): void {
|
||||
caret.style.width = "";
|
||||
caret.classList.remove(
|
||||
...["off", "default", "underline", "outline", "block", "carrot", "banana"]
|
||||
);
|
||||
caret.classList.add(Config.caretStyle);
|
||||
}
|
||||
|
||||
subscribe((eventKey) => {
|
||||
if (eventKey === "caretStyle") {
|
||||
caret.style.width = "";
|
||||
updateStyle();
|
||||
void updatePosition(true);
|
||||
}
|
||||
if (eventKey === "smoothCaret") {
|
||||
if (Config.smoothCaret === "off") {
|
||||
caret.style.animationName = "caretFlashHard";
|
||||
} else {
|
||||
caret.style.animationName = "caretFlashSmooth";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function show(noAnim = false): void {
|
||||
|
|
|
|||
|
|
@ -322,6 +322,21 @@ export function start(): void {
|
|||
void update(performance.now() + (settings?.spc ?? 0) * 1000);
|
||||
}
|
||||
|
||||
function updateStyle(): void {
|
||||
const paceCaret = $("#paceCaret");
|
||||
paceCaret.removeClass([
|
||||
"off",
|
||||
"default",
|
||||
"underline",
|
||||
"outline",
|
||||
"block",
|
||||
"carrot",
|
||||
"banana",
|
||||
]);
|
||||
paceCaret.addClass(Config.paceCaretStyle);
|
||||
}
|
||||
|
||||
ConfigEvent.subscribe((eventKey) => {
|
||||
if (eventKey === "paceCaret") void init();
|
||||
if (eventKey === "paceCaretStyle") updateStyle();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import Config from "../config";
|
|||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import { applyReducedMotion } from "../utils/misc";
|
||||
import { areUnsortedArraysEqual } from "../utils/arrays";
|
||||
|
||||
export function show(): void {
|
||||
$("#testConfig").removeClass("invisible");
|
||||
|
|
@ -225,11 +226,18 @@ export function updateExtras(key: string, value: ConfigValue): void {
|
|||
).addClass("active");
|
||||
} else if (key === "quoteLength") {
|
||||
$("#testConfig .quoteLength .textButton").removeClass("active");
|
||||
(value as QuoteLength[]).forEach((ql) => {
|
||||
$(
|
||||
"#testConfig .quoteLength .textButton[quoteLength='" + ql + "']"
|
||||
).addClass("active");
|
||||
});
|
||||
|
||||
if (areUnsortedArraysEqual(value as QuoteLength[], [0, 1, 2, 3])) {
|
||||
$("#testConfig .quoteLength .textButton[quoteLength='-1']").addClass(
|
||||
"active"
|
||||
);
|
||||
} else {
|
||||
(value as QuoteLength[]).forEach((ql) => {
|
||||
$(
|
||||
"#testConfig .quoteLength .textButton[quoteLength='" + ql + "']"
|
||||
).addClass("active");
|
||||
});
|
||||
}
|
||||
} else if (key === "numbers") {
|
||||
if (value === false) {
|
||||
$("#testConfig .numbersMode.textButton").removeClass("active");
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@ import * as KeymapEvent from "../observables/keymap-event";
|
|||
import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer";
|
||||
import * as ArabicLazyMode from "../states/arabic-lazy-mode";
|
||||
import Format from "../utils/format";
|
||||
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
|
||||
import {
|
||||
QuoteLength,
|
||||
QuoteLengthConfig,
|
||||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { Mode } from "@monkeytype/contracts/schemas/shared";
|
||||
import {
|
||||
CompletedEvent,
|
||||
|
|
@ -461,7 +464,7 @@ export async function init(): Promise<void | null> {
|
|||
|
||||
if (Config.mode === "quote") {
|
||||
if (Config.quoteLength.includes(-3) && !isAuthenticated()) {
|
||||
UpdateConfig.setQuoteLength(-1);
|
||||
UpdateConfig.setQuoteLengthAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1442,14 +1445,20 @@ $(".pageTest").on("click", "#testConfig .time .textButton", (e) => {
|
|||
|
||||
$(".pageTest").on("click", "#testConfig .quoteLength .textButton", (e) => {
|
||||
if (TestUI.testRestarting) return;
|
||||
let len: QuoteLength | QuoteLength[] = parseInt(
|
||||
const len = parseInt(
|
||||
$(e.currentTarget).attr("quoteLength") ?? "1"
|
||||
) as QuoteLength;
|
||||
|
||||
if (len !== -2) {
|
||||
if (len === -1) {
|
||||
len = [0, 1, 2, 3];
|
||||
let arr: QuoteLengthConfig = [];
|
||||
|
||||
if (e.shiftKey) {
|
||||
arr = [...Config.quoteLength, len];
|
||||
} else {
|
||||
arr = [len];
|
||||
}
|
||||
if (UpdateConfig.setQuoteLength(len, false, e.shiftKey)) {
|
||||
|
||||
if (UpdateConfig.setQuoteLength(arr, false)) {
|
||||
ManualRestart.set();
|
||||
restart();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1756,4 +1756,7 @@ ConfigEvent.subscribe((key, value) => {
|
|||
if (key === "timerColor") {
|
||||
updateLiveStatsColor(value as TimerColor);
|
||||
}
|
||||
if (key === "showOutOfFocusWarning" && value === false) {
|
||||
OutOfFocus.hide();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -525,7 +525,7 @@ async function getQuoteWordList(
|
|||
TestState.selectedQuoteId
|
||||
);
|
||||
if (targetQuote === undefined) {
|
||||
UpdateConfig.setQuoteLength(-1);
|
||||
UpdateConfig.setQuoteLengthAll();
|
||||
throw new WordGenError(
|
||||
`Quote ${TestState.selectedQuoteId} does not exist`
|
||||
);
|
||||
|
|
@ -536,14 +536,14 @@ async function getQuoteWordList(
|
|||
Config.language
|
||||
);
|
||||
if (randomQuote === null) {
|
||||
UpdateConfig.setQuoteLength(-1);
|
||||
UpdateConfig.setQuoteLengthAll();
|
||||
throw new WordGenError("No favorite quotes found");
|
||||
}
|
||||
rq = randomQuote;
|
||||
} else {
|
||||
const randomQuote = QuotesController.getRandomQuote();
|
||||
if (randomQuote === null) {
|
||||
UpdateConfig.setQuoteLength(-1);
|
||||
UpdateConfig.setQuoteLengthAll();
|
||||
throw new WordGenError("No quotes found for selected quote length");
|
||||
}
|
||||
rq = randomQuote;
|
||||
|
|
|
|||
|
|
@ -117,6 +117,28 @@ $(window).on("resize", () => {
|
|||
debouncedEvent();
|
||||
});
|
||||
|
||||
ConfigEvent.subscribe((eventKey) => {
|
||||
ConfigEvent.subscribe((eventKey, value) => {
|
||||
if (eventKey === "quickRestart") updateKeytips();
|
||||
if (eventKey === "showKeyTips") {
|
||||
if (Config.showKeyTips) {
|
||||
$("footer .keyTips").removeClass("hidden");
|
||||
} else {
|
||||
$("footer .keyTips").addClass("hidden");
|
||||
}
|
||||
}
|
||||
if (eventKey === "fontSize") {
|
||||
$("#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput").css(
|
||||
"fontSize",
|
||||
value + "rem"
|
||||
);
|
||||
}
|
||||
if (eventKey === "fontFamily") {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font",
|
||||
`"${(value as string).replace(
|
||||
/_/g,
|
||||
" "
|
||||
)}", "Roboto Mono", "Vazirmatn", monospace`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -156,5 +156,47 @@ export function replaceLegacyValues(
|
|||
configObj.fontSize = newValue;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(configObj.accountChart) &&
|
||||
configObj.accountChart.length !== 4
|
||||
) {
|
||||
configObj.accountChart = ["on", "on", "on", "on"];
|
||||
}
|
||||
|
||||
if (
|
||||
typeof configObj.minAccCustom === "number" &&
|
||||
configObj.minAccCustom > 100
|
||||
) {
|
||||
configObj.minAccCustom = 100;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(configObj.customThemeColors) &&
|
||||
//@ts-expect-error legacy configs
|
||||
configObj.customThemeColors.length === 9
|
||||
) {
|
||||
// migrate existing configs missing sub alt color
|
||||
const colors = configObj.customThemeColors;
|
||||
colors.splice(4, 0, "#000000");
|
||||
configObj.customThemeColors = colors;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(configObj.customBackgroundFilter) &&
|
||||
//@ts-expect-error legacy configs
|
||||
configObj.customBackgroundFilter.length === 5
|
||||
) {
|
||||
const arr = configObj.customBackgroundFilter;
|
||||
configObj.customBackgroundFilter = [arr[0], arr[1], arr[2], arr[3]];
|
||||
}
|
||||
|
||||
if (typeof configObj.quoteLength === "number") {
|
||||
if (configObj.quoteLength === -1) {
|
||||
configObj.quoteLength = [0, 1, 2, 3];
|
||||
} else {
|
||||
configObj.quoteLength = [configObj.quoteLength];
|
||||
}
|
||||
}
|
||||
|
||||
return configObj;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export function loadTestSettingsFromUrl(getOverride?: string): void {
|
|||
} else if (mode === "words") {
|
||||
UpdateConfig.setWordCount(parseInt(de[1], 10), true);
|
||||
} else if (mode === "quote") {
|
||||
UpdateConfig.setQuoteLength(-2, false);
|
||||
UpdateConfig.setQuoteLength([-2], false);
|
||||
TestState.setSelectedQuoteId(parseInt(de[1], 10));
|
||||
ManualRestart.set();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export type QuickRestart = z.infer<typeof QuickRestartSchema>;
|
|||
export const QuoteLengthSchema = z.union([
|
||||
z.literal(-3),
|
||||
z.literal(-2),
|
||||
z.literal(-1),
|
||||
z.literal(0),
|
||||
z.literal(1),
|
||||
z.literal(2),
|
||||
|
|
@ -21,7 +20,19 @@ export const QuoteLengthSchema = z.union([
|
|||
]);
|
||||
export type QuoteLength = z.infer<typeof QuoteLengthSchema>;
|
||||
|
||||
export const QuoteLengthConfigSchema = z.array(QuoteLengthSchema);
|
||||
export const QuoteLengthConfigSchema = z
|
||||
.array(QuoteLengthSchema)
|
||||
.describe(
|
||||
[
|
||||
"|value|description|\n|-|-|",
|
||||
"|-3|Favorite quotes|",
|
||||
"|-2|Quote search|",
|
||||
"|0|Short quotes|",
|
||||
"|1|Medium quotes|",
|
||||
"|2|Long quotes|",
|
||||
"|3|Thicc quotes|",
|
||||
].join("\n")
|
||||
);
|
||||
export type QuoteLengthConfig = z.infer<typeof QuoteLengthConfigSchema>;
|
||||
|
||||
export const CaretStyleSchema = z.enum([
|
||||
|
|
@ -346,18 +357,7 @@ export type CustomBackground = z.infer<typeof CustomBackgroundSchema>;
|
|||
|
||||
export const ConfigSchema = z
|
||||
.object({
|
||||
theme: ThemeNameSchema,
|
||||
themeLight: ThemeNameSchema,
|
||||
themeDark: ThemeNameSchema,
|
||||
autoSwitchTheme: z.boolean(),
|
||||
customTheme: z.boolean(),
|
||||
//customThemeId: token().nonnegative().max(24),
|
||||
customThemeColors: CustomThemeColorsSchema,
|
||||
favThemes: FavThemesSchema,
|
||||
showKeyTips: z.boolean(),
|
||||
smoothCaret: SmoothCaretSchema,
|
||||
codeUnindentOnBackspace: z.boolean(),
|
||||
quickRestart: QuickRestartSchema,
|
||||
// test
|
||||
punctuation: z.boolean(),
|
||||
numbers: z.boolean(),
|
||||
words: WordCountSchema,
|
||||
|
|
@ -365,76 +365,105 @@ export const ConfigSchema = z
|
|||
mode: Shared.ModeSchema,
|
||||
quoteLength: QuoteLengthConfigSchema,
|
||||
language: LanguageSchema,
|
||||
fontSize: FontSizeSchema,
|
||||
freedomMode: z.boolean(),
|
||||
burstHeatmap: z.boolean(),
|
||||
|
||||
// behavior
|
||||
difficulty: DifficultySchema,
|
||||
quickRestart: QuickRestartSchema,
|
||||
repeatQuotes: RepeatQuotesSchema,
|
||||
blindMode: z.boolean(),
|
||||
quickEnd: z.boolean(),
|
||||
caretStyle: CaretStyleSchema,
|
||||
paceCaretStyle: CaretStyleSchema,
|
||||
flipTestColors: z.boolean(),
|
||||
layout: LayoutSchema,
|
||||
alwaysShowWordsHistory: z.boolean(),
|
||||
singleListCommandLine: SingleListCommandLineSchema,
|
||||
minWpm: MinimumWordsPerMinuteSchema,
|
||||
minWpmCustomSpeed: MinWpmCustomSpeedSchema,
|
||||
minAcc: MinimumAccuracySchema,
|
||||
minAccCustom: MinimumAccuracyCustomSchema,
|
||||
minBurst: MinimumBurstSchema,
|
||||
minBurstCustomSpeed: MinimumBurstCustomSpeedSchema,
|
||||
britishEnglish: z.boolean(),
|
||||
funbox: FunboxSchema,
|
||||
customLayoutfluid: CustomLayoutFluidSchema,
|
||||
customPolyglot: CustomPolyglotSchema,
|
||||
|
||||
// input
|
||||
freedomMode: z.boolean(),
|
||||
strictSpace: z.boolean(),
|
||||
oppositeShiftMode: OppositeShiftModeSchema,
|
||||
stopOnError: StopOnErrorSchema,
|
||||
confidenceMode: ConfidenceModeSchema,
|
||||
quickEnd: z.boolean(),
|
||||
indicateTypos: IndicateTyposSchema,
|
||||
hideExtraLetters: z.boolean(),
|
||||
lazyMode: z.boolean(),
|
||||
layout: LayoutSchema,
|
||||
codeUnindentOnBackspace: z.boolean(),
|
||||
|
||||
// sound
|
||||
soundVolume: SoundVolumeSchema,
|
||||
playSoundOnClick: PlaySoundOnClickSchema,
|
||||
playSoundOnError: PlaySoundOnErrorSchema,
|
||||
|
||||
// caret
|
||||
smoothCaret: SmoothCaretSchema,
|
||||
caretStyle: CaretStyleSchema,
|
||||
paceCaret: PaceCaretSchema,
|
||||
paceCaretCustomSpeed: PaceCaretCustomSpeedSchema,
|
||||
paceCaretStyle: CaretStyleSchema,
|
||||
repeatedPace: z.boolean(),
|
||||
|
||||
// appearance
|
||||
timerStyle: TimerStyleSchema,
|
||||
liveSpeedStyle: LiveSpeedAccBurstStyleSchema,
|
||||
liveAccStyle: LiveSpeedAccBurstStyleSchema,
|
||||
liveBurstStyle: LiveSpeedAccBurstStyleSchema,
|
||||
colorfulMode: z.boolean(),
|
||||
randomTheme: RandomThemeSchema,
|
||||
timerColor: TimerColorSchema,
|
||||
timerOpacity: TimerOpacitySchema,
|
||||
stopOnError: StopOnErrorSchema,
|
||||
showAllLines: z.boolean(),
|
||||
keymapMode: KeymapModeSchema,
|
||||
keymapStyle: KeymapStyleSchema,
|
||||
keymapLegendStyle: KeymapLegendStyleSchema,
|
||||
keymapLayout: KeymapLayoutSchema,
|
||||
keymapShowTopRow: KeymapShowTopRowSchema,
|
||||
keymapSize: KeymapSizeSchema,
|
||||
fontFamily: FontFamilySchema,
|
||||
smoothLineScroll: z.boolean(),
|
||||
alwaysShowDecimalPlaces: z.boolean(),
|
||||
alwaysShowWordsHistory: z.boolean(),
|
||||
singleListCommandLine: SingleListCommandLineSchema,
|
||||
capsLockWarning: z.boolean(),
|
||||
playSoundOnError: PlaySoundOnErrorSchema,
|
||||
playSoundOnClick: PlaySoundOnClickSchema,
|
||||
soundVolume: SoundVolumeSchema,
|
||||
startGraphsAtZero: z.boolean(),
|
||||
showOutOfFocusWarning: z.boolean(),
|
||||
paceCaret: PaceCaretSchema,
|
||||
paceCaretCustomSpeed: PaceCaretCustomSpeedSchema,
|
||||
repeatedPace: z.boolean(),
|
||||
accountChart: AccountChartSchema,
|
||||
minWpm: MinimumWordsPerMinuteSchema,
|
||||
minWpmCustomSpeed: MinWpmCustomSpeedSchema,
|
||||
highlightMode: HighlightModeSchema,
|
||||
tapeMode: TapeModeSchema,
|
||||
tapeMargin: TapeMarginSchema,
|
||||
smoothLineScroll: z.boolean(),
|
||||
showAllLines: z.boolean(),
|
||||
alwaysShowDecimalPlaces: z.boolean(),
|
||||
typingSpeedUnit: TypingSpeedUnitSchema,
|
||||
ads: AdsSchema,
|
||||
hideExtraLetters: z.boolean(),
|
||||
strictSpace: z.boolean(),
|
||||
minAcc: MinimumAccuracySchema,
|
||||
minAccCustom: MinimumAccuracyCustomSchema,
|
||||
monkey: z.boolean(),
|
||||
repeatQuotes: RepeatQuotesSchema,
|
||||
oppositeShiftMode: OppositeShiftModeSchema,
|
||||
startGraphsAtZero: z.boolean(),
|
||||
maxLineWidth: MaxLineWidthSchema,
|
||||
fontSize: FontSizeSchema,
|
||||
fontFamily: FontFamilySchema,
|
||||
keymapMode: KeymapModeSchema,
|
||||
keymapLayout: KeymapLayoutSchema,
|
||||
keymapStyle: KeymapStyleSchema,
|
||||
keymapLegendStyle: KeymapLegendStyleSchema,
|
||||
keymapShowTopRow: KeymapShowTopRowSchema,
|
||||
keymapSize: KeymapSizeSchema,
|
||||
|
||||
// theme
|
||||
flipTestColors: z.boolean(),
|
||||
colorfulMode: z.boolean(),
|
||||
customBackground: CustomBackgroundSchema,
|
||||
customBackgroundSize: CustomBackgroundSizeSchema,
|
||||
customBackgroundFilter: CustomBackgroundFilterSchema,
|
||||
customLayoutfluid: CustomLayoutFluidSchema,
|
||||
monkeyPowerLevel: MonkeyPowerLevelSchema,
|
||||
minBurst: MinimumBurstSchema,
|
||||
minBurstCustomSpeed: MinimumBurstCustomSpeedSchema,
|
||||
burstHeatmap: z.boolean(),
|
||||
britishEnglish: z.boolean(),
|
||||
lazyMode: z.boolean(),
|
||||
autoSwitchTheme: z.boolean(),
|
||||
themeLight: ThemeNameSchema,
|
||||
themeDark: ThemeNameSchema,
|
||||
randomTheme: RandomThemeSchema,
|
||||
favThemes: FavThemesSchema,
|
||||
theme: ThemeNameSchema,
|
||||
customTheme: z.boolean(),
|
||||
customThemeColors: CustomThemeColorsSchema,
|
||||
|
||||
// hide elements
|
||||
showKeyTips: z.boolean(),
|
||||
showOutOfFocusWarning: z.boolean(),
|
||||
capsLockWarning: z.boolean(),
|
||||
showAverage: ShowAverageSchema,
|
||||
maxLineWidth: MaxLineWidthSchema,
|
||||
customPolyglot: CustomPolyglotSchema,
|
||||
|
||||
// other (hidden)
|
||||
accountChart: AccountChartSchema,
|
||||
monkey: z.boolean(),
|
||||
monkeyPowerLevel: MonkeyPowerLevelSchema,
|
||||
|
||||
// ads
|
||||
ads: AdsSchema,
|
||||
} satisfies Record<string, ZodSchema>)
|
||||
.strict();
|
||||
|
||||
|
|
@ -455,24 +484,14 @@ export const ConfigGroupNameSchema = z.enum([
|
|||
"appearance",
|
||||
"theme",
|
||||
"hideElements",
|
||||
"ads",
|
||||
"hidden",
|
||||
"ads",
|
||||
]);
|
||||
|
||||
export type ConfigGroupName = z.infer<typeof ConfigGroupNameSchema>;
|
||||
|
||||
export const ConfigGroupsLiteral = {
|
||||
theme: "theme",
|
||||
themeLight: "theme",
|
||||
themeDark: "theme",
|
||||
autoSwitchTheme: "theme",
|
||||
customTheme: "theme",
|
||||
customThemeColors: "theme",
|
||||
favThemes: "theme",
|
||||
showKeyTips: "hideElements",
|
||||
smoothCaret: "caret",
|
||||
codeUnindentOnBackspace: "input",
|
||||
quickRestart: "behavior",
|
||||
//test
|
||||
punctuation: "test",
|
||||
numbers: "test",
|
||||
words: "test",
|
||||
|
|
@ -480,76 +499,105 @@ export const ConfigGroupsLiteral = {
|
|||
mode: "test",
|
||||
quoteLength: "test",
|
||||
language: "test",
|
||||
fontSize: "appearance",
|
||||
freedomMode: "input",
|
||||
burstHeatmap: "test",
|
||||
|
||||
//behavior
|
||||
difficulty: "behavior",
|
||||
quickRestart: "behavior",
|
||||
repeatQuotes: "behavior",
|
||||
blindMode: "behavior",
|
||||
quickEnd: "input",
|
||||
caretStyle: "caret",
|
||||
paceCaretStyle: "caret",
|
||||
flipTestColors: "theme",
|
||||
layout: "input",
|
||||
funbox: "behavior",
|
||||
alwaysShowWordsHistory: "behavior",
|
||||
singleListCommandLine: "behavior",
|
||||
minWpm: "behavior",
|
||||
minWpmCustomSpeed: "behavior",
|
||||
minAcc: "behavior",
|
||||
minAccCustom: "behavior",
|
||||
minBurst: "behavior",
|
||||
minBurstCustomSpeed: "behavior",
|
||||
britishEnglish: "behavior",
|
||||
funbox: "behavior", //todo: maybe move to test?
|
||||
customLayoutfluid: "behavior",
|
||||
customPolyglot: "behavior",
|
||||
|
||||
//input
|
||||
freedomMode: "input",
|
||||
strictSpace: "input",
|
||||
oppositeShiftMode: "input",
|
||||
stopOnError: "input",
|
||||
confidenceMode: "input",
|
||||
quickEnd: "input",
|
||||
indicateTypos: "input",
|
||||
hideExtraLetters: "input",
|
||||
lazyMode: "input",
|
||||
layout: "input",
|
||||
codeUnindentOnBackspace: "input",
|
||||
|
||||
//sound
|
||||
soundVolume: "sound",
|
||||
playSoundOnClick: "sound",
|
||||
playSoundOnError: "sound",
|
||||
|
||||
//caret
|
||||
smoothCaret: "caret",
|
||||
caretStyle: "caret",
|
||||
paceCaret: "caret",
|
||||
paceCaretCustomSpeed: "caret",
|
||||
paceCaretStyle: "caret",
|
||||
repeatedPace: "caret",
|
||||
|
||||
//appearance
|
||||
timerStyle: "appearance",
|
||||
liveSpeedStyle: "appearance",
|
||||
liveAccStyle: "appearance",
|
||||
liveBurstStyle: "appearance",
|
||||
colorfulMode: "theme",
|
||||
randomTheme: "theme",
|
||||
timerColor: "appearance",
|
||||
timerOpacity: "appearance",
|
||||
stopOnError: "input",
|
||||
showAllLines: "appearance",
|
||||
keymapMode: "appearance",
|
||||
keymapStyle: "appearance",
|
||||
keymapLegendStyle: "appearance",
|
||||
keymapLayout: "appearance",
|
||||
keymapShowTopRow: "appearance",
|
||||
keymapSize: "appearance",
|
||||
fontFamily: "appearance",
|
||||
smoothLineScroll: "appearance",
|
||||
alwaysShowDecimalPlaces: "appearance",
|
||||
alwaysShowWordsHistory: "behavior",
|
||||
singleListCommandLine: "behavior",
|
||||
capsLockWarning: "hideElements",
|
||||
playSoundOnError: "sound",
|
||||
playSoundOnClick: "sound",
|
||||
soundVolume: "sound",
|
||||
startGraphsAtZero: "appearance",
|
||||
showOutOfFocusWarning: "hideElements",
|
||||
paceCaret: "caret",
|
||||
paceCaretCustomSpeed: "caret",
|
||||
repeatedPace: "caret",
|
||||
accountChart: "hidden",
|
||||
minWpm: "behavior",
|
||||
minWpmCustomSpeed: "behavior",
|
||||
highlightMode: "appearance",
|
||||
tapeMode: "appearance",
|
||||
tapeMargin: "appearance",
|
||||
smoothLineScroll: "appearance",
|
||||
showAllLines: "appearance",
|
||||
alwaysShowDecimalPlaces: "appearance",
|
||||
typingSpeedUnit: "appearance",
|
||||
ads: "ads",
|
||||
hideExtraLetters: "input",
|
||||
strictSpace: "input",
|
||||
minAcc: "behavior",
|
||||
minAccCustom: "behavior",
|
||||
monkey: "hidden",
|
||||
repeatQuotes: "behavior",
|
||||
oppositeShiftMode: "input",
|
||||
startGraphsAtZero: "appearance",
|
||||
maxLineWidth: "appearance",
|
||||
fontSize: "appearance",
|
||||
fontFamily: "appearance",
|
||||
keymapMode: "appearance",
|
||||
keymapLayout: "appearance",
|
||||
keymapStyle: "appearance",
|
||||
keymapLegendStyle: "appearance",
|
||||
keymapShowTopRow: "appearance",
|
||||
keymapSize: "appearance",
|
||||
|
||||
//theme
|
||||
flipTestColors: "theme",
|
||||
colorfulMode: "theme",
|
||||
customBackground: "theme",
|
||||
customBackgroundSize: "theme",
|
||||
customBackgroundFilter: "theme",
|
||||
customLayoutfluid: "behavior",
|
||||
monkeyPowerLevel: "hidden",
|
||||
minBurst: "behavior",
|
||||
minBurstCustomSpeed: "behavior",
|
||||
burstHeatmap: "test",
|
||||
britishEnglish: "behavior",
|
||||
lazyMode: "input",
|
||||
autoSwitchTheme: "theme",
|
||||
themeLight: "theme",
|
||||
themeDark: "theme",
|
||||
randomTheme: "theme",
|
||||
favThemes: "theme",
|
||||
theme: "theme",
|
||||
customTheme: "theme",
|
||||
customThemeColors: "theme",
|
||||
|
||||
//hide elements
|
||||
showKeyTips: "hideElements",
|
||||
showOutOfFocusWarning: "hideElements",
|
||||
capsLockWarning: "hideElements",
|
||||
showAverage: "hideElements",
|
||||
maxLineWidth: "appearance",
|
||||
customPolyglot: "behavior",
|
||||
|
||||
//other
|
||||
accountChart: "hidden",
|
||||
monkey: "hidden",
|
||||
monkeyPowerLevel: "hidden",
|
||||
|
||||
//ads
|
||||
ads: "ads",
|
||||
} as const satisfies Record<ConfigKey, ConfigGroupName>;
|
||||
|
||||
export type ConfigGroups = typeof ConfigGroupsLiteral;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue