huskies: merge 622_story_wrap_react_state_updates_in_act_to_silence_frontend_test_warnings
This commit is contained in:
@@ -1,8 +1,14 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { api } from "./api/client";
|
import { api } from "./api/client";
|
||||||
|
|
||||||
|
vi.mock("./api/gateway", () => ({
|
||||||
|
gatewayApi: {
|
||||||
|
getServerMode: vi.fn().mockResolvedValue({ mode: "standard" }),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("./api/client", () => {
|
vi.mock("./api/client", () => {
|
||||||
const api = {
|
const api = {
|
||||||
getCurrentProject: vi.fn(),
|
getCurrentProject: vi.fn(),
|
||||||
@@ -76,7 +82,11 @@ describe("App", () => {
|
|||||||
|
|
||||||
async function renderApp() {
|
async function renderApp() {
|
||||||
const { default: App } = await import("./App");
|
const { default: App } = await import("./App");
|
||||||
return render(<App />);
|
let result!: ReturnType<typeof render>;
|
||||||
|
await act(async () => {
|
||||||
|
result = render(<App />);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
it("calls getCurrentProject() on mount", async () => {
|
it("calls getCurrentProject() on mount", async () => {
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ describe("Chat message rendering — unified tool call UI", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate(messages);
|
capturedWsHandlers?.onUpdate(messages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ describe("Chat message rendering — unified tool call UI", () => {
|
|||||||
{ role: "assistant", content: "The file contains a main function." },
|
{ role: "assistant", content: "The file contains a main function." },
|
||||||
];
|
];
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate(messages);
|
capturedWsHandlers?.onUpdate(messages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ describe("Chat message rendering — unified tool call UI", () => {
|
|||||||
{ role: "assistant", content: "Hi there! How can I help?" },
|
{ role: "assistant", content: "Hi there! How can I help?" },
|
||||||
];
|
];
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate(messages);
|
capturedWsHandlers?.onUpdate(messages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ describe("Chat message rendering — unified tool call UI", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate(messages);
|
capturedWsHandlers?.onUpdate(messages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ describe("Chat reconciliation banner", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onReconciliationProgress(
|
capturedWsHandlers?.onReconciliationProgress(
|
||||||
"42_story_test",
|
"42_story_test",
|
||||||
"checking",
|
"checking",
|
||||||
@@ -417,7 +417,7 @@ describe("Chat reconciliation banner", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onReconciliationProgress(
|
capturedWsHandlers?.onReconciliationProgress(
|
||||||
"42_story_test",
|
"42_story_test",
|
||||||
"gates_running",
|
"gates_running",
|
||||||
@@ -435,7 +435,7 @@ describe("Chat reconciliation banner", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onReconciliationProgress(
|
capturedWsHandlers?.onReconciliationProgress(
|
||||||
"42_story_test",
|
"42_story_test",
|
||||||
"checking",
|
"checking",
|
||||||
@@ -447,7 +447,7 @@ describe("Chat reconciliation banner", () => {
|
|||||||
await screen.findByTestId("reconciliation-banner"),
|
await screen.findByTestId("reconciliation-banner"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onReconciliationProgress(
|
capturedWsHandlers?.onReconciliationProgress(
|
||||||
"",
|
"",
|
||||||
"done",
|
"done",
|
||||||
@@ -504,7 +504,7 @@ describe("Chat localStorage persistence (Story 145)", () => {
|
|||||||
{ role: "assistant", content: "Hi there!" },
|
{ role: "assistant", content: "Hi there!" },
|
||||||
];
|
];
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate(history);
|
capturedWsHandlers?.onUpdate(history);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -555,7 +555,7 @@ describe("Chat localStorage persistence (Story 145)", () => {
|
|||||||
{ role: "assistant", content: "I should survive a reload" },
|
{ role: "assistant", content: "I should survive a reload" },
|
||||||
];
|
];
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate(history);
|
capturedWsHandlers?.onUpdate(history);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -604,7 +604,7 @@ describe("Chat localStorage persistence (Story 145)", () => {
|
|||||||
{ role: "user", content: "What is Rust?" },
|
{ role: "user", content: "What is Rust?" },
|
||||||
{ role: "assistant", content: "Rust is a systems programming language." },
|
{ role: "assistant", content: "Rust is a systems programming language." },
|
||||||
];
|
];
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate(priorHistory);
|
capturedWsHandlers?.onUpdate(priorHistory);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -692,12 +692,12 @@ describe("Chat activity status indicator (Bug 140)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Simulate tokens arriving (streamingContent becomes non-empty)
|
// Simulate tokens arriving (streamingContent becomes non-empty)
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onToken("I'll read that file for you.");
|
capturedWsHandlers?.onToken("I'll read that file for you.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now simulate a tool activity event while streamingContent is non-empty
|
// Now simulate a tool activity event while streamingContent is non-empty
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onActivity("read_file");
|
capturedWsHandlers?.onActivity("read_file");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -742,7 +742,7 @@ describe("Chat activity status indicator (Bug 140)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Tokens arrive — streamingContent is non-empty, no activity
|
// Tokens arrive — streamingContent is non-empty, no activity
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onToken("Here is my response...");
|
capturedWsHandlers?.onToken("Here is my response...");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -765,12 +765,12 @@ describe("Chat activity status indicator (Bug 140)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Simulate tokens arriving
|
// Simulate tokens arriving
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onToken("Let me read that.");
|
capturedWsHandlers?.onToken("Let me read that.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Claude Code sends tool name "Read" (not "read_file")
|
// Claude Code sends tool name "Read" (not "read_file")
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onActivity("Read");
|
capturedWsHandlers?.onActivity("Read");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -792,11 +792,11 @@ describe("Chat activity status indicator (Bug 140)", () => {
|
|||||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onToken("Running tests now.");
|
capturedWsHandlers?.onToken("Running tests now.");
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onActivity("Bash");
|
capturedWsHandlers?.onActivity("Bash");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -818,11 +818,11 @@ describe("Chat activity status indicator (Bug 140)", () => {
|
|||||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onToken("Working on it.");
|
capturedWsHandlers?.onToken("Working on it.");
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onActivity("SomeCustomTool");
|
capturedWsHandlers?.onActivity("SomeCustomTool");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -899,7 +899,7 @@ describe("Chat message queue (Story 155)", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
// Simulate agent response completing (loading → false)
|
// Simulate agent response completing (loading → false)
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate([
|
capturedWsHandlers?.onUpdate([
|
||||||
{ role: "user", content: "First" },
|
{ role: "user", content: "First" },
|
||||||
{ role: "assistant", content: "Done." },
|
{ role: "assistant", content: "Done." },
|
||||||
@@ -1066,7 +1066,7 @@ describe("Chat message queue (Story 155)", () => {
|
|||||||
expect(indicators[1]).toHaveTextContent("Third");
|
expect(indicators[1]).toHaveTextContent("Third");
|
||||||
|
|
||||||
// Simulate first response completing — both "Second" and "Third" are drained at once
|
// Simulate first response completing — both "Second" and "Third" are drained at once
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate([
|
capturedWsHandlers?.onUpdate([
|
||||||
{ role: "user", content: "First" },
|
{ role: "user", content: "First" },
|
||||||
{ role: "assistant", content: "Response 1." },
|
{ role: "assistant", content: "Response 1." },
|
||||||
@@ -1145,7 +1145,7 @@ describe("Remove bubble styling from streaming messages (Story 163)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Simulate streaming tokens arriving
|
// Simulate streaming tokens arriving
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onToken("Streaming response text");
|
capturedWsHandlers?.onToken("Streaming response text");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1176,7 +1176,7 @@ describe("Remove bubble styling from streaming messages (Story 163)", () => {
|
|||||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onToken("Some markdown content");
|
capturedWsHandlers?.onToken("Some markdown content");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1200,7 +1200,7 @@ describe("Remove bubble styling from streaming messages (Story 163)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Simulate streaming tokens
|
// Simulate streaming tokens
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onToken("Final response");
|
capturedWsHandlers?.onToken("Final response");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1211,7 +1211,7 @@ describe("Remove bubble styling from streaming messages (Story 163)", () => {
|
|||||||
const streamingStyleAttr = streamingStyledDiv.getAttribute("style") ?? "";
|
const streamingStyleAttr = streamingStyledDiv.getAttribute("style") ?? "";
|
||||||
|
|
||||||
// Transition: onUpdate completes the message
|
// Transition: onUpdate completes the message
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate([
|
capturedWsHandlers?.onUpdate([
|
||||||
{ role: "user", content: "Hello" },
|
{ role: "user", content: "Hello" },
|
||||||
{ role: "assistant", content: "Final response" },
|
{ role: "assistant", content: "Final response" },
|
||||||
@@ -1244,7 +1244,7 @@ describe("Remove bubble styling from streaming messages (Story 163)", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate([
|
capturedWsHandlers?.onUpdate([
|
||||||
{ role: "user", content: "Hi" },
|
{ role: "user", content: "Hi" },
|
||||||
{ role: "assistant", content: "Hello there!" },
|
{ role: "assistant", content: "Hello there!" },
|
||||||
@@ -1268,7 +1268,7 @@ describe("Remove bubble styling from streaming messages (Story 163)", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate([
|
capturedWsHandlers?.onUpdate([
|
||||||
{ role: "user", content: "I am a user message" },
|
{ role: "user", content: "I am a user message" },
|
||||||
{ role: "assistant", content: "I am a response" },
|
{ role: "assistant", content: "I am a response" },
|
||||||
@@ -1310,7 +1310,7 @@ describe("Bug 264: Claude Code session ID persisted across browser refresh", ()
|
|||||||
|
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onSessionId("test-session-abc");
|
capturedWsHandlers?.onSessionId("test-session-abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1394,7 +1394,7 @@ describe("Bug 264: Claude Code session ID persisted across browser refresh", ()
|
|||||||
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
|
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onSessionId("my-session");
|
capturedWsHandlers?.onSessionId("my-session");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1595,7 +1595,7 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
// First add a message so there is history to clear
|
// First add a message so there is history to clear
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onUpdate([
|
capturedWsHandlers?.onUpdate([
|
||||||
{ role: "user", content: "hello" },
|
{ role: "user", content: "hello" },
|
||||||
{ role: "assistant", content: "world" },
|
{ role: "assistant", content: "world" },
|
||||||
@@ -1701,7 +1701,7 @@ describe("Bug 450: WebSocket error messages displayed in chat", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onError("Something went wrong on the server.");
|
capturedWsHandlers?.onError("Something went wrong on the server.");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1715,7 +1715,7 @@ describe("Bug 450: WebSocket error messages displayed in chat", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
capturedWsHandlers?.onError(
|
capturedWsHandlers?.onError(
|
||||||
"OAuth login required. Please visit: https://example.com/oauth/login",
|
"OAuth login required. Please visit: https://example.com/oauth/login",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ describe("usePathCompletion hook", () => {
|
|||||||
expect(result.current.matchList[0].name).toBe("Documents");
|
expect(result.current.matchList[0].name).toBe("Documents");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls setPathInput when acceptMatch is invoked", () => {
|
it("calls setPathInput when acceptMatch is invoked", async () => {
|
||||||
const setPathInput = vi.fn();
|
const setPathInput = vi.fn();
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@@ -151,7 +151,7 @@ describe("usePathCompletion hook", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.acceptMatch("/home/user/Documents/");
|
result.current.acceptMatch("/home/user/Documents/");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -308,14 +308,14 @@ describe("usePathCompletion hook", () => {
|
|||||||
expect(result.current.matchList.length).toBe(2);
|
expect(result.current.matchList.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.acceptSelectedMatch();
|
result.current.acceptSelectedMatch();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(setPathInput).toHaveBeenCalledWith("/home/user/Documents/");
|
expect(setPathInput).toHaveBeenCalledWith("/home/user/Documents/");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("acceptSelectedMatch does nothing when matchList is empty", () => {
|
it("acceptSelectedMatch does nothing when matchList is empty", async () => {
|
||||||
const setPathInput = vi.fn();
|
const setPathInput = vi.fn();
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@@ -328,7 +328,7 @@ describe("usePathCompletion hook", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.acceptSelectedMatch();
|
result.current.acceptSelectedMatch();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ describe("usePathCompletion hook", () => {
|
|||||||
expect(result.current.matchList.length).toBe(1);
|
expect(result.current.matchList.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.closeSuggestions();
|
result.current.closeSuggestions();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -450,7 +450,7 @@ describe("usePathCompletion hook", () => {
|
|||||||
expect(result.current.matchList.length).toBe(2);
|
expect(result.current.matchList.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setSelectedMatch(1);
|
result.current.setSelectedMatch(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function makeMessages(count: number): Message[] {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("useChatHistory", () => {
|
describe("useChatHistory", async () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
@@ -28,7 +28,7 @@ describe("useChatHistory", () => {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC1: restores messages from localStorage on mount", () => {
|
it("AC1: restores messages from localStorage on mount", async () => {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(sampleMessages));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(sampleMessages));
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
@@ -36,13 +36,13 @@ describe("useChatHistory", () => {
|
|||||||
expect(result.current.messages).toEqual(sampleMessages);
|
expect(result.current.messages).toEqual(sampleMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC1: returns empty array when localStorage has no data", () => {
|
it("AC1: returns empty array when localStorage has no data", async () => {
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|
||||||
expect(result.current.messages).toEqual([]);
|
expect(result.current.messages).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC1: returns empty array when localStorage contains invalid JSON", () => {
|
it("AC1: returns empty array when localStorage contains invalid JSON", async () => {
|
||||||
localStorage.setItem(STORAGE_KEY, "not-json{{{");
|
localStorage.setItem(STORAGE_KEY, "not-json{{{");
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
@@ -50,7 +50,7 @@ describe("useChatHistory", () => {
|
|||||||
expect(result.current.messages).toEqual([]);
|
expect(result.current.messages).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC1: returns empty array when localStorage contains a non-array", () => {
|
it("AC1: returns empty array when localStorage contains a non-array", async () => {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ not: "array" }));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ not: "array" }));
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
@@ -58,10 +58,10 @@ describe("useChatHistory", () => {
|
|||||||
expect(result.current.messages).toEqual([]);
|
expect(result.current.messages).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC2: saves messages to localStorage when setMessages is called with an array", () => {
|
it("AC2: saves messages to localStorage when setMessages is called with an array", async () => {
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMessages(sampleMessages);
|
result.current.setMessages(sampleMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,10 +69,10 @@ describe("useChatHistory", () => {
|
|||||||
expect(stored).toEqual(sampleMessages);
|
expect(stored).toEqual(sampleMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC2: saves messages to localStorage when setMessages is called with updater function", () => {
|
it("AC2: saves messages to localStorage when setMessages is called with updater function", async () => {
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMessages(() => sampleMessages);
|
result.current.setMessages(() => sampleMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,14 +80,14 @@ describe("useChatHistory", () => {
|
|||||||
expect(stored).toEqual(sampleMessages);
|
expect(stored).toEqual(sampleMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC3: clearMessages removes messages from state and localStorage", () => {
|
it("AC3: clearMessages removes messages from state and localStorage", async () => {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(sampleMessages));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(sampleMessages));
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|
||||||
expect(result.current.messages).toEqual(sampleMessages);
|
expect(result.current.messages).toEqual(sampleMessages);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.clearMessages();
|
result.current.clearMessages();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ describe("useChatHistory", () => {
|
|||||||
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC4: handles localStorage quota errors gracefully", () => {
|
it("AC4: handles localStorage quota errors gracefully", async () => {
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
const setItemSpy = vi
|
const setItemSpy = vi
|
||||||
.spyOn(Storage.prototype, "setItem")
|
.spyOn(Storage.prototype, "setItem")
|
||||||
@@ -106,7 +106,7 @@ describe("useChatHistory", () => {
|
|||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|
||||||
// Should not throw
|
// Should not throw
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMessages(sampleMessages);
|
result.current.setMessages(sampleMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ describe("useChatHistory", () => {
|
|||||||
setItemSpy.mockRestore();
|
setItemSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC5: scopes storage key to project path", () => {
|
it("AC5: scopes storage key to project path", async () => {
|
||||||
const projectA = "/projects/a";
|
const projectA = "/projects/a";
|
||||||
const projectB = "/projects/b";
|
const projectB = "/projects/b";
|
||||||
const keyA = `storykit-chat-history:${projectA}`;
|
const keyA = `storykit-chat-history:${projectA}`;
|
||||||
@@ -140,12 +140,12 @@ describe("useChatHistory", () => {
|
|||||||
expect(resultB.current.messages).toEqual(messagesB);
|
expect(resultB.current.messages).toEqual(messagesB);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC2: removes localStorage key when messages are set to empty array", () => {
|
it("AC2: removes localStorage key when messages are set to empty array", async () => {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(sampleMessages));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(sampleMessages));
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMessages([]);
|
result.current.setMessages([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,20 +154,20 @@ describe("useChatHistory", () => {
|
|||||||
|
|
||||||
// --- Story 179: Chat history pruning tests ---
|
// --- Story 179: Chat history pruning tests ---
|
||||||
|
|
||||||
it("S179: default limit of 200 is applied when saving to localStorage", () => {
|
it("S179: default limit of 200 is applied when saving to localStorage", async () => {
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|
||||||
expect(result.current.maxMessages).toBe(200);
|
expect(result.current.maxMessages).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("S179: messages are pruned from the front when exceeding the limit", () => {
|
it("S179: messages are pruned from the front when exceeding the limit", async () => {
|
||||||
// Set a small limit to make testing practical
|
// Set a small limit to make testing practical
|
||||||
localStorage.setItem(LIMIT_KEY, "3");
|
localStorage.setItem(LIMIT_KEY, "3");
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
const fiveMessages = makeMessages(5);
|
const fiveMessages = makeMessages(5);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMessages(fiveMessages);
|
result.current.setMessages(fiveMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,13 +180,13 @@ describe("useChatHistory", () => {
|
|||||||
expect(stored[0].content).toBe("Message 3");
|
expect(stored[0].content).toBe("Message 3");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("S179: messages under the limit are not pruned", () => {
|
it("S179: messages under the limit are not pruned", async () => {
|
||||||
localStorage.setItem(LIMIT_KEY, "10");
|
localStorage.setItem(LIMIT_KEY, "10");
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
const threeMessages = makeMessages(3);
|
const threeMessages = makeMessages(3);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMessages(threeMessages);
|
result.current.setMessages(threeMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ describe("useChatHistory", () => {
|
|||||||
expect(stored).toHaveLength(3);
|
expect(stored).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("S179: limit is configurable via localStorage key", () => {
|
it("S179: limit is configurable via localStorage key", async () => {
|
||||||
localStorage.setItem(LIMIT_KEY, "5");
|
localStorage.setItem(LIMIT_KEY, "5");
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
@@ -205,10 +205,10 @@ describe("useChatHistory", () => {
|
|||||||
expect(result.current.maxMessages).toBe(5);
|
expect(result.current.maxMessages).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("S179: setMaxMessages updates the limit and persists it", () => {
|
it("S179: setMaxMessages updates the limit and persists it", async () => {
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMaxMessages(50);
|
result.current.setMaxMessages(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,13 +216,13 @@ describe("useChatHistory", () => {
|
|||||||
expect(localStorage.getItem(LIMIT_KEY)).toBe("50");
|
expect(localStorage.getItem(LIMIT_KEY)).toBe("50");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("S179: a limit of 0 means unlimited (no pruning)", () => {
|
it("S179: a limit of 0 means unlimited (no pruning)", async () => {
|
||||||
localStorage.setItem(LIMIT_KEY, "0");
|
localStorage.setItem(LIMIT_KEY, "0");
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
const manyMessages = makeMessages(500);
|
const manyMessages = makeMessages(500);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMessages(manyMessages);
|
result.current.setMessages(manyMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,11 +233,11 @@ describe("useChatHistory", () => {
|
|||||||
expect(stored).toEqual(manyMessages);
|
expect(stored).toEqual(manyMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("S179: changing the limit re-prunes messages on next save", () => {
|
it("S179: changing the limit re-prunes messages on next save", async () => {
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
const tenMessages = makeMessages(10);
|
const tenMessages = makeMessages(10);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMessages(tenMessages);
|
result.current.setMessages(tenMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ describe("useChatHistory", () => {
|
|||||||
expect(stored).toHaveLength(10);
|
expect(stored).toHaveLength(10);
|
||||||
|
|
||||||
// Now lower the limit — the effect re-runs and prunes
|
// Now lower the limit — the effect re-runs and prunes
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.setMaxMessages(3);
|
result.current.setMaxMessages(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ describe("useChatHistory", () => {
|
|||||||
expect(stored[0].content).toBe("Message 8");
|
expect(stored[0].content).toBe("Message 8");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("S179: invalid limit in localStorage falls back to default", () => {
|
it("S179: invalid limit in localStorage falls back to default", async () => {
|
||||||
localStorage.setItem(LIMIT_KEY, "not-a-number");
|
localStorage.setItem(LIMIT_KEY, "not-a-number");
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
@@ -265,7 +265,7 @@ describe("useChatHistory", () => {
|
|||||||
expect(result.current.maxMessages).toBe(200);
|
expect(result.current.maxMessages).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("S179: negative limit in localStorage falls back to default", () => {
|
it("S179: negative limit in localStorage falls back to default", async () => {
|
||||||
localStorage.setItem(LIMIT_KEY, "-5");
|
localStorage.setItem(LIMIT_KEY, "-5");
|
||||||
|
|
||||||
const { result } = renderHook(() => useChatHistory(PROJECT));
|
const { result } = renderHook(() => useChatHistory(PROJECT));
|
||||||
|
|||||||
Reference in New Issue
Block a user