TypeScript · 3389 bytes Raw Blame History
1 import { describe, expect, it, vi } from "vitest";
2 import { render, screen } from "@testing-library/react";
3
4 import { MessageTimeline } from "@/components/MessageTimeline";
5 import type { Message } from "@/lib/ipc/types";
6
7 // Virtuoso measures layout with IntersectionObserver + ResizeObserver.
8 // In jsdom neither exists, so items never render. Replace with a plain
9 // mapping for test purposes — we're asserting card content, not
10 // virtualization behavior.
11 vi.mock("react-virtuoso", () => ({
12 Virtuoso: ({
13 data,
14 itemContent,
15 }: {
16 data: Message[];
17 itemContent: (i: number, item: Message) => React.ReactNode;
18 }) => (
19 <div>
20 {data.map((item, i) => (
21 <div key={i}>{itemContent(i, item)}</div>
22 ))}
23 </div>
24 ),
25 }));
26
27 const fixture: Message[] = [
28 {
29 kind: "user",
30 id: "u1",
31 at: "2026-04-11T00:55:35.000Z",
32 text: "plan the thread browser feature",
33 isMeta: false,
34 },
35 {
36 kind: "assistant",
37 id: "a1",
38 at: "2026-04-11T00:55:40.000Z",
39 model: "claude-opus-4-6",
40 blocks: [
41 { type: "text", text: "Here's the plan." },
42 { type: "thinking", text: "let me think about this" },
43 {
44 type: "tool_use",
45 id: "tu_1",
46 name: "Read",
47 input: { file_path: "/tmp/example.ts" },
48 },
49 ],
50 stopReason: "end_turn",
51 usage: {
52 input_tokens: 100,
53 output_tokens: 250,
54 cache_creation_input_tokens: 0,
55 cache_read_input_tokens: 0,
56 },
57 },
58 {
59 kind: "system",
60 id: "s1",
61 at: "2026-04-11T00:56:00.000Z",
62 text: "stop hook summary",
63 subtype: "stop_hook_summary",
64 },
65 {
66 kind: "attachment",
67 id: "at1",
68 at: "2026-04-11T00:56:05.000Z",
69 attachmentType: "hook_success",
70 hookName: "SessionStart:startup",
71 text: '{"continue":true}',
72 },
73 {
74 kind: "unknown",
75 id: "x1",
76 at: "2026-04-11T00:56:10.000Z",
77 rawType: "brand-new-event-kind",
78 raw: { foo: "bar" },
79 },
80 ];
81
82 describe("MessageTimeline", () => {
83 it("renders empty state", () => {
84 render(<MessageTimeline messages={[]} />);
85 expect(
86 screen.getByText(/no renderable messages/i),
87 ).toBeInTheDocument();
88 });
89
90 it("renders one card per message kind", () => {
91 render(<MessageTimeline messages={fixture} />);
92
93 // User
94 expect(
95 screen.getByText(/plan the thread browser feature/i),
96 ).toBeInTheDocument();
97 expect(screen.getByText(/you/i)).toBeInTheDocument();
98
99 // Assistant — model badge (opus-4-6 after "claude-" stripped)
100 expect(screen.getByText(/opus-4-6/i)).toBeInTheDocument();
101 expect(screen.getByText(/claude/i)).toBeInTheDocument();
102 expect(screen.getByText(/here's the plan/i)).toBeInTheDocument();
103
104 // Tool use — compact card with tool name
105 expect(screen.getByText(/^Read$/)).toBeInTheDocument();
106
107 // System
108 expect(screen.getByText(/stop_hook_summary/i)).toBeInTheDocument();
109
110 // Attachment
111 expect(screen.getByText(/hook_success/i)).toBeInTheDocument();
112
113 // Unknown fallback
114 expect(
115 screen.getByText(/unknown: brand-new-event-kind/i),
116 ).toBeInTheDocument();
117 });
118
119 it("hides thinking block body until expanded", () => {
120 render(<MessageTimeline messages={fixture} />);
121 // Thinking body is not rendered by default.
122 expect(
123 screen.queryByText(/let me think about this/i),
124 ).not.toBeInTheDocument();
125 });
126 });