diff --git a/apps/web/src/app/(app)/resources/ResourcesClient.tsx b/apps/web/src/app/(app)/resources/ResourcesClient.tsx index 510d341..f2f6cf7 100644 --- a/apps/web/src/app/(app)/resources/ResourcesClient.tsx +++ b/apps/web/src/app/(app)/resources/ResourcesClient.tsx @@ -1,7 +1,6 @@ "use client"; -import { createPortal } from "react-dom"; -import { useState, useEffect, useCallback, useMemo, useRef, type ReactNode } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useUrlFilters } from "~/hooks/useUrlFilters.js"; import { useDebounce } from "~/hooks/useDebounce.js"; import Link from "next/link"; @@ -35,118 +34,21 @@ import { usePermissions } from "~/hooks/usePermissions.js"; import { useColumnConfig } from "~/hooks/useColumnConfig.js"; import { useViewPrefs } from "~/hooks/useViewPrefs.js"; import { useRowOrder } from "~/hooks/useRowOrder.js"; -import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js"; import { DraggableTableRow } from "~/components/ui/DraggableTableRow.js"; import { SuccessToast } from "~/components/ui/SuccessToast.js"; - -type ModalState = - | { type: "closed" } - | { type: "create" } - | { type: "edit"; resource: Resource } - | { type: "import" } - | { type: "bulkEdit" }; - -type ConfirmState = - | { type: "closed" } - | { type: "batchDeactivate"; ids: string[] } - | { type: "deactivate"; resource: Resource } - | { type: "delete"; resource: Resource } - | { type: "batchDelete"; ids: string[] }; - -type ActiveFilter = "active" | "inactive" | "all"; -type BooleanFilter = "all" | "yes" | "no"; -type ResourceListPage = { - resources: Resource[]; - total: number; - nextCursor?: string | null; -}; - -type CountryOption = { - id: string; - name: string; -}; - -const DEFAULT_HIDDEN_RESOURCE_TYPES = [ResourceType.FREELANCER] as const; -const DEFAULT_BOOLEAN_FILTER: BooleanFilter = "no"; - -const RESOURCE_TYPE_LABELS: Record = { - [ResourceType.EMPLOYEE]: "Employee", - [ResourceType.FREELANCER]: "Freelancer", - [ResourceType.APPRENTICE]: "Apprentice", - [ResourceType.INTERN]: "Intern", - [ResourceType.STUDENT]: "Student", -}; - -function FilterDropdown({ - label, - children, - widthClassName = "w-80", - buttonClassName = "min-w-56", - tooltipContent, -}: { - label: string; - children: ReactNode; - widthClassName?: string; - buttonClassName?: string; - tooltipContent?: ReactNode; -}) { - const [isOpen, setIsOpen] = useState(false); - const { triggerRef, panelRef, position, handleOpenChange } = useAnchoredOverlay({ - open: isOpen, - onClose: () => setIsOpen(false), - matchTriggerWidth: true, - }); - - return ( -
-
- - {tooltipContent ? : null} -
- {isOpen && - createPortal( -
- {children} -
, - document.body, - )} -
- ); -} - -function BooleanBadge({ value }: { value: boolean }) { - return ( - - {value ? "Yes" : "No"} - - ); -} +import { + type ModalState, + type ConfirmState, + type ActiveFilter, + type BooleanFilter, + type ResourceListPage, + type CountryOption, + DEFAULT_HIDDEN_RESOURCE_TYPES, + DEFAULT_BOOLEAN_FILTER, + RESOURCE_TYPE_LABELS, +} from "./resource-client/types.js"; +import { FilterDropdown } from "./resource-client/FilterDropdown.js"; +import { BooleanBadge } from "./resource-client/BooleanBadge.js"; export function ResourcesClient() { const [resourceUrlFilters, setResourceUrlFilters] = useUrlFilters({ diff --git a/apps/web/src/app/(app)/resources/resource-client/BooleanBadge.tsx b/apps/web/src/app/(app)/resources/resource-client/BooleanBadge.tsx new file mode 100644 index 0000000..73e2913 --- /dev/null +++ b/apps/web/src/app/(app)/resources/resource-client/BooleanBadge.tsx @@ -0,0 +1,15 @@ +"use client"; + +export function BooleanBadge({ value }: { value: boolean }) { + return ( + + {value ? "Yes" : "No"} + + ); +} diff --git a/apps/web/src/app/(app)/resources/resource-client/FilterDropdown.tsx b/apps/web/src/app/(app)/resources/resource-client/FilterDropdown.tsx new file mode 100644 index 0000000..6bc283c --- /dev/null +++ b/apps/web/src/app/(app)/resources/resource-client/FilterDropdown.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useState, type ReactNode } from "react"; +import { createPortal } from "react-dom"; +import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js"; +import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; + +export function FilterDropdown({ + label, + children, + widthClassName = "w-80", + buttonClassName = "min-w-56", + tooltipContent, +}: { + label: string; + children: ReactNode; + widthClassName?: string; + buttonClassName?: string; + tooltipContent?: ReactNode; +}) { + const [isOpen, setIsOpen] = useState(false); + const { triggerRef, panelRef, position, handleOpenChange } = useAnchoredOverlay({ + open: isOpen, + onClose: () => setIsOpen(false), + matchTriggerWidth: true, + }); + + return ( +
+
+ + {tooltipContent ? : null} +
+ {isOpen && + createPortal( +
+ {children} +
, + document.body, + )} +
+ ); +} diff --git a/apps/web/src/app/(app)/resources/resource-client/types.ts b/apps/web/src/app/(app)/resources/resource-client/types.ts new file mode 100644 index 0000000..f6f95c0 --- /dev/null +++ b/apps/web/src/app/(app)/resources/resource-client/types.ts @@ -0,0 +1,40 @@ +import type { Resource } from "@capakraken/shared"; +import { ResourceType } from "@capakraken/shared"; + +export type ModalState = + | { type: "closed" } + | { type: "create" } + | { type: "edit"; resource: Resource } + | { type: "import" } + | { type: "bulkEdit" }; + +export type ConfirmState = + | { type: "closed" } + | { type: "batchDeactivate"; ids: string[] } + | { type: "deactivate"; resource: Resource } + | { type: "delete"; resource: Resource } + | { type: "batchDelete"; ids: string[] }; + +export type ActiveFilter = "active" | "inactive" | "all"; +export type BooleanFilter = "all" | "yes" | "no"; +export type ResourceListPage = { + resources: Resource[]; + total: number; + nextCursor?: string | null; +}; + +export type CountryOption = { + id: string; + name: string; +}; + +export const DEFAULT_HIDDEN_RESOURCE_TYPES = [ResourceType.FREELANCER] as const; +export const DEFAULT_BOOLEAN_FILTER: BooleanFilter = "no"; + +export const RESOURCE_TYPE_LABELS: Record = { + [ResourceType.EMPLOYEE]: "Employee", + [ResourceType.FREELANCER]: "Freelancer", + [ResourceType.APPRENTICE]: "Apprentice", + [ResourceType.INTERN]: "Intern", + [ResourceType.STUDENT]: "Student", +}; diff --git a/apps/web/src/components/projects/project-wizard/useProjectWizardForm.test.ts b/apps/web/src/components/projects/project-wizard/useProjectWizardForm.test.ts index 58b7319..2c9f770 100644 --- a/apps/web/src/components/projects/project-wizard/useProjectWizardForm.test.ts +++ b/apps/web/src/components/projects/project-wizard/useProjectWizardForm.test.ts @@ -41,7 +41,14 @@ describe("canGoNext", () => { shortCode: "PRJ01", name: "Test", blueprintFieldDefs: [ - { key: "field1", type: "TEXT" as never, label: "Field 1", required: true }, + { + id: "f1", + key: "field1", + type: "TEXT" as never, + label: "Field 1", + required: true, + order: 0, + }, ], dynamicFields: {}, }); @@ -53,7 +60,14 @@ describe("canGoNext", () => { shortCode: "PRJ01", name: "Test", blueprintFieldDefs: [ - { key: "field1", type: "TEXT" as never, label: "Field 1", required: true }, + { + id: "f1", + key: "field1", + type: "TEXT" as never, + label: "Field 1", + required: true, + order: 0, + }, ], dynamicFields: { field1: "value" }, }); @@ -65,7 +79,14 @@ describe("canGoNext", () => { shortCode: "PRJ01", name: "Test", blueprintFieldDefs: [ - { key: "tags", type: "TAG_LIST" as never, label: "Tags", required: true }, + { + id: "f2", + key: "tags", + type: "TAG_LIST" as never, + label: "Tags", + required: true, + order: 0, + }, ], dynamicFields: { tags: [] }, }); @@ -77,7 +98,14 @@ describe("canGoNext", () => { shortCode: "PRJ01", name: "Test", blueprintFieldDefs: [ - { key: "optional", type: "TEXT" as never, label: "Optional", required: false }, + { + id: "f3", + key: "optional", + type: "TEXT" as never, + label: "Optional", + required: false, + order: 0, + }, ], dynamicFields: {}, }); @@ -153,7 +181,7 @@ describe("canGoNext", () => { }); it("returns false when a requirement has empty role and no roleId", () => { - const req = { ...makeReq(), role: "", roleId: undefined, hoursPerDay: 8, headcount: 1 }; + const req = { ...makeReq(), role: "", hoursPerDay: 8, headcount: 1 }; const state = stateWith({ staffingReqs: [req] }); expect(canGoNext(2, state)).toBe(false); }); diff --git a/apps/web/src/components/ui/ErrorBoundary.test.tsx b/apps/web/src/components/ui/ErrorBoundary.test.tsx index 261ba8f..a7a6fd2 100644 --- a/apps/web/src/components/ui/ErrorBoundary.test.tsx +++ b/apps/web/src/components/ui/ErrorBoundary.test.tsx @@ -1,7 +1,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { render, screen } from "~/test-utils.js"; import userEvent from "@testing-library/user-event"; -import type { ErrorInfo } from "react"; +import React, { type ErrorInfo } from "react"; import { ErrorBoundary, DefaultErrorFallback } from "./ErrorBoundary.js"; // Suppress React's console.error output during error boundary tests @@ -16,7 +16,7 @@ afterEach(() => { }); // A component that unconditionally throws -function ThrowingChild({ message = "Test error" }: { message?: string }) { +function ThrowingChild({ message = "Test error" }: { message?: string }): React.ReactNode { throw new Error(message); } @@ -131,7 +131,7 @@ describe("ErrorBoundary", () => { }); it("shows a generic message when error.message is empty", () => { - function ThrowEmptyMessage() { + function ThrowEmptyMessage(): React.ReactNode { const e = new Error(""); throw e; } diff --git a/apps/web/src/components/ui/ShimmerSkeleton.test.tsx b/apps/web/src/components/ui/ShimmerSkeleton.test.tsx index a3282b0..fd3c5ed 100644 --- a/apps/web/src/components/ui/ShimmerSkeleton.test.tsx +++ b/apps/web/src/components/ui/ShimmerSkeleton.test.tsx @@ -88,7 +88,9 @@ describe("ShimmerSkeleton", () => { }); describe("rounded prop", () => { - const roundedCases: Array<[React.ComponentProps["rounded"], string]> = [ + const roundedCases: Array< + [NonNullable["rounded"]>, string] + > = [ ["sm", "rounded-sm"], ["md", "rounded-md"], ["lg", "rounded-lg"], diff --git a/apps/web/src/hooks/useFilters.test.ts b/apps/web/src/hooks/useFilters.test.ts index 4e51557..f189d26 100644 --- a/apps/web/src/hooks/useFilters.test.ts +++ b/apps/web/src/hooks/useFilters.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { renderHook, act } from "@testing-library/react"; +import { FieldType } from "@capakraken/shared"; // --------------------------------------------------------------------------- // Mock next/navigation — must be registered before the module under test is @@ -33,12 +34,12 @@ function setSearchParams(entries: [string, string][]) { function lastReplaceUrl(): string { const calls = mockReplace.mock.calls; - return calls[calls.length - 1][0] as string; + return calls[calls.length - 1]![0] as string; } function lastReplaceOpts(): unknown { const calls = mockReplace.mock.calls; - return calls[calls.length - 1][1]; + return calls[calls.length - 1]![1]; } // --------------------------------------------------------------------------- @@ -130,11 +131,11 @@ describe("useFilters", () => { it("parses a single custom field filter from cf_/cft_ params", () => { setSearchParams([ ["cf_rating", "5"], - ["cft_rating", "NUMBER"], + ["cft_rating", FieldType.NUMBER], ]); const { result } = renderHook(() => useFilters()); expect(result.current.customFieldFilters).toEqual([ - { key: "rating", value: "5", type: "NUMBER" }, + { key: "rating", value: "5", type: FieldType.NUMBER }, ]); }); @@ -142,7 +143,7 @@ describe("useFilters", () => { setSearchParams([["cf_tag", "hero"]]); const { result } = renderHook(() => useFilters()); expect(result.current.customFieldFilters).toEqual([ - { key: "tag", value: "hero", type: "TEXT" }, + { key: "tag", value: "hero", type: FieldType.TEXT }, ]); }); @@ -151,7 +152,7 @@ describe("useFilters", () => { ["cf_dept", "vfx"], ["cft_dept", "SELECT"], ["cf_level", "senior"], - ["cft_level", "TEXT"], + ["cft_level", FieldType.TEXT], ]); const { result } = renderHook(() => useFilters()); expect(result.current.customFieldFilters).toHaveLength(2); @@ -163,7 +164,7 @@ describe("useFilters", () => { it("excludes cft_ entries from customFieldFilters (only cf_ entries are rows)", () => { setSearchParams([ ["cf_x", "1"], - ["cft_x", "NUMBER"], + ["cft_x", FieldType.NUMBER], ]); const { result } = renderHook(() => useFilters()); expect(result.current.customFieldFilters).toHaveLength(1); @@ -172,7 +173,7 @@ describe("useFilters", () => { it("skips custom field filter when the value is empty string", () => { setSearchParams([ ["cf_empty", ""], - ["cft_empty", "TEXT"], + ["cft_empty", FieldType.TEXT], ]); const { result } = renderHook(() => useFilters()); expect(result.current.customFieldFilters).toHaveLength(0); @@ -245,7 +246,7 @@ describe("useFilters", () => { it("sets cf_ and cft_ params for a new custom field filter", () => { const { result } = renderHook(() => useFilters()); act(() => { - result.current.setCustomFieldFilter("score", "10", "NUMBER"); + result.current.setCustomFieldFilter("score", "10", FieldType.NUMBER); }); const url = lastReplaceUrl(); expect(url).toContain("cf_score=10"); @@ -255,11 +256,11 @@ describe("useFilters", () => { it("removes cf_ and cft_ params when value is empty", () => { setSearchParams([ ["cf_score", "10"], - ["cft_score", "NUMBER"], + ["cft_score", FieldType.NUMBER], ]); const { result } = renderHook(() => useFilters()); act(() => { - result.current.setCustomFieldFilter("score", "", "NUMBER"); + result.current.setCustomFieldFilter("score", "", FieldType.NUMBER); }); const url = lastReplaceUrl(); expect(url).not.toContain("cf_score"); @@ -270,11 +271,11 @@ describe("useFilters", () => { setSearchParams([ ["search", "alice"], ["cf_score", "10"], - ["cft_score", "NUMBER"], + ["cft_score", FieldType.NUMBER], ]); const { result } = renderHook(() => useFilters()); act(() => { - result.current.setCustomFieldFilter("score", "", "NUMBER"); + result.current.setCustomFieldFilter("score", "", FieldType.NUMBER); }); const url = lastReplaceUrl(); expect(url).toContain("search=alice"); @@ -283,7 +284,7 @@ describe("useFilters", () => { it("calls router.replace with scroll: false", () => { const { result } = renderHook(() => useFilters()); act(() => { - result.current.setCustomFieldFilter("tag", "hero", "TEXT"); + result.current.setCustomFieldFilter("tag", "hero", FieldType.TEXT); }); expect(lastReplaceOpts()).toEqual({ scroll: false }); }); diff --git a/apps/web/src/hooks/useLocalStorage.test.ts b/apps/web/src/hooks/useLocalStorage.test.ts index 7c54de9..8c4eafc 100644 --- a/apps/web/src/hooks/useLocalStorage.test.ts +++ b/apps/web/src/hooks/useLocalStorage.test.ts @@ -109,7 +109,7 @@ describe("useLocalStorage", () => { }); it("handles object values", () => { - const { result } = renderHook(() => useLocalStorage("obj", { a: 1 })); + const { result } = renderHook(() => useLocalStorage>("obj", { a: 1 })); act(() => { result.current[1]({ a: 2, b: 3 }); diff --git a/apps/web/src/hooks/useMultiSelectIntersection.test.ts b/apps/web/src/hooks/useMultiSelectIntersection.test.ts index 1157715..de4e202 100644 --- a/apps/web/src/hooks/useMultiSelectIntersection.test.ts +++ b/apps/web/src/hooks/useMultiSelectIntersection.test.ts @@ -52,6 +52,9 @@ function makeState(overrides: Partial = {}): MultiSelectState selectedAllocationIds: [], selectedResourceIds: [], dateRange: null, + multiDragDaysDelta: 0, + isMultiDragging: false, + multiDragMode: "move", ...overrides, }; } @@ -362,7 +365,7 @@ describe("useMultiSelectIntersection", () => { }); expect(setMultiSelectState).toHaveBeenCalledOnce(); - const updaterFn = setMultiSelectState.mock.calls[0][0] as ( + const updaterFn = setMultiSelectState.mock.calls[0]![0] as ( prev: MultiSelectState, ) => MultiSelectState; const prevState = makeState(); @@ -414,7 +417,7 @@ describe("useMultiSelectIntersection", () => { }); expect(setMultiSelectState).toHaveBeenCalledOnce(); - const updaterFn = setMultiSelectState.mock.calls[0][0] as ( + const updaterFn = setMultiSelectState.mock.calls[0]![0] as ( prev: MultiSelectState, ) => MultiSelectState; const nextState = updaterFn(makeState()); @@ -475,7 +478,7 @@ describe("useMultiSelectIntersection", () => { }); expect(setMultiSelectState).toHaveBeenCalledOnce(); - const updaterFn = setMultiSelectState.mock.calls[0][0] as ( + const updaterFn = setMultiSelectState.mock.calls[0]![0] as ( prev: MultiSelectState, ) => MultiSelectState; const nextState = updaterFn(makeState()); @@ -523,7 +526,7 @@ describe("useMultiSelectIntersection", () => { }); expect(setMultiSelectState).toHaveBeenCalledOnce(); - const updaterFn = setMultiSelectState.mock.calls[0][0] as ( + const updaterFn = setMultiSelectState.mock.calls[0]![0] as ( prev: MultiSelectState, ) => MultiSelectState; const nextState = updaterFn(makeState()); @@ -573,7 +576,7 @@ describe("useMultiSelectIntersection", () => { }); expect(setMultiSelectState).toHaveBeenCalledOnce(); - const updaterFn = setMultiSelectState.mock.calls[0][0] as ( + const updaterFn = setMultiSelectState.mock.calls[0]![0] as ( prev: MultiSelectState, ) => MultiSelectState; const nextState = updaterFn(makeState()); @@ -628,7 +631,7 @@ describe("useMultiSelectIntersection", () => { }); expect(setMultiSelectState).toHaveBeenCalledOnce(); - const updaterFn = setMultiSelectState.mock.calls[0][0] as ( + const updaterFn = setMultiSelectState.mock.calls[0]![0] as ( prev: MultiSelectState, ) => MultiSelectState; const nextState = updaterFn(makeState()); @@ -684,7 +687,7 @@ describe("useMultiSelectIntersection", () => { }); expect(setMultiSelectState).toHaveBeenCalledOnce(); - const updaterFn = setMultiSelectState.mock.calls[0][0] as ( + const updaterFn = setMultiSelectState.mock.calls[0]![0] as ( prev: MultiSelectState, ) => MultiSelectState; const nextState = updaterFn(makeState()); diff --git a/apps/web/src/hooks/useTimelineKeyboard.test.ts b/apps/web/src/hooks/useTimelineKeyboard.test.ts index abe7afd..94a126e 100644 --- a/apps/web/src/hooks/useTimelineKeyboard.test.ts +++ b/apps/web/src/hooks/useTimelineKeyboard.test.ts @@ -15,15 +15,18 @@ function fireKeydown(options: KeyboardEventInit) { // Helper: create a scroll container element with controllable scrollLeft // --------------------------------------------------------------------------- -function makeScrollContainer(): HTMLDivElement { - const el = document.createElement("div"); +type ScrollDiv = HTMLDivElement & { _scrollLeft: number }; + +function makeScrollContainer(): ScrollDiv { + const el = document.createElement("div") as ScrollDiv; + el._scrollLeft = 0; // jsdom does not actually scroll, but we can track assignments Object.defineProperty(el, "scrollLeft", { get() { - return this._scrollLeft ?? 0; + return (this as ScrollDiv)._scrollLeft; }, set(v: number) { - this._scrollLeft = v; + (this as ScrollDiv)._scrollLeft = v; }, configurable: true, }); @@ -53,7 +56,7 @@ function setWindowsPlatform() { // --------------------------------------------------------------------------- describe("useTimelineKeyboard", () => { - let scrollEl: HTMLDivElement; + let scrollEl: ScrollDiv; let onDeleteSelected: ReturnType; beforeEach(() => { diff --git a/apps/web/src/hooks/useViewPrefs.test.ts b/apps/web/src/hooks/useViewPrefs.test.ts index a47ff22..4b97dd7 100644 --- a/apps/web/src/hooks/useViewPrefs.test.ts +++ b/apps/web/src/hooks/useViewPrefs.test.ts @@ -111,7 +111,7 @@ describe("useViewPrefs", () => { act(() => { result.current.setSavedSort({ field: "age", dir: "asc" }); }); - const written = JSON.parse(lsStub.setItem.mock.calls[0][1] as string) as unknown; + const written = JSON.parse(lsStub.setItem.mock.calls[0]![1] as string) as unknown; expect(written).toMatchObject({ sort: { field: "age", dir: "asc" } }); }); @@ -121,7 +121,7 @@ describe("useViewPrefs", () => { act(() => { result.current.setSavedSort(null); }); - const lastCall = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]; + const lastCall = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!; const written = JSON.parse(lastCall[1] as string) as Record; expect(written).not.toHaveProperty("sort"); }); @@ -195,7 +195,7 @@ describe("useViewPrefs", () => { result.current.setRowOrder(["c", "a", "b"]); }); const written = JSON.parse( - lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1][1] as string, + lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]![1] as string, ) as Record; expect(written).toMatchObject({ rowOrder: ["c", "a", "b"] }); }); @@ -206,7 +206,7 @@ describe("useViewPrefs", () => { act(() => { result.current.setRowOrder([]); }); - const lastCall = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]; + const lastCall = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!; const written = JSON.parse(lastCall[1] as string) as Record; expect(written).not.toHaveProperty("rowOrder"); }); @@ -293,8 +293,8 @@ describe("useViewPrefs", () => { act(() => { result.current.setRowOrder(["b", "a"]); }); - const lastCall = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]; - const written = JSON.parse(lastCall[1] as string) as Record; + const lastCall1 = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!; + const written = JSON.parse(lastCall1[1] as string) as Record; expect(written).toMatchObject({ sort: { field: "name", dir: "asc" }, rowOrder: ["b", "a"], @@ -307,9 +307,9 @@ describe("useViewPrefs", () => { act(() => { result.current.setSavedSort({ field: "date", dir: "desc" }); }); - const lastCall = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]; - const written = JSON.parse(lastCall[1] as string) as Record; - expect(written).toMatchObject({ + const lastCall2 = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!; + const written2 = JSON.parse(lastCall2[1] as string) as Record; + expect(written2).toMatchObject({ sort: { field: "date", dir: "desc" }, rowOrder: ["x", "y"], }); diff --git a/apps/web/src/lib/planningEntryIds.test.ts b/apps/web/src/lib/planningEntryIds.test.ts index 3af07f8..9432ffb 100644 --- a/apps/web/src/lib/planningEntryIds.test.ts +++ b/apps/web/src/lib/planningEntryIds.test.ts @@ -48,9 +48,9 @@ describe("getPlanningEntryMutationId", () => { it("returns sourceAllocationId when entityId is undefined", () => { const result = getPlanningEntryMutationId({ id: "id-1", - entityId: undefined, + entityId: undefined as string | undefined, sourceAllocationId: "alloc-1", - }); + } as Parameters[0]); expect(result).toBe("alloc-1"); }); @@ -78,9 +78,7 @@ describe("getPlanningEntryMutationId", () => { it("returns id when both entityId and sourceAllocationId are undefined", () => { const result = getPlanningEntryMutationId({ id: "id-1", - entityId: undefined, - sourceAllocationId: undefined, - }); + } as Parameters[0]); expect(result).toBe("id-1"); });