theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

Message.test.tsx (12009B)


      1 import { describe, it, expect, vi } from "vitest";
      2 import { render, screen, fireEvent } from "@testing-library/react";
      3 import { Message } from "./Message";
      4 import type { Message as MessageType } from "../../store/chat";
      5 
      6 const userMsg: MessageType = {
      7   id: "m1",
      8   role: "user",
      9   content: "Hello world",
     10   toolCalls: [],
     11   timestamp: Date.now(),
     12 };
     13 
     14 const assistantMsg: MessageType = {
     15   id: "m2",
     16   role: "assistant",
     17   content: "Hi there!",
     18   toolCalls: [],
     19   timestamp: Date.now(),
     20 };
     21 
     22 describe("Message", () => {
     23   it("renders user message with right alignment", () => {
     24     const { container } = render(<Message message={userMsg} />);
     25     expect(screen.getByText("Hello world")).toBeInTheDocument();
     26     expect(container.querySelector(".justify-end")).toBeInTheDocument();
     27   });
     28 
     29   it("renders assistant message with left alignment", () => {
     30     const { container } = render(<Message message={assistantMsg} />);
     31     expect(screen.getByText("Hi there!")).toBeInTheDocument();
     32     expect(container.querySelector(".justify-start")).toBeInTheDocument();
     33   });
     34 
     35   it("renders model name when provided", () => {
     36     const msg = { ...assistantMsg, model: "gpt-4" };
     37     render(<Message message={msg} />);
     38     expect(screen.getByText("gpt-4")).toBeInTheDocument();
     39   });
     40 
     41   it("does not render model name when not provided", () => {
     42     render(<Message message={assistantMsg} />);
     43     expect(screen.queryByText("gpt-4")).not.toBeInTheDocument();
     44   });
     45 
     46   it("renders user message content as paragraph", () => {
     47     render(<Message message={userMsg} />);
     48     const p = screen.getByText("Hello world");
     49     expect(p.tagName).toBe("P");
     50   });
     51 
     52   it("does not render content when content is empty", () => {
     53     const msg = { ...assistantMsg, content: "" };
     54     render(<Message message={msg} />);
     55     // Empty content should not render the content div
     56     expect(screen.queryByText("Hi there!")).not.toBeInTheDocument();
     57   });
     58 });
     59 
     60 describe("Message edit", () => {
     61   it("shows edit button on hover for user messages", () => {
     62     render(<Message message={userMsg} onEdit={vi.fn()} onRegenerate={vi.fn()} />);
     63     expect(screen.getByLabelText("Edit message")).toBeInTheDocument();
     64   });
     65 
     66   it("does not show edit button for assistant messages", () => {
     67     render(<Message message={assistantMsg} onEdit={vi.fn()} onRegenerate={vi.fn()} />);
     68     expect(screen.queryByLabelText("Edit message")).not.toBeInTheDocument();
     69   });
     70 
     71   it("clicking edit shows inline editor with current content", () => {
     72     render(<Message message={userMsg} onEdit={vi.fn()} onRegenerate={vi.fn()} />);
     73     fireEvent.click(screen.getByLabelText("Edit message"));
     74     const textarea = screen.getByDisplayValue("Hello world") as HTMLTextAreaElement;
     75     expect(textarea).toBeInTheDocument();
     76   });
     77 
     78   it("saving edit calls onEdit with new content", () => {
     79     const onEdit = vi.fn();
     80     render(<Message message={userMsg} onEdit={onEdit} onRegenerate={vi.fn()} />);
     81     fireEvent.click(screen.getByLabelText("Edit message"));
     82     const textarea = screen.getByDisplayValue("Hello world");
     83     fireEvent.change(textarea, { target: { value: "Updated message" } });
     84     fireEvent.click(screen.getByLabelText("Save edit"));
     85     expect(onEdit).toHaveBeenCalledWith("m1", "Updated message");
     86   });
     87 
     88   it("pressing Escape cancels edit", () => {
     89     render(<Message message={userMsg} onEdit={vi.fn()} onRegenerate={vi.fn()} />);
     90     fireEvent.click(screen.getByLabelText("Edit message"));
     91     fireEvent.keyDown(screen.getByDisplayValue("Hello world"), { key: "Escape" });
     92     expect(screen.getByText("Hello world")).toBeInTheDocument();
     93     expect(screen.queryByDisplayValue("Hello world")).not.toBeInTheDocument();
     94   });
     95 
     96   it("pressing Ctrl+Enter saves edit", () => {
     97     const onEdit = vi.fn();
     98     render(<Message message={userMsg} onEdit={onEdit} />);
     99     fireEvent.click(screen.getByLabelText("Edit message"));
    100     fireEvent.keyDown(screen.getByDisplayValue("Hello world"), {
    101       key: "Enter",
    102       ctrlKey: true,
    103     });
    104     expect(onEdit).toHaveBeenCalledWith("m1", "Hello world");
    105   });
    106 
    107   it("pressing Meta+Enter saves edit", () => {
    108     const onEdit = vi.fn();
    109     render(<Message message={userMsg} onEdit={onEdit} />);
    110     fireEvent.click(screen.getByLabelText("Edit message"));
    111     fireEvent.keyDown(screen.getByDisplayValue("Hello world"), {
    112       key: "Enter",
    113       metaKey: true,
    114     });
    115     expect(onEdit).toHaveBeenCalledWith("m1", "Hello world");
    116   });
    117 
    118   it("Cancel button resets content and exits edit mode", () => {
    119     render(<Message message={userMsg} onEdit={vi.fn()} />);
    120     fireEvent.click(screen.getByLabelText("Edit message"));
    121     const textarea = screen.getByDisplayValue("Hello world");
    122     fireEvent.change(textarea, { target: { value: "Changed" } });
    123     fireEvent.click(screen.getByText("Cancel"));
    124     // Should show original content again
    125     expect(screen.getByText("Hello world")).toBeInTheDocument();
    126     expect(screen.queryByDisplayValue("Changed")).not.toBeInTheDocument();
    127   });
    128 
    129   it("does not call onEdit when content is empty/whitespace", () => {
    130     const onEdit = vi.fn();
    131     render(<Message message={userMsg} onEdit={onEdit} />);
    132     fireEvent.click(screen.getByLabelText("Edit message"));
    133     const textarea = screen.getByDisplayValue("Hello world");
    134     fireEvent.change(textarea, { target: { value: "   " } });
    135     fireEvent.click(screen.getByLabelText("Save edit"));
    136     expect(onEdit).not.toHaveBeenCalled();
    137   });
    138 
    139   it("does not render edit button when onEdit is not provided", () => {
    140     render(<Message message={userMsg} />);
    141     expect(screen.queryByLabelText("Edit message")).not.toBeInTheDocument();
    142   });
    143 });
    144 
    145 describe("Message regenerate", () => {
    146   it("shows regenerate button on hover for assistant messages", () => {
    147     render(<Message message={assistantMsg} onEdit={vi.fn()} onRegenerate={vi.fn()} />);
    148     expect(screen.getByLabelText("Regenerate response")).toBeInTheDocument();
    149   });
    150 
    151   it("does not show regenerate button for user messages", () => {
    152     render(<Message message={userMsg} onEdit={vi.fn()} onRegenerate={vi.fn()} />);
    153     expect(screen.queryByLabelText("Regenerate response")).not.toBeInTheDocument();
    154   });
    155 
    156   it("clicking regenerate calls onRegenerate with message id", () => {
    157     const onRegenerate = vi.fn();
    158     render(<Message message={assistantMsg} onEdit={vi.fn()} onRegenerate={onRegenerate} />);
    159     fireEvent.click(screen.getByLabelText("Regenerate response"));
    160     expect(onRegenerate).toHaveBeenCalledWith("m2");
    161   });
    162 
    163   it("does not render regenerate button when onRegenerate is not provided", () => {
    164     render(<Message message={assistantMsg} onEdit={vi.fn()} />);
    165     expect(screen.queryByLabelText("Regenerate response")).not.toBeInTheDocument();
    166   });
    167 });
    168 
    169 describe("Message original link", () => {
    170   it("shows 'View original' link when forkId is present", () => {
    171     const editedMsg = { ...userMsg, forkId: "fork-1", originalContent: "Original text" };
    172     render(<Message message={editedMsg} onEdit={vi.fn()} />);
    173     expect(screen.getByText("View original")).toBeInTheDocument();
    174   });
    175 
    176   it("does not show 'View original' link when forkId is absent", () => {
    177     render(<Message message={userMsg} onEdit={vi.fn()} />);
    178     expect(screen.queryByText("View original")).not.toBeInTheDocument();
    179   });
    180 
    181   it("does not show 'View original' when originalContent is absent", () => {
    182     const msg = { ...userMsg, forkId: "fork-1" };
    183     render(<Message message={msg} onEdit={vi.fn()} />);
    184     expect(screen.queryByText("View original")).not.toBeInTheDocument();
    185   });
    186 
    187   it("clicking 'View original' alerts with original content", () => {
    188     const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => {});
    189     const editedMsg = { ...userMsg, forkId: "fork-1", originalContent: "Original text" };
    190     render(<Message message={editedMsg} onEdit={vi.fn()} />);
    191     fireEvent.click(screen.getByText("View original"));
    192     expect(alertSpy).toHaveBeenCalledWith("Original text");
    193     alertSpy.mockRestore();
    194   });
    195 });
    196 
    197 describe("Message tool calls", () => {
    198   it("renders tool calls when present", () => {
    199     const msgWithTools: MessageType = {
    200       ...assistantMsg,
    201       toolCalls: [
    202         { id: "tc1", name: "search", args: "{}", result: null, status: "pending" },
    203       ],
    204     };
    205     const { container } = render(<Message message={msgWithTools} />);
    206     const nameSpan = container.querySelector(".font-mono");
    207     expect(nameSpan?.textContent).toBe("search");
    208   });
    209 
    210   it("renders multiple tool calls", () => {
    211     const msgWithTools: MessageType = {
    212       ...assistantMsg,
    213       toolCalls: [
    214         { id: "tc1", name: "search", args: "{}", result: null, status: "pending" },
    215         { id: "tc2", name: "calculate", args: "{}", result: null, status: "pending" },
    216       ],
    217     };
    218     const { container } = render(<Message message={msgWithTools} />);
    219     const nameSpans = container.querySelectorAll(".font-mono");
    220     expect(nameSpans[0]?.textContent).toBe("search");
    221     expect(nameSpans[1]?.textContent).toBe("calculate");
    222   });
    223 });
    224 
    225 describe("Message styling", () => {
    226   it("user message has bg-gray-800", () => {
    227     const { container } = render(<Message message={userMsg} />);
    228     const bubble = container.querySelector(".bg-gray-800");
    229     expect(bubble).toBeInTheDocument();
    230   });
    231 
    232   it("assistant message has bg-gray-900", () => {
    233     const { container } = render(<Message message={assistantMsg} />);
    234     const bubble = container.querySelector(".bg-gray-900");
    235     expect(bubble).toBeInTheDocument();
    236   });
    237 });
    238 
    239 describe("Message image attachments", () => {
    240   it("renders image preview when message has image attachments", () => {
    241     const msgWithImage: MessageType = {
    242       ...userMsg,
    243       attachments: [
    244         { id: "att-1", filename: "photo.png", content_type: "image/png", created_at: Date.now() },
    245       ],
    246     };
    247     render(<Message message={msgWithImage} />);
    248     const img = screen.getByRole("img", { name: "photo.png" });
    249     expect(img).toBeInTheDocument();
    250     expect(img).toHaveAttribute("src", "/api/attachments/att-1");
    251   });
    252 
    253   it("wraps image preview in a link that opens in new tab", () => {
    254     const msgWithImage: MessageType = {
    255       ...userMsg,
    256       attachments: [
    257         { id: "att-1", filename: "photo.png", content_type: "image/png", created_at: Date.now() },
    258       ],
    259     };
    260     render(<Message message={msgWithImage} />);
    261     const link = screen.getByRole("link");
    262     expect(link).toHaveAttribute("href", "/api/attachments/att-1");
    263     expect(link).toHaveAttribute("target", "_blank");
    264   });
    265 
    266   it("renders multiple image previews", () => {
    267     const msgWithImages: MessageType = {
    268       ...userMsg,
    269       attachments: [
    270         { id: "att-1", filename: "a.png", content_type: "image/png", created_at: Date.now() },
    271         { id: "att-2", filename: "b.jpg", content_type: "image/jpeg", created_at: Date.now() },
    272       ],
    273     };
    274     render(<Message message={msgWithImages} />);
    275     const images = screen.getAllByRole("img");
    276     expect(images).toHaveLength(2);
    277   });
    278 
    279   it("does not render image for non-image attachments", () => {
    280     const msgWithFile: MessageType = {
    281       ...userMsg,
    282       attachments: [
    283         { id: "att-1", filename: "doc.pdf", content_type: "application/pdf", created_at: Date.now() },
    284       ],
    285     };
    286     render(<Message message={msgWithFile} />);
    287     expect(screen.queryByRole("img")).not.toBeInTheDocument();
    288   });
    289 
    290   it("does not render attachments on assistant messages", () => {
    291     const msgWithImage: MessageType = {
    292       ...assistantMsg,
    293       attachments: [
    294         { id: "att-1", filename: "photo.png", content_type: "image/png", created_at: Date.now() },
    295       ],
    296     };
    297     render(<Message message={msgWithImage} />);
    298     expect(screen.queryByRole("img")).not.toBeInTheDocument();
    299   });
    300 
    301   it("renders no attachment elements when attachments is empty", () => {
    302     render(<Message message={userMsg} />);
    303     expect(screen.queryByRole("img")).not.toBeInTheDocument();
    304     expect(screen.queryByRole("link")).not.toBeInTheDocument();
    305   });
    306 });