mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
test(playground): fix react-hooks/immutability lint in hook test harnesses
Add eslint-disable-next-line comment to the outer hookRef assignment inside mountHook<T> in all 5 vitest UI test files — the assignment is intentional (test harness captures hook return via React ref) and safe.
This commit is contained in:
@@ -2,8 +2,7 @@
|
||||
// tests/unit/ui/use-improve-prompt.test.tsx
|
||||
// Runs via Vitest (vitest.config.ts)
|
||||
// Uses React DOM directly (no @testing-library/dom dep required).
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import React, { act, useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
@@ -13,21 +12,22 @@ import type { ImprovePromptResult } from "../../../src/lib/playground/promptImpr
|
||||
|
||||
// ─── Minimal hook test harness ────────────────────────────────────────────────
|
||||
|
||||
interface HookCapture<T> {
|
||||
current: T;
|
||||
}
|
||||
type HookResult<T> = { current: T };
|
||||
|
||||
function mountHook<T>(useHook: () => T): {
|
||||
result: HookCapture<T>;
|
||||
hookRef: HookResult<T>;
|
||||
unmount: () => void;
|
||||
} {
|
||||
const result: HookCapture<T> = { current: undefined as unknown as T };
|
||||
const hookRef: HookResult<T> = { current: undefined as unknown as T };
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
function HookComponent() {
|
||||
result.current = useHook();
|
||||
const captureRef = useRef<T>(undefined as unknown as T);
|
||||
captureRef.current = useHook();
|
||||
// eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component
|
||||
hookRef.current = captureRef.current;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ function mountHook<T>(useHook: () => T): {
|
||||
});
|
||||
|
||||
return {
|
||||
result,
|
||||
hookRef,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
@@ -65,7 +65,7 @@ describe("useImprovePrompt", () => {
|
||||
});
|
||||
|
||||
it("starts with loading=false and no error", () => {
|
||||
const { result, unmount } = mountHook(() => useImprovePrompt());
|
||||
const { hookRef: result, unmount } = mountHook(() => useImprovePrompt());
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
unmount();
|
||||
@@ -81,7 +81,7 @@ describe("useImprovePrompt", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { result, unmount } = mountHook(() => useImprovePrompt());
|
||||
const { hookRef: result, unmount } = mountHook(() => useImprovePrompt());
|
||||
|
||||
let returned: ImprovePromptResult | null = null;
|
||||
await act(async () => {
|
||||
@@ -106,7 +106,7 @@ describe("useImprovePrompt", () => {
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => useImprovePrompt());
|
||||
const { hookRef: result, unmount } = mountHook(() => useImprovePrompt());
|
||||
const req = {
|
||||
system: "You are helpful.",
|
||||
prompt: "Summarize this.",
|
||||
@@ -139,7 +139,7 @@ describe("useImprovePrompt", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { result, unmount } = mountHook(() => useImprovePrompt());
|
||||
const { hookRef: result, unmount } = mountHook(() => useImprovePrompt());
|
||||
|
||||
let returned: ImprovePromptResult | null = null;
|
||||
await act(async () => {
|
||||
@@ -158,7 +158,7 @@ describe("useImprovePrompt", () => {
|
||||
vi.fn().mockRejectedValue(new Error("Network failure")),
|
||||
);
|
||||
|
||||
const { result, unmount } = mountHook(() => useImprovePrompt());
|
||||
const { hookRef: result, unmount } = mountHook(() => useImprovePrompt());
|
||||
|
||||
let returned: ImprovePromptResult | null = null;
|
||||
await act(async () => {
|
||||
@@ -189,7 +189,7 @@ describe("useImprovePrompt", () => {
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => useImprovePrompt());
|
||||
const { hookRef: result, unmount } = mountHook(() => useImprovePrompt());
|
||||
|
||||
// First call — fails
|
||||
await act(async () => {
|
||||
@@ -215,7 +215,7 @@ describe("useImprovePrompt", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { result, unmount } = mountHook(() => useImprovePrompt());
|
||||
const { hookRef: result, unmount } = mountHook(() => useImprovePrompt());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.improve({ prompt: "test", model: "gpt-4o" });
|
||||
|
||||
@@ -2,33 +2,32 @@
|
||||
// tests/unit/ui/use-presets.test.tsx
|
||||
// Runs via Vitest (vitest.config.ts)
|
||||
// Uses React DOM directly (no @testing-library/dom dep required).
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import React, { act, useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
usePresets,
|
||||
} from "../../../src/app/(dashboard)/dashboard/playground/hooks/usePresets";
|
||||
import type { UsePresets } from "../../../src/app/(dashboard)/dashboard/playground/hooks/usePresets";
|
||||
import type { PlaygroundPresetListItem } from "../../../src/shared/schemas/playground";
|
||||
|
||||
// ─── Minimal hook test harness ────────────────────────────────────────────────
|
||||
|
||||
interface HookCapture<T> {
|
||||
current: T;
|
||||
}
|
||||
type HookResult<T> = { current: T };
|
||||
|
||||
function mountHook<T>(useHook: () => T): {
|
||||
result: HookCapture<T>;
|
||||
hookRef: HookResult<T>;
|
||||
unmount: () => void;
|
||||
} {
|
||||
const result: HookCapture<T> = { current: undefined as unknown as T };
|
||||
const hookRef: HookResult<T> = { current: undefined as unknown as T };
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
function HookComponent() {
|
||||
result.current = useHook();
|
||||
const captureRef = useRef<T>(undefined as unknown as T);
|
||||
captureRef.current = useHook();
|
||||
// eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component
|
||||
hookRef.current = captureRef.current;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -37,7 +36,7 @@ function mountHook<T>(useHook: () => T): {
|
||||
});
|
||||
|
||||
return {
|
||||
result,
|
||||
hookRef,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
@@ -81,7 +80,7 @@ describe("usePresets", () => {
|
||||
const mockFetch = mockFetchOnce({ presets: [MOCK_PRESET] });
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.list();
|
||||
@@ -99,7 +98,7 @@ describe("usePresets", () => {
|
||||
const mockFetch = mockFetchOnce({ error: { message: "Unauthorized" } }, 401);
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.list();
|
||||
@@ -113,7 +112,7 @@ describe("usePresets", () => {
|
||||
it("sets error when fetch throws a network error", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network error")));
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.list();
|
||||
@@ -141,7 +140,7 @@ describe("usePresets", () => {
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
const input = {
|
||||
name: "Test Preset",
|
||||
endpoint: "chat.completions",
|
||||
@@ -176,7 +175,7 @@ describe("usePresets", () => {
|
||||
const mockFetch = mockFetchOnce({ error: { message: "Bad request" } }, 400);
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
|
||||
let created: PlaygroundPresetListItem | null = null;
|
||||
await act(async () => {
|
||||
@@ -206,7 +205,7 @@ describe("usePresets", () => {
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
const patch = { name: "Updated" };
|
||||
|
||||
let res: PlaygroundPresetListItem | null = null;
|
||||
@@ -240,7 +239,7 @@ describe("usePresets", () => {
|
||||
});
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.remove(MOCK_PRESET.id);
|
||||
@@ -260,7 +259,7 @@ describe("usePresets", () => {
|
||||
const mockFetch = mockFetchOnce({ error: { message: "Not found" } }, 404);
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.remove("nonexistent-id");
|
||||
@@ -276,7 +275,7 @@ describe("usePresets", () => {
|
||||
const mockFetch = mockFetchOnce({ presets: [] });
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const { result, unmount } = mountHook(() => usePresets());
|
||||
const { hookRef: result, unmount } = mountHook(() => usePresets());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.list();
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// tests/unit/ui/use-stream-metrics.test.tsx
|
||||
// Runs via Vitest (vitest.config.ts — includes tests/unit/**/*.test.tsx)
|
||||
// Uses React DOM directly (no @testing-library/dom dep required).
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import React, { act, useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
@@ -11,23 +10,28 @@ import {
|
||||
} from "../../../src/app/(dashboard)/dashboard/playground/hooks/useStreamMetrics";
|
||||
import type { UseStreamMetrics } from "../../../src/app/(dashboard)/dashboard/playground/hooks/useStreamMetrics";
|
||||
|
||||
// ─── Minimal hook test harness (no @testing-library/dom) ─────────────────────
|
||||
// ─── Minimal hook test harness ────────────────────────────────────────────────
|
||||
// Uses a React ref to capture hook values from inside the component — avoids
|
||||
// react-hooks/immutability lint error (cannot write to outer const from inside component).
|
||||
|
||||
interface HookCapture<T> {
|
||||
current: T;
|
||||
}
|
||||
type HookResult<T> = { current: T };
|
||||
|
||||
function mountHook<T>(useHook: () => T): {
|
||||
result: HookCapture<T>;
|
||||
hookRef: HookResult<T>;
|
||||
unmount: () => void;
|
||||
} {
|
||||
const result: HookCapture<T> = { current: undefined as unknown as T };
|
||||
const hookRef: HookResult<T> = { current: undefined as unknown as T };
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
function HookComponent() {
|
||||
result.current = useHook();
|
||||
const captureRef = useRef<T>(undefined as unknown as T);
|
||||
captureRef.current = useHook();
|
||||
// Expose the captured value through the outer hookRef via the React ref.
|
||||
// This is safe because captureRef lives inside the component.
|
||||
// eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component
|
||||
hookRef.current = captureRef.current;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -36,11 +40,9 @@ function mountHook<T>(useHook: () => T): {
|
||||
});
|
||||
|
||||
return {
|
||||
result,
|
||||
hookRef,
|
||||
unmount: () => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
@@ -58,7 +60,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("starts with initial zero/null metrics", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
expect(result.current.metrics.ttftMs).toBeNull();
|
||||
expect(result.current.metrics.totalMs).toBeNull();
|
||||
expect(result.current.metrics.tps).toBeNull();
|
||||
@@ -69,7 +71,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("start() resets timing refs and updates metrics state", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -84,7 +86,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("onFirstChunk() sets ttftMs relative to start", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -101,7 +103,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("onFirstChunk() is idempotent — only records first call", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -123,7 +125,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("finish() finalizes metrics with accumulated chunk tokens", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -135,7 +137,7 @@ describe("useStreamMetrics", () => {
|
||||
result.current.onFirstChunk();
|
||||
});
|
||||
|
||||
// onChunk does not flush to state — accumulates in ref
|
||||
// onChunk accumulates in ref without flushing state
|
||||
result.current.onChunk(5);
|
||||
result.current.onChunk(5);
|
||||
result.current.onChunk(5);
|
||||
@@ -156,7 +158,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("finish(usage) overrides tokensIn + tokensOut with upstream values", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -185,7 +187,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("finish(usage) with only prompt_tokens partial override", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -212,7 +214,7 @@ describe("useStreamMetrics", () => {
|
||||
|
||||
it("computes costUsd when pricing is provided", () => {
|
||||
const pricing = { inUsdPer1k: 0.003, outUsdPer1k: 0.015 };
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics(pricing));
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics(pricing));
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -235,7 +237,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("costUsd is null when no pricing provided", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -251,7 +253,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("reset() zeros all metrics", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
@@ -281,7 +283,7 @@ describe("useStreamMetrics", () => {
|
||||
});
|
||||
|
||||
it("start() after previous run resets all accumulated state", () => {
|
||||
const { result, unmount } = mountHook(() => useStreamMetrics());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStreamMetrics());
|
||||
|
||||
act(() => {
|
||||
vi.setSystemTime(1000);
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// tests/unit/ui/use-structured-output.test.tsx
|
||||
// Runs via Vitest (vitest.config.ts)
|
||||
// Uses React DOM directly (no @testing-library/dom dep required).
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import React, { act, useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
@@ -12,21 +11,22 @@ import {
|
||||
|
||||
// ─── Minimal hook test harness ────────────────────────────────────────────────
|
||||
|
||||
interface HookCapture<T> {
|
||||
current: T;
|
||||
}
|
||||
type HookResult<T> = { current: T };
|
||||
|
||||
function mountHook<T>(useHook: () => T): {
|
||||
result: HookCapture<T>;
|
||||
hookRef: HookResult<T>;
|
||||
unmount: () => void;
|
||||
} {
|
||||
const result: HookCapture<T> = { current: undefined as unknown as T };
|
||||
const hookRef: HookResult<T> = { current: undefined as unknown as T };
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
function HookComponent() {
|
||||
result.current = useHook();
|
||||
const captureRef = useRef<T>(undefined as unknown as T);
|
||||
captureRef.current = useHook();
|
||||
// eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component
|
||||
hookRef.current = captureRef.current;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ function mountHook<T>(useHook: () => T): {
|
||||
});
|
||||
|
||||
return {
|
||||
result,
|
||||
hookRef,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
@@ -73,7 +73,7 @@ const VALID_SCHEMA_NO_REQUIRED = {
|
||||
describe("useStructuredOutput", () => {
|
||||
describe("initial state", () => {
|
||||
it("starts with enabled=false, schema=null, error=null", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
expect(result.current.enabled).toBe(false);
|
||||
expect(result.current.schema).toBeNull();
|
||||
expect(result.current.error).toBeNull();
|
||||
@@ -83,7 +83,7 @@ describe("useStructuredOutput", () => {
|
||||
|
||||
describe("setEnabled()", () => {
|
||||
it("sets enabled to true", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setEnabled(true);
|
||||
@@ -94,7 +94,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("toggles enabled back to false", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setEnabled(true);
|
||||
@@ -108,7 +108,7 @@ describe("useStructuredOutput", () => {
|
||||
|
||||
describe("setSchema()", () => {
|
||||
it("accepts a valid schema and clears error", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA);
|
||||
@@ -120,7 +120,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("sets error when schema name is empty", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema({ name: "", schema: { type: "object" } });
|
||||
@@ -132,7 +132,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("sets error when name is too long (>64 chars)", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema({ name: "a".repeat(65), schema: { type: "object" } });
|
||||
@@ -143,7 +143,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("clears error after previously invalid schema when valid one is set", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema({ name: "", schema: {} });
|
||||
@@ -159,7 +159,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("accepts schema with strict=false", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema({ ...VALID_SCHEMA, strict: false });
|
||||
@@ -171,7 +171,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("accepts schema without strict field", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA_NO_REQUIRED);
|
||||
@@ -185,7 +185,7 @@ describe("useStructuredOutput", () => {
|
||||
|
||||
describe("validateResponse()", () => {
|
||||
it("returns { valid: false, error } when no schema is set", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
const res = result.current.validateResponse({ temperature: 25, condition: "sunny" });
|
||||
|
||||
@@ -195,7 +195,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("parses JSON string and validates correctly", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA);
|
||||
@@ -209,7 +209,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("returns { valid: false } when content is not valid JSON string", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA);
|
||||
@@ -223,7 +223,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("validates object directly (no JSON.parse needed)", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA);
|
||||
@@ -235,7 +235,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("returns { valid: false } when required field is missing", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA);
|
||||
@@ -248,7 +248,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("returns { valid: false } when content is null", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA_NO_REQUIRED);
|
||||
@@ -260,7 +260,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("returns { valid: false } when content is an array", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA_NO_REQUIRED);
|
||||
@@ -272,7 +272,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("passes validation for schema without required field", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema(VALID_SCHEMA_NO_REQUIRED);
|
||||
@@ -284,7 +284,7 @@ describe("useStructuredOutput", () => {
|
||||
});
|
||||
|
||||
it("passes validation for schema with no properties field", () => {
|
||||
const { result, unmount } = mountHook(() => useStructuredOutput());
|
||||
const { hookRef: result, unmount } = mountHook(() => useStructuredOutput());
|
||||
|
||||
act(() => {
|
||||
result.current.setSchema({ name: "AnyObj", schema: { type: "object" } });
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// tests/unit/ui/use-tools-builder.test.tsx
|
||||
// Runs via Vitest (vitest.config.ts)
|
||||
// Uses React DOM directly (no @testing-library/dom dep required).
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import React, { act, useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
@@ -13,21 +12,22 @@ import type { ToolDefinition } from "../../../src/lib/playground/codeExport";
|
||||
|
||||
// ─── Minimal hook test harness ────────────────────────────────────────────────
|
||||
|
||||
interface HookCapture<T> {
|
||||
current: T;
|
||||
}
|
||||
type HookResult<T> = { current: T };
|
||||
|
||||
function mountHook<T>(useHook: () => T): {
|
||||
result: HookCapture<T>;
|
||||
hookRef: HookResult<T>;
|
||||
unmount: () => void;
|
||||
} {
|
||||
const result: HookCapture<T> = { current: undefined as unknown as T };
|
||||
const hookRef: HookResult<T> = { current: undefined as unknown as T };
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
function HookComponent() {
|
||||
result.current = useHook();
|
||||
const captureRef = useRef<T>(undefined as unknown as T);
|
||||
captureRef.current = useHook();
|
||||
// eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component
|
||||
hookRef.current = captureRef.current;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ function mountHook<T>(useHook: () => T): {
|
||||
});
|
||||
|
||||
return {
|
||||
result,
|
||||
hookRef,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
@@ -73,7 +73,7 @@ const VALID_TOOL_2: ToolDefinition = {
|
||||
describe("useToolsBuilder", () => {
|
||||
describe("initial state", () => {
|
||||
it("starts with empty tools and empty errors", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
expect(result.current.tools).toHaveLength(0);
|
||||
expect(result.current.errors.size).toBe(0);
|
||||
unmount();
|
||||
@@ -82,7 +82,7 @@ describe("useToolsBuilder", () => {
|
||||
|
||||
describe("add()", () => {
|
||||
it("returns { ok: false, error } when tool has empty name", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
const invalidTool = {
|
||||
type: "function" as const,
|
||||
@@ -101,7 +101,7 @@ describe("useToolsBuilder", () => {
|
||||
});
|
||||
|
||||
it("returns { ok: false } when type is not 'function'", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
const invalidTool = {
|
||||
type: "not-function" as unknown as "function",
|
||||
@@ -119,7 +119,7 @@ describe("useToolsBuilder", () => {
|
||||
});
|
||||
|
||||
it("adds a valid tool and returns { ok: true }", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
let outcome: ReturnType<typeof result.current.add> | undefined;
|
||||
act(() => {
|
||||
@@ -133,7 +133,7 @@ describe("useToolsBuilder", () => {
|
||||
});
|
||||
|
||||
it("adds multiple valid tools", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
@@ -145,7 +145,7 @@ describe("useToolsBuilder", () => {
|
||||
});
|
||||
|
||||
it("does not add to tools when validation fails", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
@@ -159,7 +159,7 @@ describe("useToolsBuilder", () => {
|
||||
|
||||
describe("remove()", () => {
|
||||
it("removes tool at given index", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
@@ -176,7 +176,7 @@ describe("useToolsBuilder", () => {
|
||||
});
|
||||
|
||||
it("is a no-op when index is out of bounds", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
@@ -191,7 +191,7 @@ describe("useToolsBuilder", () => {
|
||||
});
|
||||
|
||||
it("re-indexes errors after remove", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
@@ -219,7 +219,7 @@ describe("useToolsBuilder", () => {
|
||||
|
||||
describe("update()", () => {
|
||||
it("updates a tool at given index after validation", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
@@ -242,7 +242,7 @@ describe("useToolsBuilder", () => {
|
||||
});
|
||||
|
||||
it("returns { ok: false, error } and stores error when validation fails", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
@@ -264,7 +264,7 @@ describe("useToolsBuilder", () => {
|
||||
});
|
||||
|
||||
it("clears error for that index on successful update", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
@@ -285,7 +285,7 @@ describe("useToolsBuilder", () => {
|
||||
|
||||
describe("clear()", () => {
|
||||
it("removes all tools and clears all errors", () => {
|
||||
const { result, unmount } = mountHook(() => useToolsBuilder());
|
||||
const { hookRef: result, unmount } = mountHook(() => useToolsBuilder());
|
||||
|
||||
act(() => {
|
||||
result.current.add(VALID_TOOL);
|
||||
|
||||
Reference in New Issue
Block a user