import { describe, it, expect, beforeEach, afterAll, vi } from "vitest"; import * as Config from "../../src/ts/config/setters"; import * as Lifecycle from "../../src/ts/config/lifecycle"; import * as ConfigUtils from "../../src/ts/config/utils"; import { __testing } from "../../src/ts/config/testing"; import * as Misc from "../../src/ts/utils/misc"; import * as Env from "../../src/ts/utils/env"; import { ConfigKey, Config as ConfigType, CaretStyleSchema, } from "@monkeytype/schemas/configs"; import * as FunboxValidation from "../../src/ts/config/funbox-validation"; import * as ConfigValidation from "../../src/ts/config/validation"; import { configEvent } from "../../src/ts/events/config"; import * as ApeConfig from "../../src/ts/ape/config"; import * as Notifications from "../../src/ts/states/notifications"; const { replaceConfig, getConfig } = __testing; describe("Config", () => { const isDevEnvironmentMock = vi.spyOn(Env, "isDevEnvironment"); beforeEach(() => { isDevEnvironmentMock.mockClear(); replaceConfig({}); }); 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(ApeConfig, "saveConfig"); const notificationAddMock = vi.spyOn( Notifications, "showNoticeNotification", ); const miscReloadAfterMock = vi.spyOn(Misc, "reloadAfter"); const miscTriggerResizeMock = vi.spyOn(Misc, "triggerResize"); const mocks = [ canSetConfigWithCurrentFunboxesMock, isConfigValueValidMock, dispatchConfigEventMock, dbSaveConfigMock, notificationAddMock, miscReloadAfterMock, miscTriggerResizeMock, ]; beforeEach(async () => { vi.useFakeTimers(); mocks.forEach((it) => it.mockClear()); vi.mock("../../src/ts/test/test-state", () => ({ isActive: true, })); isConfigValueValidMock.mockReturnValue(true); canSetConfigWithCurrentFunboxesMock.mockReturnValue(true); dbSaveConfigMock.mockResolvedValue(); replaceConfig({}); }); afterAll(() => { mocks.forEach((it) => it.mockRestore()); vi.useRealTimers(); }); beforeEach(() => isDevEnvironmentMock.mockClear()); it("should throw if config key in not found in metadata", () => { expect(() => { Config.setConfig("nonExistentKey" as ConfigKey, true); }).toThrow(`Config metadata for key "nonExistentKey" is not defined.`); }); it("fails if test is active and funbox no_quit", () => { //GIVEN replaceConfig({ funbox: ["no_quit"], numbers: false }); //WHEN expect( Config.setConfig("numbers", true, { nosave: true, }), ).toBe(false); //THEN expect(notificationAddMock).toHaveBeenCalledWith( "No quit funbox is active. Please finish the test.", { important: true, }, ); }); //TODO isBlocked it("should fail if config is blocked", () => { //GIVEN replaceConfig({ tapeMode: "letter" }); //WHEN / THEN expect(Config.setConfig("showAllLines", true)).toBe(false); }); it("disables live text stats when enabling monkey", () => { //GIVEN replaceConfig({ liveSpeedStyle: "text", liveAccStyle: "text", monkey: false, }); //WHEN / THEN expect(Config.setConfig("monkey", true)).toBe(true); expect(getConfig()).toMatchObject({ monkey: true, liveSpeedStyle: "mini", liveAccStyle: "mini", }); expect(notificationAddMock).not.toHaveBeenCalled(); }); it("disables monkey when enabling live speed text", () => { //GIVEN replaceConfig({ monkey: true, liveSpeedStyle: "off" }); //WHEN / THEN expect(Config.setConfig("liveSpeedStyle", "text")).toBe(true); expect(getConfig()).toMatchObject({ monkey: false, liveSpeedStyle: "text", }); expect(notificationAddMock).not.toHaveBeenCalled(); }); it("disables monkey when enabling live accuracy text", () => { //GIVEN replaceConfig({ monkey: true, liveAccStyle: "off" }); //WHEN / THEN expect(Config.setConfig("liveAccStyle", "text")).toBe(true); expect(getConfig()).toMatchObject({ monkey: false, liveAccStyle: "text", }); expect(notificationAddMock).not.toHaveBeenCalled(); }); it("should use overrideValue", () => { //WHEN Config.setConfig("customLayoutfluid", ["3l", "ABNT2", "3l"]); //THEN expect(getConfig().customLayoutfluid).toEqual(["3l", "ABNT2"]); }); it("fails if config is invalid", () => { //GIVEN isConfigValueValidMock.mockReturnValue(false); //WHEN / THEN expect(Config.setConfig("caretStyle", "banana" as any)).toBe(false); expect(isConfigValueValidMock).toHaveBeenCalledWith( "caret style", "banana", CaretStyleSchema, ); }); it("cannot set if funbox disallows", () => { //GIVEN canSetConfigWithCurrentFunboxesMock.mockReturnValue(false); //WHEN / THEN expect(Config.setConfig("numbers", true)).toBe(false); }); it("sets overrideConfigs", () => { //GIVEN replaceConfig({ confidenceMode: "off", freedomMode: false, //already set correctly stopOnError: "letter", //should get updated }); //WHEN Config.setConfig("confidenceMode", "max"); //THEN expect(dispatchConfigEventMock).not.toHaveBeenCalledWith({ key: "freedomMode", newValue: false, nosave: true, previousValue: true, }); expect(dispatchConfigEventMock).toHaveBeenCalledWith({ key: "stopOnError", newValue: "off", nosave: false, previousValue: "letter", }); expect(dispatchConfigEventMock).toHaveBeenCalledWith({ key: "confidenceMode", newValue: "max", nosave: false, previousValue: "off", }); }); it("saves to localstorage if nosave=false", async () => { //GIVEN replaceConfig({ numbers: false }); //WHEN Config.setConfig("numbers", true); //THEN //wait for debounce await vi.advanceTimersByTimeAsync(2500); //save expect(dbSaveConfigMock).toHaveBeenCalledWith({ numbers: true }); }); it("saves configOverride values to localstorage if nosave=false", async () => { //GIVEN replaceConfig({}); //WHEN Config.setConfig("minWpmCustomSpeed", 120); //THEN //wait for debounce await vi.advanceTimersByTimeAsync(2500); //save expect(dbSaveConfigMock).toHaveBeenCalledWith({ minWpmCustomSpeed: 120, minWpm: "custom", }); }); it("does not save to localstorage if nosave=true", async () => { //GIVEN replaceConfig({ numbers: false }); //WHEN Config.setConfig("numbers", true, { nosave: true, }); //THEN //wait for debounce await vi.advanceTimersByTimeAsync(2500); expect(dbSaveConfigMock).not.toHaveBeenCalled(); }); it("dispatches event on set", () => { //GIVEN replaceConfig({ numbers: false }); //WHEN Config.setConfig("numbers", true, { nosave: true, }); //THEN expect(dispatchConfigEventMock).toHaveBeenCalledWith({ key: "numbers", newValue: true, nosave: true, previousValue: false, }); }); it("triggers resize if property is set", () => { ///WHEN Config.setConfig("maxLineWidth", 50); expect(miscTriggerResizeMock).toHaveBeenCalled(); }); it("does not triggers resize if property is not set", () => { ///WHEN Config.setConfig("startGraphsAtZero", true); expect(miscTriggerResizeMock).not.toHaveBeenCalled(); }); it("does not triggers resize if property on nosave", () => { ///WHEN Config.setConfig("maxLineWidth", 50, { nosave: true }); expect(miscTriggerResizeMock).not.toHaveBeenCalled(); }); it("calls afterSet", () => { //GIVEN isDevEnvironmentMock.mockReturnValue(false); replaceConfig({ ads: "off" }); //WHEN Config.setConfig("ads", "sellout"); //THEN expect(notificationAddMock).toHaveBeenCalledWith( "Ad settings changed. Refreshing...", ); expect(miscReloadAfterMock).toHaveBeenCalledWith(3); }); }); describe("apply", () => { it("should fill missing values with defaults", async () => { //GIVEN replaceConfig({ mode: "words", }); await Lifecycle.applyConfig({ numbers: true, punctuation: true, }); const config = getConfig(); expect(config.mode).toBe("time"); expect(config.numbers).toBe(true); expect(config.punctuation).toBe(true); }); describe("should reset to default if setting failed", () => { const testCases: { display: string; value: Partial; expected: Partial; }[] = [ { // invalid funbox display: "invalid funbox", value: { funbox: ["invalid_funbox"] as any }, expected: { funbox: [] }, }, { display: "mode incompatible with funbox", value: { mode: "quote", funbox: ["58008"] }, expected: { funbox: [] }, }, { display: "invalid combination of funboxes", value: { funbox: ["58008", "gibberish"] }, expected: { funbox: [] }, }, { display: "sanitizes config, remove extra keys", value: { mode: "zen", unknownKey: true, unknownArray: [1, 2] } as any, expected: { mode: "zen" }, }, { display: "applies config migration", value: { mode: "zen", swapEscAndTab: true } as any, expected: { mode: "zen", quickRestart: "esc" }, }, ]; it.each(testCases)("$display", async ({ value, expected }) => { await Lifecycle.applyConfig(value); const config = getConfig(); const applied = Object.fromEntries( Object.entries(config).filter(([key]) => Object.keys(expected).includes(key), ), ); expect(applied).toEqual(expected); }); }); describe("should apply keys in an order to avoid overrides", () => { const testCases: { display: string; value: Partial; expected: Partial; }[] = [ { display: "quote length shouldnt override mode, punctuation and numbers", value: { punctuation: true, numbers: true, quoteLength: [0], mode: "time", }, expected: { punctuation: true, numbers: true, quoteLength: [0], mode: "time", }, }, ]; it.each(testCases)("$display", async ({ value, expected }) => { await Lifecycle.applyConfig(value); const config = getConfig(); const applied = Object.fromEntries( Object.entries(config).filter(([key]) => Object.keys(expected).includes(key), ), ); expect(applied).toEqual(expected); }); }); it("should apply a partial config but keep the rest unchanged", async () => { replaceConfig({ numbers: true, }); await Lifecycle.applyConfig({ ...ConfigUtils.getConfigChanges(), punctuation: true, }); const config = getConfig(); expect(config.numbers).toBe(true); }); it("should not enable minWpm if not provided", async () => { replaceConfig({ minWpm: "off", }); await Lifecycle.applyConfig({ minWpmCustomSpeed: 100, }); const config = getConfig(); expect(config.minWpm).toBe("off"); expect(config.minWpmCustomSpeed).toEqual(100); }); it("should apply minWpm if part of the full config", async () => { replaceConfig({ minWpm: "off", }); await Lifecycle.applyConfig({ minWpm: "custom", minWpmCustomSpeed: 100, }); const config = getConfig(); expect(config.minWpm).toBe("custom"); expect(config.minWpmCustomSpeed).toEqual(100); }); it("should keep the keymap off when applying keymapLayout", async () => { replaceConfig({}); await Lifecycle.applyConfig({ keymapLayout: "qwerty", }); const config = getConfig(); expect(config.keymapLayout).toEqual("qwerty"); expect(config.keymapMode).toEqual("off"); }); }); });