blob: 3f306966bdd47021b846e60a289b19ab64894435 [file] [log] [blame]
Sean McCullough86b56862025-04-18 13:04:03 -07001import { html, fixture, expect, oneEvent } from "@open-wc/testing";
2import "./sketch-timeline-message";
3import type { SketchTimelineMessage } from "./sketch-timeline-message";
4import { TimelineMessage, ToolCall, GitCommit, Usage } from "../types";
5
6describe("SketchTimelineMessage", () => {
7 // Helper function to create mock timeline messages
8 function createMockMessage(props: Partial<TimelineMessage> = {}): TimelineMessage {
9 return {
10 idx: props.idx || 0,
11 type: props.type || "agent",
12 content: props.content || "Hello world",
13 timestamp: props.timestamp || "2023-05-15T12:00:00Z",
14 elapsed: props.elapsed || 1500000000, // 1.5 seconds in nanoseconds
15 end_of_turn: props.end_of_turn || false,
16 conversation_id: props.conversation_id || "conv123",
17 tool_calls: props.tool_calls || [],
18 commits: props.commits || [],
19 usage: props.usage,
20 ...props
21 };
22 }
23
24 it("renders with basic message content", async () => {
25 const message = createMockMessage({
26 type: "agent",
27 content: "This is a test message"
28 });
29
30 const el: SketchTimelineMessage = await fixture(html`
31 <sketch-timeline-message
32 .message=${message}
33 ></sketch-timeline-message>
34 `);
35
36 const messageContent = el.shadowRoot!.querySelector(".message-text");
37 expect(messageContent).to.exist;
38 expect(messageContent!.textContent!.trim()).to.include("This is a test message");
39 });
40
41 it("renders with correct message type classes", async () => {
42 const messageTypes = ["user", "agent", "tool", "error"];
43
44 for (const type of messageTypes) {
45 const message = createMockMessage({ type });
46
47 const el: SketchTimelineMessage = await fixture(html`
48 <sketch-timeline-message
49 .message=${message}
50 ></sketch-timeline-message>
51 `);
52
53 const messageElement = el.shadowRoot!.querySelector(".message");
54 expect(messageElement).to.exist;
55 expect(messageElement!.classList.contains(type)).to.be.true;
56 }
57 });
58
59 it("renders end-of-turn marker correctly", async () => {
60 const message = createMockMessage({
61 end_of_turn: true
62 });
63
64 const el: SketchTimelineMessage = await fixture(html`
65 <sketch-timeline-message
66 .message=${message}
67 ></sketch-timeline-message>
68 `);
69
70 const messageElement = el.shadowRoot!.querySelector(".message");
71 expect(messageElement).to.exist;
72 expect(messageElement!.classList.contains("end-of-turn")).to.be.true;
73 });
74
75 it("formats timestamps correctly", async () => {
76 const message = createMockMessage({
77 timestamp: "2023-05-15T12:00:00Z"
78 });
79
80 const el: SketchTimelineMessage = await fixture(html`
81 <sketch-timeline-message
82 .message=${message}
83 ></sketch-timeline-message>
84 `);
85
86 const timestamp = el.shadowRoot!.querySelector(".message-timestamp");
87 expect(timestamp).to.exist;
88 // Should include a formatted date like "May 15, 2023"
89 expect(timestamp!.textContent).to.include("May 15, 2023");
90 // Should include elapsed time
91 expect(timestamp!.textContent).to.include("(1.50s)");
92 });
93
94 it("renders markdown content correctly", async () => {
95 const markdownContent = "# Heading\n\n- List item 1\n- List item 2\n\n`code block`";
96 const message = createMockMessage({
97 content: markdownContent
98 });
99
100 const el: SketchTimelineMessage = await fixture(html`
101 <sketch-timeline-message
102 .message=${message}
103 ></sketch-timeline-message>
104 `);
105
106 const contentElement = el.shadowRoot!.querySelector(".markdown-content");
107 expect(contentElement).to.exist;
108 expect(contentElement!.innerHTML).to.include("<h1>Heading</h1>");
109 expect(contentElement!.innerHTML).to.include("<ul>");
110 expect(contentElement!.innerHTML).to.include("<li>List item 1</li>");
111 expect(contentElement!.innerHTML).to.include("<code>code block</code>");
112 });
113
114 it("displays usage information when available", async () => {
115 const usage: Usage = {
116 input_tokens: 150,
117 output_tokens: 300,
118 cost_usd: 0.025,
119 cache_read_input_tokens: 50
120 };
121
122 const message = createMockMessage({
123 usage
124 });
125
126 const el: SketchTimelineMessage = await fixture(html`
127 <sketch-timeline-message
128 .message=${message}
129 ></sketch-timeline-message>
130 `);
131
132 const usageElement = el.shadowRoot!.querySelector(".message-usage");
133 expect(usageElement).to.exist;
134 expect(usageElement!.textContent).to.include("In: 150");
135 expect(usageElement!.textContent).to.include("Out: 300");
136 expect(usageElement!.textContent).to.include("Cache: 50");
137 expect(usageElement!.textContent).to.include("$0.03");
138 });
139
140 it("renders commit information correctly", async () => {
141 const commits: GitCommit[] = [
142 {
143 hash: "1234567890abcdef",
144 subject: "Fix bug in application",
145 body: "This fixes a major bug in the application\n\nSigned-off-by: Developer",
146 pushed_branch: "main"
147 }
148 ];
149
150 const message = createMockMessage({
151 commits
152 });
153
154 const el: SketchTimelineMessage = await fixture(html`
155 <sketch-timeline-message
156 .message=${message}
157 ></sketch-timeline-message>
158 `);
159
160 const commitsContainer = el.shadowRoot!.querySelector(".commits-container");
161 expect(commitsContainer).to.exist;
162
163 const commitHeader = commitsContainer!.querySelector(".commits-header");
164 expect(commitHeader).to.exist;
165 expect(commitHeader!.textContent).to.include("1 new commit");
166
167 const commitHash = commitsContainer!.querySelector(".commit-hash");
168 expect(commitHash).to.exist;
169 expect(commitHash!.textContent).to.equal("12345678"); // First 8 chars
170
171 const pushedBranch = commitsContainer!.querySelector(".pushed-branch");
172 expect(pushedBranch).to.exist;
173 expect(pushedBranch!.textContent).to.include("main");
174 });
175
176 it("dispatches show-commit-diff event when commit diff button is clicked", async () => {
177 const commits: GitCommit[] = [
178 {
179 hash: "1234567890abcdef",
180 subject: "Fix bug in application",
181 body: "This fixes a major bug in the application",
182 pushed_branch: "main"
183 }
184 ];
185
186 const message = createMockMessage({
187 commits
188 });
189
190 const el: SketchTimelineMessage = await fixture(html`
191 <sketch-timeline-message
192 .message=${message}
193 ></sketch-timeline-message>
194 `);
195
196 const diffButton = el.shadowRoot!.querySelector(".commit-diff-button") as HTMLButtonElement;
197 expect(diffButton).to.exist;
198
199 // Set up listener for the event
200 setTimeout(() => diffButton!.click());
201 const { detail } = await oneEvent(el, "show-commit-diff");
202
203 expect(detail).to.exist;
204 expect(detail.commitHash).to.equal("1234567890abcdef");
205 });
206
207 it("handles message type icon display correctly", async () => {
208 // First message of a type should show icon
209 const firstMessage = createMockMessage({
210 type: "user",
211 idx: 0
212 });
213
214 // Second message of same type should not show icon
215 const secondMessage = createMockMessage({
216 type: "user",
217 idx: 1
218 });
219
220 // Test first message (should show icon)
221 const firstEl: SketchTimelineMessage = await fixture(html`
222 <sketch-timeline-message
223 .message=${firstMessage}
224 ></sketch-timeline-message>
225 `);
226
227 const firstIcon = firstEl.shadowRoot!.querySelector(".message-icon");
228 expect(firstIcon).to.exist;
229 expect(firstIcon!.textContent!.trim()).to.equal("U");
230
231 // Test second message with previous message of same type
232 const secondEl: SketchTimelineMessage = await fixture(html`
233 <sketch-timeline-message
234 .message=${secondMessage}
235 .previousMessage=${firstMessage}
236 ></sketch-timeline-message>
237 `);
238
239 const secondIcon = secondEl.shadowRoot!.querySelector(".message-icon");
240 expect(secondIcon).to.not.exist;
241 });
242
243 it("formats numbers correctly", async () => {
244 const el: SketchTimelineMessage = await fixture(html`
245 <sketch-timeline-message></sketch-timeline-message>
246 `);
247
248 // Test accessing private method via the component instance
249 expect(el.formatNumber(1000)).to.equal("1,000");
250 expect(el.formatNumber(null, "N/A")).to.equal("N/A");
251 expect(el.formatNumber(undefined, "--")).to.equal("--");
252 });
253
254 it("formats currency values correctly", async () => {
255 const el: SketchTimelineMessage = await fixture(html`
256 <sketch-timeline-message></sketch-timeline-message>
257 `);
258
259 // Test with different precisions
260 expect(el.formatCurrency(10.12345, "$0.00", true)).to.equal("$10.1235"); // message level (4 decimals)
261 expect(el.formatCurrency(10.12345, "$0.00", false)).to.equal("$10.12"); // total level (2 decimals)
262 expect(el.formatCurrency(null, "N/A")).to.equal("N/A");
263 expect(el.formatCurrency(undefined, "--")).to.equal("--");
264 });
265});