diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 74838149..bd4a95ec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -233,6 +233,10 @@ jobs:
working-directory: frontend
run: npm run lint
+ - name: Frontend unit tests (Vitest)
+ working-directory: frontend
+ run: npm test
+
- name: Next.js build (production bundle check)
working-directory: frontend
run: npm run build
diff --git a/frontend/src/components/chat/MessageBubble.test.tsx b/frontend/src/components/chat/MessageBubble.test.tsx
index f48acd9e..fd671a9f 100644
--- a/frontend/src/components/chat/MessageBubble.test.tsx
+++ b/frontend/src/components/chat/MessageBubble.test.tsx
@@ -1,7 +1,8 @@
-import { render, screen } from "@testing-library/react";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import MessageBubble from "./MessageBubble";
import type { ChatMsg } from "@/store/chat-store";
+import { api } from "@/lib/api";
vi.mock("@/lib/api", () => ({
api: {
@@ -52,4 +53,37 @@ describe("MessageBubble", () => {
expect(screen.getByLabelText("Copy response")).toBeInTheDocument();
expect(screen.getByLabelText("Share response")).toBeInTheDocument();
});
+
+ it("copies assistant message content to clipboard when copy button is clicked", () => {
+ const content = "This is some assistant response text";
+ render();
+
+ fireEvent.click(screen.getByLabelText("Copy response"));
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(content);
+ expect(screen.getByLabelText("Copied")).toBeInTheDocument();
+ });
+
+ it("shares assistant message via API and copies share link to clipboard", async () => {
+ vi.mocked(api.post).mockResolvedValueOnce({
+ message_id: "msg-1",
+ share_url: "/shared/abc-123",
+ });
+
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByLabelText("Share response"));
+
+ await waitFor(() => {
+ expect(api.post).toHaveBeenCalledWith("/api/v1/chat/share/msg-1");
+ });
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
+ "http://localhost:3000/shared/abc-123",
+ );
+ });
});
diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts
index 51e7ef37..4309d5f5 100644
--- a/frontend/src/test/setup.ts
+++ b/frontend/src/test/setup.ts
@@ -7,6 +7,14 @@ afterEach(() => {
vi.clearAllMocks();
});
+Object.defineProperty(navigator, "clipboard", {
+ writable: true,
+ value: {
+ writeText: vi.fn(),
+ readText: vi.fn(),
+ },
+});
+
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({