refactor(web): extract ResourcesClient types + inline components, fix test TS errors
Extract types.ts, FilterDropdown.tsx, BooleanBadge.tsx from ResourcesClient.tsx into resource-client/ subdirectory. ResourcesClient reduced from 1,613 to 1,507 lines. Fix TypeScript strict mode errors across 8 test files: - Add id/order to BlueprintFieldDefinition test objects - Use FieldType enum instead of string literals in useFilters - Add non-null assertions for mock.calls array access - Type ScrollDiv for jsdom scrollLeft workaround - Fix exactOptionalPropertyTypes violations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("useLocalStorage", () => {
|
||||
});
|
||||
|
||||
it("handles object values", () => {
|
||||
const { result } = renderHook(() => useLocalStorage("obj", { a: 1 }));
|
||||
const { result } = renderHook(() => useLocalStorage<Record<string, number>>("obj", { a: 1 }));
|
||||
|
||||
act(() => {
|
||||
result.current[1]({ a: 2, b: 3 });
|
||||
|
||||
@@ -52,6 +52,9 @@ function makeState(overrides: Partial<MultiSelectState> = {}): 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());
|
||||
|
||||
@@ -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<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
const lastCall1 = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!;
|
||||
const written = JSON.parse(lastCall1[1] as string) as Record<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
expect(written2).toMatchObject({
|
||||
sort: { field: "date", dir: "desc" },
|
||||
rowOrder: ["x", "y"],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user