diff --git a/backend/__tests__/__integration__/dal/result.spec.ts b/backend/__tests__/__integration__/dal/result.spec.ts index f466cf528eb9..15581dd3f160 100644 --- a/backend/__tests__/__integration__/dal/result.spec.ts +++ b/backend/__tests__/__integration__/dal/result.spec.ts @@ -39,7 +39,7 @@ async function createDummyData( charStats: [0, 0, 0, 0], acc: 0, mode: "time", - mode2: "10" as never, + mode2: "10", quoteLength: 1, timestamp, restartCount: 0, diff --git a/backend/__tests__/__integration__/dal/user.spec.ts b/backend/__tests__/__integration__/dal/user.spec.ts index 5f44cffe1d41..5b3200f1add5 100644 --- a/backend/__tests__/__integration__/dal/user.spec.ts +++ b/backend/__tests__/__integration__/dal/user.spec.ts @@ -2137,7 +2137,7 @@ describe("UserDal", () => { maxLength: 50, lastResultTimestamp: 0, hourOffset: -1, - } as any, + }, xp: 42, inventory: { badges: [{ id: 23 }, { id: 5 }], diff --git a/backend/__tests__/api/controllers/quotes.spec.ts b/backend/__tests__/api/controllers/quotes.spec.ts index d183e6f06672..96e4098dccc0 100644 --- a/backend/__tests__/api/controllers/quotes.spec.ts +++ b/backend/__tests__/api/controllers/quotes.spec.ts @@ -160,7 +160,7 @@ describe("QuotesController", () => { beforeEach(() => { addQuoteMock.mockClear(); - addQuoteMock.mockResolvedValue({} as any); + addQuoteMock.mockResolvedValue({}); verifyCaptchaMock.mockClear(); verifyCaptchaMock.mockResolvedValue(true); diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index 25a036a47fbf..17e9ac1f64bb 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -24,7 +24,6 @@ import GeorgeQueue from "../../../src/queues/george-queue"; import * as DiscordUtils from "../../../src/utils/discord"; import * as Captcha from "../../../src/utils/captcha"; import * as FirebaseAdmin from "../../../src/init/firebase-admin"; -import { FirebaseError } from "firebase-admin"; import * as ApeKeysDal from "../../../src/dal/ape-keys"; import * as LogDal from "../../../src/dal/logs"; import { ObjectId } from "mongodb"; @@ -1439,7 +1438,7 @@ describe("user controller test", () => { }); it("should fail for unknown error", async () => { //GIVEN - authUpdateEmailMock.mockRejectedValue({} as FirebaseError); + authUpdateEmailMock.mockRejectedValue({}); //WHEN await mockApp diff --git a/backend/__tests__/middlewares/auth.spec.ts b/backend/__tests__/middlewares/auth.spec.ts index 902e3196691c..a49ac1ce8e40 100644 --- a/backend/__tests__/middlewares/auth.spec.ts +++ b/backend/__tests__/middlewares/auth.spec.ts @@ -77,7 +77,7 @@ describe("middlewares/auth", () => { throw error; } return "Next function called"; - }) as unknown as NextFunction; + }); }); afterEach(() => { diff --git a/backend/package.json b/backend/package.json index bc0e77f0f078..a47cf2102b57 100644 --- a/backend/package.json +++ b/backend/package.json @@ -81,8 +81,8 @@ "@vitest/coverage-v8": "4.1.5", "concurrently": "8.2.2", "openapi3-ts": "2.0.2", - "oxlint": "1.68.0", - "oxlint-tsgolint": "0.23.0", + "oxlint": "1.73.0", + "oxlint-tsgolint": "0.24.0", "readline-sync": "1.4.10", "supertest": "7.1.4", "testcontainers": "11.11.0", diff --git a/backend/src/api/controllers/dev.ts b/backend/src/api/controllers/dev.ts index e16572f441ce..134a8f50e6e3 100644 --- a/backend/src/api/controllers/dev.ts +++ b/backend/src/api/controllers/dev.ts @@ -163,7 +163,7 @@ function createResult( charStats: [131, 0, 0, 0], acc: random(80, 100), language: "english", - mode: mode as Mode, + mode: mode, mode2: mode2 as unknown as never, timestamp: timestamp.valueOf(), testDuration: testDuration, diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index c9eb977bbad4..80baab7dfb71 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -536,7 +536,7 @@ function getRelevantUserInfo(user: UserDAL.DBUser): RelevantUserInfo { "ips", "testActivity", "suspicious", - ]) as RelevantUserInfo; + ]); } export async function getUser(req: MonkeyRequest): Promise { diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index d8f9e69262fa..911524010796 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -30,7 +30,6 @@ import { getLiveConfiguration } from "../../init/configuration"; import Logger from "../../utils/logger"; import { createExpressEndpoints, initServer } from "@ts-rest/express"; import { ZodIssue } from "zod"; -import { MonkeyValidationError } from "@monkeytype/contracts/util/api"; import { authenticateTsRestRequest } from "../../middlewares/auth"; import { rateLimitRequest } from "../../middlewares/rate-limit"; import { verifyPermissions } from "../../middlewares/permission"; @@ -111,9 +110,7 @@ function applyTsRestApiRoutes(app: IRouter): void { return; } - res - .status(422) - .json({ message, validationErrors } as MonkeyValidationError); + res.status(422).json({ message, validationErrors }); }, globalMiddleware: [ authenticateTsRestRequest(), diff --git a/docs/LANGUAGES.md b/docs/LANGUAGES.md index 6f380d669371..2cd639af9734 100644 --- a/docs/LANGUAGES.md +++ b/docs/LANGUAGES.md @@ -24,6 +24,7 @@ The contents of the file should be as follows: "joiningScript": boolean, "orderedByFrequency": boolean, "bcp47": string, + "preferredFont":string, "words": string[] } ``` @@ -34,6 +35,7 @@ It is recommended that you familiarize yourselves with JSON before adding a lang For `bcp47` put your languages [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag). If the words you're adding are ordered by frequency (most common words at the top, least at the bottom) set the value of `orderedByFrequency` to `true`, otherwise `false`. Finally, add your list of words to the `words` field. +If the language is not rendered correctly, you can provide a `preferredFont`. The font needs to be one of monkeytypes fonts. Check the [fonts documentation](./FONTS.md). Then, go to `packages/schemas/src/languages.ts` and add your new language name at the _end_ of the `LanguageSchema` enum. Make sure to end the line with a comma. Make sure to add all your language names if you have created multiple word lists of differing lengths in the same language. diff --git a/frontend/__tests__/components/common/AnimatedModal.spec.tsx b/frontend/__tests__/components/common/AnimatedModal.spec.tsx index 3eb79f9a145e..61e5a4a5e178 100644 --- a/frontend/__tests__/components/common/AnimatedModal.spec.tsx +++ b/frontend/__tests__/components/common/AnimatedModal.spec.tsx @@ -2,11 +2,11 @@ import { render } from "@solidjs/testing-library"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { AnimatedModal } from "../../../src/ts/components/common/AnimatedModal"; -import { hideModal, ModalId, showModal } from "../../../src/ts/states/modals"; +import { hideModal, showModal } from "../../../src/ts/states/modals"; describe("AnimatedModal", () => { beforeEach(() => { - showModal("Support" as ModalId); + showModal("Support"); vi.clearAllMocks(); // Mock dialog methods that don't exist in jsdom @@ -60,7 +60,7 @@ describe("AnimatedModal", () => { }); it("doesnt render children if not open", () => { - hideModal("Support" as ModalId); + hideModal("Support"); const { modalDiv } = renderModal({}); expect(modalDiv).not.toBeInTheDocument(); diff --git a/frontend/__tests__/components/ui/form/InputField.spec.tsx b/frontend/__tests__/components/ui/form/InputField.spec.tsx index 8b079abdcf3e..aaa221edf9d1 100644 --- a/frontend/__tests__/components/ui/form/InputField.spec.tsx +++ b/frontend/__tests__/components/ui/form/InputField.spec.tsx @@ -4,22 +4,32 @@ import { describe, it, expect, vi } from "vitest"; import { InputField } from "../../../../src/ts/components/ui/form/InputField"; -function makeField(name: string, value = ""): AnyFieldApi { +function makeField( + name: string, + value?: string | number | boolean, +): AnyFieldApi { + let current = value; + const meta = { + isValidating: false, + isTouched: false, + isValid: true, + isDefaultValue: true, + errors: [] as string[], + }; return { name, - state: { - value, - meta: { - isValidating: false, - isTouched: false, - isValid: true, - isDefaultValue: true, - errors: [], - }, + get state() { + return { + value: current, + meta, + }; }, - options: {}, + options: { default: value }, handleBlur: vi.fn(), - handleChange: vi.fn(), + handleChange: vi.fn((v: unknown) => { + current = v as typeof value; + }), + setValue: vi.fn(), getMeta: () => ({ hasWarning: false, warnings: [] }), } as unknown as AnyFieldApi; } @@ -61,6 +71,16 @@ describe("InputField", () => { expect(field.handleChange).toHaveBeenCalledWith("test"); }); + it("calls handleChange on input for number", async () => { + const field = makeField("name", 2.5); + render(() => field} type="number" />); + + fireEvent.input(screen.getByRole("spinbutton"), { + target: { value: "1.25" }, + }); + expect(field.handleChange).toHaveBeenCalledWith(1.25); + }); + it("calls handleBlur on blur", async () => { const field = makeField("name"); render(() => field} />); @@ -100,4 +120,43 @@ describe("InputField", () => { expect(container.querySelector(".fa-circle-notch")).not.toBeInTheDocument(); }); + + it("resets to default value on blur when empty for type number", async () => { + const field = makeField("age", 25); + field.form = { options: { defaultValues: { age: 25 } } } as any; + render(() => ( + field} + type="number" + resetToDefaultIfEmptyOnBlur + /> + )); + + fireEvent.input(screen.getByRole("spinbutton"), { + target: { value: "" }, + }); + fireEvent.blur(screen.getByRole("spinbutton")); + expect(field.setValue).toHaveBeenCalledWith(25); + }); + + it("resets to default value on blur when empty for type string", async () => { + const field = makeField("name", "Alice"); + field.form = { options: { defaultValues: { name: "Alice" } } } as any; + render(() => ( + field} resetToDefaultIfEmptyOnBlur /> + )); + + fireEvent.input(screen.getByRole("textbox"), { + target: { value: "" }, + }); + fireEvent.blur(screen.getByRole("textbox")); + expect(field.setValue).toHaveBeenCalledWith("Alice"); + }); + + it("renders NaN as empty string for type number", async () => { + const field = makeField("value", NaN); + render(() => field} type="number" />); + + expect(screen.getByRole("spinbutton").getAttribute("value")).toBeNull(); + }); }); diff --git a/frontend/__tests__/controllers/preset-controller.spec.ts b/frontend/__tests__/controllers/preset-controller.spec.ts index 4b23d4cb3b4f..3d412e47e021 100644 --- a/frontend/__tests__/controllers/preset-controller.spec.ts +++ b/frontend/__tests__/controllers/preset-controller.spec.ts @@ -164,7 +164,7 @@ describe("PresetController", () => { ...partialPreset, } as any; dbGetSnapshotMock.mockReturnValue({} as any); - getPresetMock.mockReturnValue(preset as any); + getPresetMock.mockReturnValue(preset); return preset; }; }); diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts index 40e3ced07ee2..6a95f6a54887 100644 --- a/frontend/__tests__/root/config.spec.ts +++ b/frontend/__tests__/root/config.spec.ts @@ -15,6 +15,8 @@ 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"; +import * as TestState from "../../src/ts/states/test"; + const { replaceConfig, getConfig } = __testing; describe("Config", () => { @@ -41,6 +43,7 @@ describe("Config", () => { ); const miscReloadAfterMock = vi.spyOn(Misc, "reloadAfter"); const miscTriggerResizeMock = vi.spyOn(Misc, "triggerResize"); + const stateIsTestActiveMock = vi.spyOn(TestState, "isTestActive"); const mocks = [ canSetConfigWithCurrentFunboxesMock, @@ -50,19 +53,17 @@ describe("Config", () => { notificationAddMock, miscReloadAfterMock, miscTriggerResizeMock, + stateIsTestActiveMock, ]; 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(); + stateIsTestActiveMock.mockReturnValue(true); replaceConfig({}); }); diff --git a/frontend/__tests__/test/events/data.spec.ts b/frontend/__tests__/test/events/data.spec.ts index 3cd8f6625bb5..d093fae25e7f 100644 --- a/frontend/__tests__/test/events/data.spec.ts +++ b/frontend/__tests__/test/events/data.spec.ts @@ -223,10 +223,10 @@ describe("data.ts", () => { // simulate forceReleaseAllKeys passing indexed codes directly logTestEvent("keyup", 1030, { code: "NoCode0", - } as KeyupEventData); + }); logTestEvent("keyup", 1040, { code: "NoCode1", - } as KeyupEventData); + }); const events = getAllTestEvents(); expect(events).toHaveLength(4); @@ -239,7 +239,7 @@ describe("data.ts", () => { it("rejects indexed NoCode keyup with no matching keydown", () => { logTestEvent("keyup", 1010, { code: "NoCode0", - } as KeyupEventData); + }); expect(getAllTestEvents()).toHaveLength(0); }); diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts index 4682015fff07..862fd8a8db06 100644 --- a/frontend/__tests__/test/events/stats.spec.ts +++ b/frontend/__tests__/test/events/stats.spec.ts @@ -844,7 +844,7 @@ describe("stats.ts", () => { charIndex: 3, inputType: "deleteWordBackward", inputValue: "=", - } as InputEventData); + }); // second Ctrl+Backspace: Firefox ate sentinel + "=" → goToPreviousWord; // clearedNextWord marks word 1 (= wordIndex + 1) as abandoned @@ -854,7 +854,7 @@ describe("stats.ts", () => { inputType: "deleteWordBackward", inputValue: "", clearedNextWord: true, - } as InputEventData); + }); logTestEvent("timer", 5000, timer("end", 4)); diff --git a/frontend/package.json b/frontend/package.json index 28577153aaac..bd52eaa7aa61 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -110,8 +110,8 @@ "madge": "8.0.0", "magic-string": "0.30.17", "normalize.css": "8.0.1", - "oxlint": "1.68.0", - "oxlint-tsgolint": "0.23.0", + "oxlint": "1.73.0", + "oxlint-tsgolint": "0.24.0", "postcss": "8.5.15", "sass": "1.70.0", "solid-devtools": "0.34.5", diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html index 55ea01f92bc1..a4e0c0482850 100644 --- a/frontend/src/html/pages/test.html +++ b/frontend/src/html/pages/test.html @@ -65,6 +65,7 @@
+