Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 35 additions & 1 deletion frontend/src/components/chat/MessageBubble.test.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -52,4 +53,37 @@
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(<MessageBubble message={makeMessage({ content })} />);

fireEvent.click(screen.getByLabelText("Copy response"));

expect(navigator.clipboard.writeText).toHaveBeenCalledWith(content);
expect(screen.getByLabelText("Copied")).toBeInTheDocument();

Check failure on line 64 in frontend/src/components/chat/MessageBubble.test.tsx

View workflow job for this annotation

GitHub Actions / βš›οΈ Frontend β€” TypeScript & Build

src/components/chat/MessageBubble.test.tsx > MessageBubble > copies assistant message content to clipboard when copy button is clicked

TestingLibraryElementError: Unable to find a label with the text of: Copied Ignored nodes: comments, script, style <body> <div> <div class="flex gap-3 py-3 animate-fade-in-up justify-start" > <div class="w-8 h-8 rounded-lg bg-primary/15 flex items-center justify-center shrink-0 mt-0.5" > <svg aria-hidden="true" class="lucide lucide-brain w-4 h-4 text-primary" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > <path d="M12 18V5" /> <path d="M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4" /> <path d="M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5" /> <path d="M17.997 5.125a4 4 0 0 1 2.526 5.77" /> <path d="M18 18a4 4 0 0 0 2-7.464" /> <path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517" /> <path d="M6 18a4 4 0 0 1-2-7.464" /> <path d="M6.003 5.125a4 4 0 0 0-2.526 5.77" /> </svg> </div> <div class="relative max-w-[80%] rounded-xl px-4 py-3 group bg-card border border-border/50 rounded-bl-sm" > <button aria-label="Share response" class="group/button inline-flex shrink-0 items-center justify-center border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 hover:bg-muted aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3 absolute top-2 right-2 text-muted-foreground hover:text-foreground transition-opacity opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto" data-slot="tooltip-trigger" id="base-ui-_r_9_" type="button" > <svg aria-hidden="true" class="lucide lucide-share2 lucide-share-2 w-3.5 h-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > <circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /> </svg> </button> <button aria-label="Copy response" class="group/button inline-flex shrink-0 items-center justify-center border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid
});

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(
<MessageBubble
message={makeMessage({ content: "Share this content" })}
/>,
);

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",
);
});
});
8 changes: 8 additions & 0 deletions frontend/src/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
Loading