diff --git a/package.json b/package.json index 6b28eaa2d..1ecab8cb9 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "test:core": "bun packages/scripts/src/testing/run.ts core", "test:web": "bun packages/scripts/src/testing/run.ts web", "test:scripts": "bun packages/scripts/src/testing/run.ts scripts", - "type-check": "bunx typescript@6.0.3 --noEmit && bunx typescript@6.0.3 -p packages/web/tsconfig.app.json --noEmit", + "type-check": "bunx typescript@6.0.3 --noEmit && bunx typescript@6.0.3 -p packages/web/tsconfig.app.json --noEmit && bun run type-check:web-tests", + "type-check:web-tests": "bunx typescript@6.0.3 -p packages/web/tsconfig.test.json --noEmit", "verify": "bun packages/scripts/src/testing/verify.ts", "build:backend": "bun packages/scripts/src/commands/build.backend.ts", "build:web": "cd packages/web && bun --env-file=../backend/.env.local run build.ts" diff --git a/packages/web/src/__tests__/patched-jest.d.ts b/packages/web/src/__tests__/patched-jest.d.ts index 6ba2a6a7c..f2839a9be 100644 --- a/packages/web/src/__tests__/patched-jest.d.ts +++ b/packages/web/src/__tests__/patched-jest.d.ts @@ -1,3 +1,9 @@ import type { jest as JestNamespace } from "bun:test"; +import type { TestingLibraryMatchers } from "@types/testing-library__jest-dom/matchers"; export declare const jest: typeof JestNamespace; + +declare module "bun:test" { + interface Matchers + extends TestingLibraryMatchers {} +} diff --git a/packages/web/src/common/apis/base/base.api.test.ts b/packages/web/src/common/apis/base/base.api.test.ts index cbee1585c..ae982afd9 100644 --- a/packages/web/src/common/apis/base/base.api.test.ts +++ b/packages/web/src/common/apis/base/base.api.test.ts @@ -23,7 +23,7 @@ describe("BaseApi backend availability", () => { it("marks the backend unavailable when fetch cannot reach it", async () => { globalThis.fetch = mock(() => Promise.reject(new TypeError("Failed to fetch")), - ) as typeof fetch; + ) as unknown as typeof fetch; await expect(BaseApi.get("/event")).rejects.toMatchObject({ name: "ApiError", @@ -36,7 +36,7 @@ describe("BaseApi backend availability", () => { markBackendUnavailable(); globalThis.fetch = mock(() => Promise.resolve(new Response("{}", { status: 200 })), - ) as typeof fetch; + ) as unknown as typeof fetch; await BaseApi.get("/config"); diff --git a/packages/web/src/common/apis/util/api.util.test.ts b/packages/web/src/common/apis/util/api.util.test.ts index 159984522..52aedc5ba 100644 --- a/packages/web/src/common/apis/util/api.util.test.ts +++ b/packages/web/src/common/apis/util/api.util.test.ts @@ -2,7 +2,7 @@ import { ApiErrorResponseSchema, GoogleConnectErrorResponseSchema, } from "@core/types/auth.types"; -import { type ApiError } from "../api.types"; +import { type ApiError, type ApiResponse } from "../api.types"; import { getApiErrorCode, parseApiError, diff --git a/packages/web/src/common/hooks/useBufferedVisibility.test.ts b/packages/web/src/common/hooks/useBufferedVisibility.test.ts index 961b2adb2..42206c7b4 100644 --- a/packages/web/src/common/hooks/useBufferedVisibility.test.ts +++ b/packages/web/src/common/hooks/useBufferedVisibility.test.ts @@ -32,10 +32,13 @@ describe("useBufferedVisibility", () => { ) => { const id = ++currentTimeoutId; if (typeof callback === "function") { - activeTimeouts.set(id, { callback, delay: delay ?? 0 }); + activeTimeouts.set(id, { + callback: () => callback(), + delay: delay ?? 0, + }); } return id; - }) as typeof setTimeout); + }) as unknown as typeof setTimeout); clearTimeoutSpy = spyOn(globalThis, "clearTimeout").mockImplementation((( id?: number, @@ -43,7 +46,7 @@ describe("useBufferedVisibility", () => { if (id !== undefined) { activeTimeouts.delete(id); } - }) as typeof clearTimeout); + }) as unknown as typeof clearTimeout); }); afterEach(() => { diff --git a/packages/web/src/common/hooks/useEventDNDActions.test.ts b/packages/web/src/common/hooks/useEventDNDActions.test.ts index ca502df18..4b9d02fb3 100644 --- a/packages/web/src/common/hooks/useEventDNDActions.test.ts +++ b/packages/web/src/common/hooks/useEventDNDActions.test.ts @@ -1,8 +1,8 @@ import { type DragEndEvent } from "@dnd-kit/core"; import { renderHook } from "@testing-library/react"; import { BehaviorSubject } from "rxjs"; -import { Categories_Event } from "@core/types/event.types"; -import dayjs from "@core/util/date/dayjs"; +import { Categories_Event, type Schema_Event } from "@core/types/event.types"; +import dayjs, { type Dayjs } from "@core/util/date/dayjs"; import { ID_GRID_ALLDAY_ROW, ID_GRID_MAIN, @@ -41,16 +41,18 @@ mock.module("@web/views/Day/util/agenda/agenda.util", () => ({ // Provide a focused mock that includes the named export consumers expect. // Keep a safe default for other exports so concurrent tests won't break imports. getSnappedMinutes: mockGetSnappedMinutes, - getAgendaEventTitle: (event) => `${event?.title || ""} -`, - getAgendaEventTime: (d) => (d ? new Date(d).toISOString() : ""), - getAgendaEventPosition: (_date) => 0, - getNowLinePosition: (_date) => 0, - getEventTimeFromPosition: (_y, dateInView) => - dateInView.startOf ? dateInView.startOf("day") : new Date(), - roundMinutesToNearestFifteen: (minutes) => Math.round(minutes / 15) * 15, - roundToNearestFifteenWithinHour: (minutes) => + getAgendaEventTitle: (event: Schema_Event) => `${event.title ?? ""} -`, + getAgendaEventTime: (date: Date | string) => + date ? new Date(date).toISOString() : "", + getAgendaEventPosition: (_date: Date) => 0, + getNowLinePosition: (_date: Date) => 0, + getEventTimeFromPosition: (_y: number, dateInView: Dayjs) => + dateInView.startOf("day"), + roundMinutesToNearestFifteen: (minutes: number) => + Math.round(minutes / 15) * 15, + roundToNearestFifteenWithinHour: (minutes: number) => Math.min(45, Math.round(minutes / 15) * 15), - getEventHeight: (_event) => 4, + getEventHeight: (_event: Pick) => 4, })); mock.module("@web/ducks/events/selectors/event.selectors", () => ({ diff --git a/packages/web/src/common/hooks/useMainGridSelectionState.test.ts b/packages/web/src/common/hooks/useMainGridSelectionState.test.ts index b19bf0f9e..82dd63074 100644 --- a/packages/web/src/common/hooks/useMainGridSelectionState.test.ts +++ b/packages/web/src/common/hooks/useMainGridSelectionState.test.ts @@ -16,10 +16,10 @@ describe("useMainGridSelectionState", () => { callback: TimerHandler, ) => { if (typeof callback === "function") { - timeoutCallback = callback; + timeoutCallback = () => callback(); } return 1; - }) as typeof setTimeout); + }) as unknown as typeof setTimeout); act(() => { selecting$.next(false); diff --git a/packages/web/src/common/hooks/useOpenAtCursor.test.ts b/packages/web/src/common/hooks/useOpenAtCursor.test.ts index a60a067cd..0ce037bf2 100644 --- a/packages/web/src/common/hooks/useOpenAtCursor.test.ts +++ b/packages/web/src/common/hooks/useOpenAtCursor.test.ts @@ -34,10 +34,10 @@ describe("useOpenAtCursor", () => { callback: TimerHandler, ) => { if (typeof callback === "function") { - timeoutCallbacks.push(callback); + timeoutCallbacks.push(() => callback()); } return timeoutCallbacks.length; - }) as typeof setTimeout); + }) as unknown as typeof setTimeout); // Reset state before each test open$.next(false); diff --git a/packages/web/src/common/hooks/useVersionCheck.test.ts b/packages/web/src/common/hooks/useVersionCheck.test.ts index aa63a404c..692c2a24f 100644 --- a/packages/web/src/common/hooks/useVersionCheck.test.ts +++ b/packages/web/src/common/hooks/useVersionCheck.test.ts @@ -71,7 +71,7 @@ describe("useVersionCheck", () => { ok: true, json: async () => ({ version: "dev" }), }); - global.fetch = fetchMock as typeof fetch; + global.fetch = fetchMock as unknown as typeof fetch; }); afterEach(() => { diff --git a/packages/web/src/common/repositories/event/remote.event.repository.test.ts b/packages/web/src/common/repositories/event/remote.event.repository.test.ts index a71d1bb5e..3f0d6526b 100644 --- a/packages/web/src/common/repositories/event/remote.event.repository.test.ts +++ b/packages/web/src/common/repositories/event/remote.event.repository.test.ts @@ -37,6 +37,7 @@ mock.module("@web/common/storage/adapter/adapter", () => ({ const { RemoteEventRepository } = require("./remote.event.repository") as typeof import("./remote.event.repository"); +type RemoteEventRepositoryInstance = InstanceType; function createBackendUnavailableError(): Error { const error = new Error("Request failed"); @@ -45,7 +46,7 @@ function createBackendUnavailableError(): Error { } describe("RemoteEventRepository", () => { - let repository: RemoteEventRepository; + let repository: RemoteEventRepositoryInstance; beforeEach(() => { mockCreate.mockClear(); diff --git a/packages/web/src/common/storage/migrations/data/task-id-to-underscore-id.test.ts b/packages/web/src/common/storage/migrations/data/task-id-to-underscore-id.test.ts index 425e7ba14..e8edeee84 100644 --- a/packages/web/src/common/storage/migrations/data/task-id-to-underscore-id.test.ts +++ b/packages/web/src/common/storage/migrations/data/task-id-to-underscore-id.test.ts @@ -26,7 +26,6 @@ function createMockAdapter(): MockedStorageAdapter { clearAllEvents: mock().mockResolvedValue(undefined), getMigrationRecords: mock().mockResolvedValue([]), setMigrationRecord: mock().mockResolvedValue(undefined), - close: mock(), }; } diff --git a/packages/web/src/common/storage/migrations/external/demo-data-seed.test.ts b/packages/web/src/common/storage/migrations/external/demo-data-seed.test.ts index 0aa7acc3e..77696a8de 100644 --- a/packages/web/src/common/storage/migrations/external/demo-data-seed.test.ts +++ b/packages/web/src/common/storage/migrations/external/demo-data-seed.test.ts @@ -12,6 +12,7 @@ import { createMockStorageAdapter } from "@web/__tests__/utils/storage/mock-stor import { GridEventSchema, type Schema_GridEvent, + type Schema_WebEvent, } from "@web/common/types/web.event.types"; import { demoDataSeedMigration } from "./demo-data-seed"; import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; @@ -48,7 +49,7 @@ describe("demoDataSeedMigration", () => { expect(adapter.putTasks).toHaveBeenCalled(); // Verify events were created (7 total: 5 today + 2 someday) - const eventsCall = adapter.putEvents.mock.calls[0][0]; + const eventsCall = adapter.putEvents.mock.calls[0][0] as Schema_WebEvent[]; expect(eventsCall).toHaveLength(7); // Verify tasks were created for 3 days @@ -91,7 +92,7 @@ describe("demoDataSeedMigration", () => { await demoDataSeedMigration.migrate(adapter); - const eventsCall = adapter.putEvents.mock.calls[0][0]; + const eventsCall = adapter.putEvents.mock.calls[0][0] as Schema_WebEvent[]; const today = dayjs().toYearMonthDayString(); // Check that at least one timed event starts today @@ -110,7 +111,9 @@ describe("demoDataSeedMigration", () => { const yesterday = dayjs().subtract(1, "day").toYearMonthDayString(); const tomorrow = dayjs().add(1, "day").toYearMonthDayString(); - const putTasksCalls = adapter.putTasks.mock.calls; + const putTasksCalls = adapter.putTasks.mock.calls as Array< + [string, ReturnType[]] + >; const dateKeys = putTasksCalls.map((call) => call[0]); expect(dateKeys).toContain(today); @@ -124,11 +127,15 @@ describe("demoDataSeedMigration", () => { await demoDataSeedMigration.migrate(adapter); const yesterday = dayjs().subtract(1, "day").toYearMonthDayString(); - const putTasksCalls = adapter.putTasks.mock.calls; + const putTasksCalls = adapter.putTasks.mock.calls as Array< + [string, ReturnType[]] + >; const yesterdayCall = putTasksCalls.find((call) => call[0] === yesterday); - expect(yesterdayCall).toBeDefined(); - const yesterdayTasks = yesterdayCall![1]; + if (!yesterdayCall) { + throw new Error("Expected yesterday tasks to be seeded"); + } + const yesterdayTasks = yesterdayCall[1]; expect(yesterdayTasks.every((t) => t.status === "completed")).toBe(true); }); @@ -137,7 +144,7 @@ describe("demoDataSeedMigration", () => { await demoDataSeedMigration.migrate(adapter); - const eventsCall = adapter.putEvents.mock.calls[0][0]; + const eventsCall = adapter.putEvents.mock.calls[0][0] as Schema_WebEvent[]; const priorities = new Set(eventsCall.map((e) => e.priority)); expect(priorities.has(Priorities.WORK)).toBe(true); @@ -151,7 +158,7 @@ describe("demoDataSeedMigration", () => { await demoDataSeedMigration.migrate(adapter); - const eventsCall = adapter.putEvents.mock.calls[0][0]; + const eventsCall = adapter.putEvents.mock.calls[0][0] as Schema_WebEvent[]; const somedayEvents = eventsCall.filter((e) => e.isSomeday); expect(somedayEvents).toHaveLength(2); @@ -162,7 +169,7 @@ describe("demoDataSeedMigration", () => { await demoDataSeedMigration.migrate(adapter); - const eventsCall = adapter.putEvents.mock.calls[0][0]; + const eventsCall = adapter.putEvents.mock.calls[0][0] as Schema_WebEvent[]; const allDayEvents = eventsCall.filter((e) => e.isAllDay); expect(allDayEvents).toHaveLength(1); @@ -174,7 +181,7 @@ describe("demoDataSeedMigration", () => { await demoDataSeedMigration.migrate(adapter); - const eventsCall = adapter.putEvents.mock.calls[0][0]; + const eventsCall = adapter.putEvents.mock.calls[0][0] as Schema_WebEvent[]; const timedEvents = eventsCall.filter((e) => !e.isSomeday && !e.isAllDay); // RFC3339 offset format: "2026-02-19T16:30:00-08:00" (no milliseconds, offset instead of Z) @@ -193,7 +200,7 @@ describe("demoDataSeedMigration", () => { await demoDataSeedMigration.migrate(adapter); - const eventsCall = adapter.putEvents.mock.calls[0][0]; + const eventsCall = adapter.putEvents.mock.calls[0][0] as Schema_WebEvent[]; const timedEvents = eventsCall.filter( (e) => !e.isSomeday && !e.isAllDay, ) as Schema_GridEvent[]; @@ -219,7 +226,7 @@ describe("demoDataSeedMigration", () => { await demoDataSeedMigration.migrate(adapter); - const eventsCall = adapter.putEvents.mock.calls[0][0]; + const eventsCall = adapter.putEvents.mock.calls[0][0] as Schema_WebEvent[]; for (const event of eventsCall) { if (!event.isSomeday && !event.isAllDay) { diff --git a/packages/web/src/common/storage/migrations/external/localstorage-tasks.test.ts b/packages/web/src/common/storage/migrations/external/localstorage-tasks.test.ts index e205474c3..88339f981 100644 --- a/packages/web/src/common/storage/migrations/external/localstorage-tasks.test.ts +++ b/packages/web/src/common/storage/migrations/external/localstorage-tasks.test.ts @@ -54,7 +54,6 @@ function createMockAdapter(): MockedStorageAdapter { clearAllEvents: mock().mockResolvedValue(undefined), getMigrationRecords: mock().mockResolvedValue([]), setMigrationRecord: mock().mockResolvedValue(undefined), - close: mock(), }; } @@ -199,6 +198,7 @@ describe("localStorageTasksMigration", () => { expect(localStorage.getItem(validKey)).toBeNull(); expect(localStorage.getItem(invalidKey)).toBe("invalid json {{{"); + consoleErrorSpy.mockRestore(); }); it("skips non-array parsed values and does not remove key", async () => { diff --git a/packages/web/src/common/utils/app-init.util.test.ts b/packages/web/src/common/utils/app-init.util.test.ts index d780e59c7..84e207ca7 100644 --- a/packages/web/src/common/utils/app-init.util.test.ts +++ b/packages/web/src/common/utils/app-init.util.test.ts @@ -44,7 +44,7 @@ describe("app-init.util", () => { timeoutCallback = () => callback(); } return timeoutId; - }) as typeof setTimeout); + }) as unknown as typeof setTimeout); }); afterEach(() => { diff --git a/packages/web/src/common/utils/dom/event-emitter.util.test.ts b/packages/web/src/common/utils/dom/event-emitter.util.test.ts index 174a19eaa..c920078fc 100644 --- a/packages/web/src/common/utils/dom/event-emitter.util.test.ts +++ b/packages/web/src/common/utils/dom/event-emitter.util.test.ts @@ -35,7 +35,7 @@ describe("event-emitter.util", () => { expect(val.x).toBe(100); expect(val.y).toBe(100); expect(val.element).toBe(mockElement); - expect(val.event).toBe(event); + expect(val.event as unknown).toBe(event); subscription.unsubscribe(); resolve(); }); diff --git a/packages/web/src/common/utils/dom/event-target-visibility.util.test.ts b/packages/web/src/common/utils/dom/event-target-visibility.util.test.ts index 47797317e..cea3e954b 100644 --- a/packages/web/src/common/utils/dom/event-target-visibility.util.test.ts +++ b/packages/web/src/common/utils/dom/event-target-visibility.util.test.ts @@ -7,7 +7,8 @@ const mockIntersectionObserver = mock(); const mockObserve = mock(); const mockDisconnect = mock(); -global.IntersectionObserver = mockIntersectionObserver; +global.IntersectionObserver = + mockIntersectionObserver as unknown as typeof IntersectionObserver; describe("onEventTargetVisibility", () => { beforeEach(() => { @@ -40,7 +41,9 @@ describe("onEventTargetVisibility", () => { currentTarget: document.createElement("div"), } as SyntheticEvent; - let observerCallback: (entries: IntersectionObserverEntry[]) => void; + let observerCallback: + | ((entries: IntersectionObserverEntry[]) => void) + | undefined; mockIntersectionObserver.mockImplementation((callback) => { observerCallback = callback; @@ -53,8 +56,11 @@ describe("onEventTargetVisibility", () => { const eventHandler = onEventTargetVisibility(mockCallback, true); eventHandler(mockEvent); + if (!observerCallback) { + throw new Error("Expected IntersectionObserver callback"); + } // Simulate intersection observer callback with isIntersecting = true - observerCallback!([{ isIntersecting: true } as IntersectionObserverEntry]); + observerCallback([{ isIntersecting: true } as IntersectionObserverEntry]); expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockDisconnect).toHaveBeenCalledTimes(1); @@ -66,7 +72,9 @@ describe("onEventTargetVisibility", () => { currentTarget: document.createElement("div"), } as SyntheticEvent; - let observerCallback: (entries: IntersectionObserverEntry[]) => void; + let observerCallback: + | ((entries: IntersectionObserverEntry[]) => void) + | undefined; mockIntersectionObserver.mockImplementation((callback) => { observerCallback = callback; @@ -79,8 +87,11 @@ describe("onEventTargetVisibility", () => { const eventHandler = onEventTargetVisibility(mockCallback, false); eventHandler(mockEvent); + if (!observerCallback) { + throw new Error("Expected IntersectionObserver callback"); + } // Simulate intersection observer callback with isIntersecting = false - observerCallback!([{ isIntersecting: false } as IntersectionObserverEntry]); + observerCallback([{ isIntersecting: false } as IntersectionObserverEntry]); expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockDisconnect).toHaveBeenCalledTimes(1); @@ -92,7 +103,9 @@ describe("onEventTargetVisibility", () => { currentTarget: document.createElement("div"), } as SyntheticEvent; - let observerCallback: (entries: IntersectionObserverEntry[]) => void; + let observerCallback: + | ((entries: IntersectionObserverEntry[]) => void) + | undefined; mockIntersectionObserver.mockImplementation((callback) => { observerCallback = callback; @@ -105,8 +118,11 @@ describe("onEventTargetVisibility", () => { const eventHandler = onEventTargetVisibility(mockCallback, true); eventHandler(mockEvent); + if (!observerCallback) { + throw new Error("Expected IntersectionObserver callback"); + } // Simulate intersection observer callback with isIntersecting = false (doesn't match expected true) - observerCallback!([{ isIntersecting: false } as IntersectionObserverEntry]); + observerCallback([{ isIntersecting: false } as IntersectionObserverEntry]); expect(mockCallback).not.toHaveBeenCalled(); expect(mockDisconnect).not.toHaveBeenCalled(); @@ -118,7 +134,9 @@ describe("onEventTargetVisibility", () => { currentTarget: document.createElement("div"), } as SyntheticEvent; - let observerCallback: (entries: IntersectionObserverEntry[]) => void; + let observerCallback: + | ((entries: IntersectionObserverEntry[]) => void) + | undefined; mockIntersectionObserver.mockImplementation((callback) => { observerCallback = callback; @@ -131,8 +149,11 @@ describe("onEventTargetVisibility", () => { const eventHandler = onEventTargetVisibility(mockCallback); eventHandler(mockEvent); + if (!observerCallback) { + throw new Error("Expected IntersectionObserver callback"); + } // Simulate intersection observer callback with isIntersecting = false (matches default) - observerCallback!([{ isIntersecting: false } as IntersectionObserverEntry]); + observerCallback([{ isIntersecting: false } as IntersectionObserverEntry]); expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockDisconnect).toHaveBeenCalledTimes(1); @@ -144,7 +165,9 @@ describe("onEventTargetVisibility", () => { currentTarget: document.createElement("div"), } as SyntheticEvent; - let observerCallback: (entries: IntersectionObserverEntry[]) => void; + let observerCallback: + | ((entries: IntersectionObserverEntry[]) => void) + | undefined; mockIntersectionObserver.mockImplementation((callback) => { observerCallback = callback; @@ -157,8 +180,11 @@ describe("onEventTargetVisibility", () => { const eventHandler = onEventTargetVisibility(mockCallback, true); eventHandler(mockEvent); + if (!observerCallback) { + throw new Error("Expected IntersectionObserver callback"); + } // Simulate intersection observer callback with multiple entries - observerCallback!([ + observerCallback([ { isIntersecting: true } as IntersectionObserverEntry, { isIntersecting: false } as IntersectionObserverEntry, ]); diff --git a/packages/web/src/common/utils/event/event.util.test.ts b/packages/web/src/common/utils/event/event.util.test.ts index 1a186e465..200b01744 100644 --- a/packages/web/src/common/utils/event/event.util.test.ts +++ b/packages/web/src/common/utils/event/event.util.test.ts @@ -67,8 +67,9 @@ describe("_assembleGridEvent", () => { it("should successfully convert Someday event to Grid event by adding position field", () => { // Create a mock Someday event (without position field) const somedayEvent = createMockStandaloneEvent({ + _id: new ObjectId().toString(), isSomeday: true, - }) as Schema_WebEvent; + }) as Schema_WebEvent & { _id: string }; const generator = _assembleGridEvent(somedayEvent); @@ -93,7 +94,10 @@ describe("_assembleGridEvent", () => { describe("addId", () => { it("should add a raw MongoID", () => { - const event = createMockStandaloneEvent() as Schema_GridEvent; + const event = { + ...createMockStandaloneEvent(), + _id: "existing-id", + } as Schema_GridEvent; const result = addId(event); expect(result._id).toBeDefined(); diff --git a/packages/web/src/common/utils/grid/assign.row.test.ts b/packages/web/src/common/utils/grid/assign.row.test.ts index cdf388711..d7c175d97 100644 --- a/packages/web/src/common/utils/grid/assign.row.test.ts +++ b/packages/web/src/common/utils/grid/assign.row.test.ts @@ -116,7 +116,9 @@ describe("case: 10 multi-week events with identical times", () => { }); describe("case: March 13-19", () => { - const { rowsCount, allDayEvents } = assignEventsToRow(mar13To19); + const { rowsCount, allDayEvents } = assignEventsToRow( + mar13To19 as Parameters[0], + ); it("uses 5 rows", () => { expect(rowsCount).toBe(5); }); diff --git a/packages/web/src/common/validators/grid.event.validator.test.ts b/packages/web/src/common/validators/grid.event.validator.test.ts index 23090aa8c..ef4c39403 100644 --- a/packages/web/src/common/validators/grid.event.validator.test.ts +++ b/packages/web/src/common/validators/grid.event.validator.test.ts @@ -73,6 +73,6 @@ describe("validateGridEvent", () => { }, }; - expect(() => validateGridEvent(event)).toThrow(); + expect(() => validateGridEvent(event as Schema_GridEvent)).toThrow(); }); }); diff --git a/packages/web/src/components/DND/DNDContext.test.tsx b/packages/web/src/components/DND/DNDContext.test.tsx index 7a65fc180..76cebae8e 100644 --- a/packages/web/src/components/DND/DNDContext.test.tsx +++ b/packages/web/src/components/DND/DNDContext.test.tsx @@ -10,7 +10,14 @@ import { spyOn, } from "bun:test"; -const DndContext = mock(({ children }: { children: ReactNode }) => ( +type MockDndContextProps = { + children: ReactNode; + onDragAbort: () => void; + onDragCancel: () => void; + onDragEnd: () => void; +}; + +const DndContext = mock(({ children }: MockDndContextProps) => (
{children}
)); const usePointerPosition = mock(); diff --git a/packages/web/src/components/HeaderInfoIcon/HeaderInfoIcon.test.tsx b/packages/web/src/components/HeaderInfoIcon/HeaderInfoIcon.test.tsx index 4accd07ed..2c6780cef 100644 --- a/packages/web/src/components/HeaderInfoIcon/HeaderInfoIcon.test.tsx +++ b/packages/web/src/components/HeaderInfoIcon/HeaderInfoIcon.test.tsx @@ -46,7 +46,7 @@ const mockUseConnectGoogle = mock( }), ); const mockShouldShowAnonymousCalendarChangeSignUpPrompt = mock(() => false); -const mockSubscribeToAuthState = mock(() => () => {}); +const mockSubscribeToAuthState = mock((_listener: () => void) => () => {}); const mockClearAnonymousCalendarChangeSignUpPrompt = mock(); const mockClearAuthenticationState = mock(); const mockGetAuthState = mock(); diff --git a/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.test.tsx b/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.test.tsx index edb89ea0c..0d5c36c52 100644 --- a/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.test.tsx +++ b/packages/web/src/components/SyncEventsOverlay/SyncEventsOverlay.test.tsx @@ -89,10 +89,10 @@ describe("SyncEventsOverlay", () => { if (typeof callback === "function") { pendingTimers.push(() => callback()); } - return 1 as ReturnType; - }) as typeof setTimeout); + return 1; + }) as unknown as typeof setTimeout); clearTimeoutSpy = spyOn(globalThis, "clearTimeout").mockImplementation( - (() => undefined) as typeof clearTimeout, + (() => undefined) as unknown as typeof clearTimeout, ); document.body.removeAttribute("data-app-locked"); }); diff --git a/packages/web/src/ducks/events/sagas/event.sagas.prompt.test.ts b/packages/web/src/ducks/events/sagas/event.sagas.prompt.test.ts index 03dfb6b2f..5331420cc 100644 --- a/packages/web/src/ducks/events/sagas/event.sagas.prompt.test.ts +++ b/packages/web/src/ducks/events/sagas/event.sagas.prompt.test.ts @@ -38,9 +38,10 @@ const { createEvent, editEvent } = require("./event.sagas") as typeof import("./event.sagas"); describe("event sign-up prompt failure paths", () => { + const mockEventId = "event-1"; const mockEvent = { ...createMockStandaloneEvent(), - _id: "event-1", + _id: mockEventId, isSomeday: false, } as Schema_Event; @@ -106,7 +107,7 @@ describe("event sign-up prompt failure paths", () => { events: { entities: { value: { - [mockEvent._id]: mockEvent, + [mockEventId]: mockEvent, }, }, pendingEvents: { eventIds: [] }, diff --git a/packages/web/src/ducks/events/sagas/event.sagas.test.ts b/packages/web/src/ducks/events/sagas/event.sagas.test.ts index a1771cb65..0b406e3ca 100644 --- a/packages/web/src/ducks/events/sagas/event.sagas.test.ts +++ b/packages/web/src/ducks/events/sagas/event.sagas.test.ts @@ -244,7 +244,7 @@ describe("createEvent saga - optimistic rendering", () => { it("should keep event in state during API call", async () => { // Create a promise that we can control - let resolveApiCall: (value: ApiResponse) => void; + let resolveApiCall!: (value: ApiResponse) => void; const apiPromise = new Promise>((resolve) => { resolveApiCall = resolve; }); @@ -283,7 +283,7 @@ describe("createEvent saga - optimistic rendering", () => { expect(dayEventIdsDuringCall).toContain(optimisticId); // Resolve the API call - resolveApiCall?.({ + resolveApiCall({ status: 200, } as ApiResponse); @@ -552,7 +552,9 @@ describe("pending events state management", () => { iterator.next(); expect( - iterator.next(stateAfterCreate.events.entities.value?.[eventId]).value, + iterator.next( + stateAfterCreate.events.entities.value?.[eventId] as never, + ).value, ).toEqual(put(pendingEventsSlice.actions.add(eventId))); }); @@ -587,7 +589,7 @@ describe("pending events state management", () => { } as never); iterator.next(); - expect(iterator.next(existingEvent).value).toEqual( + expect(iterator.next(existingEvent as never).value).toEqual( put(pendingEventsSlice.actions.add(eventId)), ); expect(iterator.next().value).toEqual( @@ -636,7 +638,7 @@ describe("pending events state management", () => { } as never); iterator.next(); - expect(iterator.next(existingEvent).value).toEqual( + expect(iterator.next(existingEvent as never).value).toEqual( put(pendingEventsSlice.actions.add(eventId)), ); expect(iterator.next().value).toEqual( diff --git a/packages/web/src/views/Calendar/components/Draft/hooks/actions/submit.parser.test.ts b/packages/web/src/views/Calendar/components/Draft/hooks/actions/submit.parser.test.ts index d563fdf99..e608d09e8 100644 --- a/packages/web/src/views/Calendar/components/Draft/hooks/actions/submit.parser.test.ts +++ b/packages/web/src/views/Calendar/components/Draft/hooks/actions/submit.parser.test.ts @@ -5,23 +5,60 @@ import { } from "@web/common/types/web.event.types"; import { afterAll, describe, expect, it, mock } from "bun:test"; +const createMockGridPosition = (): Schema_GridEvent["position"] => ({ + isOverlapping: false, + totalEventsInGroup: 1, + widthMultiplier: 1, + horizontalOrder: 1, + dragOffset: { x: 0, y: 0 }, + initialX: null, + initialY: null, +}); + +const createMockGridEvent = ( + overrides: Partial = {}, +): Schema_GridEvent => ({ + _id: "test-grid-event-id", + title: "Test Grid Event", + startDate: "2024-01-15T10:00:00Z", + endDate: "2024-01-15T11:00:00Z", + isAllDay: false, + isSomeday: false, + origin: Origin.COMPASS, + priority: Priorities.UNASSIGNED, + user: "test-user", + position: createMockGridPosition(), + ...overrides, +}); + +const createMockSomedayEvent = ( + overrides: Partial = {}, +): Schema_SomedayEvent => ({ + _id: "test-someday-event-id", + title: "Test Someday Event", + startDate: "2024-01-15T10:00:00Z", + endDate: "2024-01-15T11:00:00Z", + isAllDay: false, + isSomeday: true, + origin: Origin.COMPASS, + priority: Priorities.UNASSIGNED, + user: "test-user", + order: 1, + ...overrides, +}); + const validateGridEvent = mock( (event: Schema_GridEvent): Schema_GridEvent => event, ); const validateSomedayEvent = mock( (event: Schema_SomedayEvent): Schema_SomedayEvent => event, ); -const assembleGridEvent = mock((event: Partial) => ({ - ...event, - position: { - isOverlapping: false, - widthMultiplier: 1, - horizontalOrder: 1, - dragOffset: { y: 0 }, - initialX: null, - initialY: null, - }, -})); +const assembleGridEvent = mock((event: Partial) => + createMockGridEvent({ + ...event, + position: event.position ?? createMockGridPosition(), + }), +); mock.module("@web/common/validators/grid.event.validator", () => ({ validateGridEvent, @@ -39,45 +76,6 @@ const { OnSubmitParser, parseSomedayEventBeforeSubmit, prepEventBeforeSubmit } = require("./submit.parser") as typeof import("./submit.parser"); describe("submit.parser", () => { - const createMockGridEvent = ( - overrides: Partial = {}, - ): Schema_GridEvent => ({ - _id: "test-grid-event-id", - title: "Test Grid Event", - startDate: "2024-01-15T10:00:00Z", - endDate: "2024-01-15T11:00:00Z", - isAllDay: false, - isSomeday: false, - origin: Origin.COMPASS, - priority: Priorities.UNASSIGNED, - user: "test-user", - position: { - isOverlapping: false, - widthMultiplier: 1, - horizontalOrder: 1, - dragOffset: { y: 0 }, - initialX: null, - initialY: null, - }, - ...overrides, - }); - - const createMockSomedayEvent = ( - overrides: Partial = {}, - ): Schema_SomedayEvent => ({ - _id: "test-someday-event-id", - title: "Test Someday Event", - startDate: "2024-01-15T10:00:00Z", - endDate: "2024-01-15T11:00:00Z", - isAllDay: false, - isSomeday: true, - origin: Origin.COMPASS, - priority: Priorities.UNASSIGNED, - user: "test-user", - order: 1, - ...overrides, - }); - describe("OnSubmitParser", () => { describe("constructor", () => { it("should initialize with a grid event", () => { @@ -276,9 +274,10 @@ describe("submit.parser", () => { const draft = createMockGridEvent({ position: { isOverlapping: true, + totalEventsInGroup: 2, widthMultiplier: 0.5, horizontalOrder: 2, - dragOffset: { y: 10 }, + dragOffset: { x: 0, y: 10 }, initialX: 100, initialY: 200, }, diff --git a/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.test.ts b/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.test.ts index 68b00c724..d615485e2 100644 --- a/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.test.ts +++ b/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.test.ts @@ -50,9 +50,10 @@ const createDraft = ( user: "user-1", position: { isOverlapping: false, + totalEventsInGroup: 1, widthMultiplier: 1, horizontalOrder: 1, - dragOffset: { y: 0 }, + dragOffset: { x: 0, y: 0 }, initialX: null, initialY: null, }, @@ -134,7 +135,7 @@ describe("useDraftActions", () => { reason: null, value: { count: 1, - data: [draft._id], + data: ["event-1"], offset: 0, page: 1, pageSize: 1, diff --git a/packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.test.ts b/packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.test.ts index 7c6f88a40..0774ab217 100644 --- a/packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.test.ts +++ b/packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.test.ts @@ -24,9 +24,10 @@ const createDraft = ( user: "user-1", position: { isOverlapping: false, + totalEventsInGroup: 1, widthMultiplier: 1, horizontalOrder: 1, - dragOffset: { y: 0 }, + dragOffset: { x: 0, y: 0 }, initialX: null, initialY: null, }, diff --git a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.test.ts b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.test.ts index 80ece4c4d..35f48df17 100644 --- a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.test.ts +++ b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.test.ts @@ -15,9 +15,6 @@ describe("snapToGrid", () => { y: 0, bottom: 0, right: 0, - toJSON: () => { - throw new Error("Function not implemented."); - }, }, hourHeight: 60, colWidths: Array(7).fill(100) as number[], diff --git a/packages/web/src/views/Day/components/Toasts/UndoToast/UndoDeleteToast.test.tsx b/packages/web/src/views/Day/components/Toasts/UndoToast/UndoDeleteToast.test.tsx index 480e57546..0e120153f 100644 --- a/packages/web/src/views/Day/components/Toasts/UndoToast/UndoDeleteToast.test.tsx +++ b/packages/web/src/views/Day/components/Toasts/UndoToast/UndoDeleteToast.test.tsx @@ -3,7 +3,12 @@ import "@testing-library/jest-dom"; import { fireEvent, render, screen } from "@testing-library/react"; import { getModifierKeyTestId } from "@web/common/utils/shortcut/shortcut.util"; -const toastMock = mock(() => "test-toast-id"); +type ToastMock = ReturnType & { + dismiss: ReturnType; + update: ReturnType; +}; + +const toastMock = mock(() => "test-toast-id") as ToastMock; toastMock.dismiss = mock(); toastMock.update = mock(); @@ -50,7 +55,10 @@ describe("UndoDeleteToast", () => { ); const toastButton = screen.getByText("Deleted").closest("button"); - fireEvent.click(toastButton!); + if (!toastButton) { + throw new Error("Expected toast button"); + } + fireEvent.click(toastButton); expect(mockOnRestore).toHaveBeenCalledTimes(1); }); @@ -63,7 +71,10 @@ describe("UndoDeleteToast", () => { ); const toastButton = screen.getByText("Deleted").closest("button"); - fireEvent.click(toastButton!); + if (!toastButton) { + throw new Error("Expected toast button"); + } + fireEvent.click(toastButton); expect(toastMock.dismiss).toHaveBeenCalledWith(testToastId); }); diff --git a/packages/web/src/views/Day/context/__tests__/DNDTasksContext.test.tsx b/packages/web/src/views/Day/context/__tests__/DNDTasksContext.test.tsx index 82b6f9ccc..ebc0cf9f6 100644 --- a/packages/web/src/views/Day/context/__tests__/DNDTasksContext.test.tsx +++ b/packages/web/src/views/Day/context/__tests__/DNDTasksContext.test.tsx @@ -116,9 +116,12 @@ describe("DNDTasksProvider", () => { expect(screen.getByTestId("context-value")).toHaveTextContent( "has-context", ); - expect(contextValue).toBeDefined(); - expect(typeof contextValue.onDragStart).toBe("function"); - expect(typeof contextValue.onDragEnd).toBe("function"); + const value = contextValue as DNDTasksContextValue | null; + if (!value) { + throw new Error("Expected DND tasks context to be available"); + } + expect(typeof value.onDragStart).toBe("function"); + expect(typeof value.onDragEnd).toBe("function"); }); it("should call setSelectedTaskIndex and announce on drag start", () => { diff --git a/packages/web/src/views/Day/hooks/events/useAgendaInteractionsAtCursor.test.ts b/packages/web/src/views/Day/hooks/events/useAgendaInteractionsAtCursor.test.ts index 5e9b1c437..faee240c4 100644 --- a/packages/web/src/views/Day/hooks/events/useAgendaInteractionsAtCursor.test.ts +++ b/packages/web/src/views/Day/hooks/events/useAgendaInteractionsAtCursor.test.ts @@ -1,4 +1,7 @@ -import { type useFloating } from "@floating-ui/react"; +import { + type useFloating, + type useInteractions as useFloatingInteractions, +} from "@floating-ui/react"; import { renderHook } from "@testing-library/react"; import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; @@ -40,7 +43,11 @@ describe("useAgendaInteractionsAtCursor", () => { const mockHover = {}; const mockFocus = {}; const mockDismiss = {}; - const mockInteractions = { getReferenceProps: mock() }; + const mockInteractions = { + getFloatingProps: mock(), + getItemProps: mock(), + getReferenceProps: mock(), + } as ReturnType; useClick.mockReturnValue(mockClick); useHover.mockReturnValue(mockHover); diff --git a/packages/web/src/views/Day/util/time/time.util.test.ts b/packages/web/src/views/Day/util/time/time.util.test.ts index d98f36d28..43bb84108 100644 --- a/packages/web/src/views/Day/util/time/time.util.test.ts +++ b/packages/web/src/views/Day/util/time/time.util.test.ts @@ -34,7 +34,7 @@ describe("setupMinuteSync", () => { intervalCallback = () => callback(); } return intervalId; - }) as typeof setInterval); + }) as unknown as typeof setInterval); setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation((( callback: TimerHandler, ) => { @@ -42,7 +42,7 @@ describe("setupMinuteSync", () => { timeoutCallback = () => callback(); } return timeoutId; - }) as typeof setTimeout); + }) as unknown as typeof setTimeout); }); afterEach(() => { diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx index 88e6f5db3..7447286e2 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx @@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"; import { useCallback, useState } from "react"; import { ThemeProvider } from "styled-components"; import { Origin, Priorities } from "@core/constants/core.constants"; +import { type Schema_Event } from "@core/types/event.types"; import { SessionContext } from "@web/auth/compass/session/SessionProvider"; import { markBackendUnavailable, @@ -36,7 +37,7 @@ function renderRecurrenceSection({ const setEventSpy = mock(); function Harness() { - const [event, setEvent] = useState(baseEvent()); + const [event, setEvent] = useState(baseEvent()); const handleSetEvent = useCallback((nextEvent) => { setEventSpy(nextEvent); setEvent(nextEvent); diff --git a/packages/web/tsconfig.test.json b/packages/web/tsconfig.test.json new file mode 100644 index 000000000..81fdd9bd8 --- /dev/null +++ b/packages/web/tsconfig.test.json @@ -0,0 +1,28 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./declaration.d.ts", + "./src/**/*.d.ts", + "./src/**/*.test.ts", + "./src/**/*.test.tsx", + "./src/**/*.test.utils.ts", + "./src/**/*.test.utils.tsx", + "./src/**/*.spec.ts", + "./src/**/*.spec.tsx", + "./src/**/*.test-util.ts", + "./src/**/*.test-util.tsx" + ], + "compilerOptions": { + "noEmit": true, + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "paths": { + "@web/*": ["./src/*"], + "@core/*": ["../core/src/*"], + "@backend/*": ["../backend/src/*"] + } + } +}