blob: 3d9b7992910322f097459abd5d3df2769527a0eb [file] [log] [blame]
Sean McCulloughb29f8912025-04-20 15:39:11 -07001import { test, expect } from "@sand4rt/experimental-ct-web";
2import { SketchTimelineMessage } from "./sketch-timeline-message";
Sean McCulloughd9f13372025-04-21 15:08:49 -07003import {
4 AgentMessage,
5 CodingAgentMessageType,
6 GitCommit,
7 Usage,
8} from "../types";
Sean McCullough86b56862025-04-18 13:04:03 -07009
Sean McCulloughb29f8912025-04-20 15:39:11 -070010// Helper function to create mock timeline messages
Sean McCulloughd9f13372025-04-21 15:08:49 -070011function createMockMessage(props: Partial<AgentMessage> = {}): AgentMessage {
Sean McCulloughb29f8912025-04-20 15:39:11 -070012 return {
13 idx: props.idx || 0,
14 type: props.type || "agent",
15 content: props.content || "Hello world",
16 timestamp: props.timestamp || "2023-05-15T12:00:00Z",
17 elapsed: props.elapsed || 1500000000, // 1.5 seconds in nanoseconds
18 end_of_turn: props.end_of_turn || false,
19 conversation_id: props.conversation_id || "conv123",
20 tool_calls: props.tool_calls || [],
21 commits: props.commits || [],
22 usage: props.usage,
23 ...props,
24 };
25}
26
27test("renders with basic message content", async ({ mount }) => {
28 const message = createMockMessage({
29 type: "agent",
30 content: "This is a test message",
31 });
32
33 const component = await mount(SketchTimelineMessage, {
34 props: {
35 message: message,
36 },
37 });
38
39 await expect(component.locator(".message-text")).toBeVisible();
40 await expect(component.locator(".message-text")).toContainText(
41 "This is a test message",
42 );
43});
44
45test.skip("renders with correct message type classes", async ({ mount }) => {
Sean McCulloughd9f13372025-04-21 15:08:49 -070046 const messageTypes: CodingAgentMessageType[] = [
47 "user",
48 "agent",
49 "error",
50 "budget",
51 "tool",
52 "commit",
53 "auto",
54 ];
Sean McCulloughb29f8912025-04-20 15:39:11 -070055
56 for (const type of messageTypes) {
57 const message = createMockMessage({ type });
58
59 const component = await mount(SketchTimelineMessage, {
60 props: {
61 message: message,
62 },
63 });
64
65 await expect(component.locator(".message")).toBeVisible();
66 await expect(component.locator(`.message.${type}`)).toBeVisible();
Sean McCullough86b56862025-04-18 13:04:03 -070067 }
Sean McCulloughb29f8912025-04-20 15:39:11 -070068});
Sean McCullough86b56862025-04-18 13:04:03 -070069
Sean McCulloughb29f8912025-04-20 15:39:11 -070070test("renders end-of-turn marker correctly", async ({ mount }) => {
71 const message = createMockMessage({
72 end_of_turn: true,
73 });
74
75 const component = await mount(SketchTimelineMessage, {
76 props: {
77 message: message,
78 },
79 });
80
81 await expect(component.locator(".message")).toBeVisible();
82 await expect(component.locator(".message.end-of-turn")).toBeVisible();
83});
84
85test("formats timestamps correctly", async ({ mount }) => {
86 const message = createMockMessage({
87 timestamp: "2023-05-15T12:00:00Z",
88 });
89
90 const component = await mount(SketchTimelineMessage, {
91 props: {
92 message: message,
93 },
94 });
95
96 await expect(component.locator(".message-timestamp")).toBeVisible();
97 // Should include a formatted date like "May 15, 2023"
98 await expect(component.locator(".message-timestamp")).toContainText(
99 "May 15, 2023",
100 );
101 // Should include elapsed time
102 await expect(component.locator(".message-timestamp")).toContainText(
103 "(1.50s)",
104 );
105});
106
107test("renders markdown content correctly", async ({ mount }) => {
108 const markdownContent =
109 "# Heading\n\n- List item 1\n- List item 2\n\n`code block`";
110 const message = createMockMessage({
111 content: markdownContent,
112 });
113
114 const component = await mount(SketchTimelineMessage, {
115 props: {
116 message: message,
117 },
118 });
119
120 await expect(component.locator(".markdown-content")).toBeVisible();
121
122 // Check HTML content
123 const html = await component
124 .locator(".markdown-content")
125 .evaluate((element) => element.innerHTML);
126 expect(html).toContain("<h1>Heading</h1>");
127 expect(html).toContain("<ul>");
128 expect(html).toContain("<li>List item 1</li>");
129 expect(html).toContain("<code>code block</code>");
130});
131
132test("displays usage information when available", async ({ mount }) => {
133 const usage: Usage = {
134 input_tokens: 150,
135 output_tokens: 300,
136 cost_usd: 0.025,
137 cache_read_input_tokens: 50,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700138 cache_creation_input_tokens: 0,
Sean McCulloughb29f8912025-04-20 15:39:11 -0700139 };
140
141 const message = createMockMessage({
142 usage,
143 });
144
145 const component = await mount(SketchTimelineMessage, {
146 props: {
147 message: message,
148 },
149 });
150
151 await expect(component.locator(".message-usage")).toBeVisible();
Autoformatter5c70bfe2025-04-25 21:28:00 +0000152 await expect(component.locator(".message-usage")).toContainText(
153 "200".toLocaleString(),
154 ); // In (150 + 50 cache)
155 await expect(component.locator(".message-usage")).toContainText(
156 "300".toLocaleString(),
157 ); // Out
Sean McCulloughb29f8912025-04-20 15:39:11 -0700158 await expect(component.locator(".message-usage")).toContainText("$0.03"); // Cost
159});
160
161test("renders commit information correctly", async ({ mount }) => {
162 const commits: GitCommit[] = [
163 {
164 hash: "1234567890abcdef",
165 subject: "Fix bug in application",
166 body: "This fixes a major bug in the application\n\nSigned-off-by: Developer",
167 pushed_branch: "main",
168 },
169 ];
170
171 const message = createMockMessage({
172 commits,
173 });
174
175 const component = await mount(SketchTimelineMessage, {
176 props: {
177 message: message,
178 },
179 });
180
181 await expect(component.locator(".commits-container")).toBeVisible();
182 await expect(component.locator(".commits-header")).toBeVisible();
183 await expect(component.locator(".commits-header")).toContainText("1 new");
184
185 await expect(component.locator(".commit-hash")).toBeVisible();
186 await expect(component.locator(".commit-hash")).toHaveText("12345678"); // First 8 chars
187
188 await expect(component.locator(".pushed-branch")).toBeVisible();
189 await expect(component.locator(".pushed-branch")).toContainText("main");
190});
191
192test("dispatches show-commit-diff event when commit diff button is clicked", async ({
193 mount,
194}) => {
195 const commits: GitCommit[] = [
196 {
197 hash: "1234567890abcdef",
198 subject: "Fix bug in application",
199 body: "This fixes a major bug in the application",
200 pushed_branch: "main",
201 },
202 ];
203
204 const message = createMockMessage({
205 commits,
206 });
207
208 const component = await mount(SketchTimelineMessage, {
209 props: {
210 message: message,
211 },
212 });
213
214 await expect(component.locator(".commit-diff-button")).toBeVisible();
215
216 // Set up promise to wait for the event
217 const eventPromise = component.evaluate((el) => {
218 return new Promise((resolve) => {
219 el.addEventListener(
220 "show-commit-diff",
221 (event) => {
222 resolve((event as CustomEvent).detail);
223 },
224 { once: true },
225 );
Sean McCullough86b56862025-04-18 13:04:03 -0700226 });
Sean McCullough86b56862025-04-18 13:04:03 -0700227 });
228
Sean McCulloughb29f8912025-04-20 15:39:11 -0700229 // Click the diff button
230 await component.locator(".commit-diff-button").click();
Sean McCullough71941bd2025-04-18 13:31:48 -0700231
Sean McCulloughb29f8912025-04-20 15:39:11 -0700232 // Wait for the event and check its details
233 const detail = await eventPromise;
234 expect(detail["commitHash"]).toBe("1234567890abcdef");
235});
Sean McCullough71941bd2025-04-18 13:31:48 -0700236
Sean McCulloughb29f8912025-04-20 15:39:11 -0700237test.skip("handles message type icon display correctly", async ({ mount }) => {
238 // First message of a type should show icon
239 const firstMessage = createMockMessage({
240 type: "user",
241 idx: 0,
Sean McCullough86b56862025-04-18 13:04:03 -0700242 });
243
Sean McCulloughb29f8912025-04-20 15:39:11 -0700244 // Second message of same type should not show icon
245 const secondMessage = createMockMessage({
246 type: "user",
247 idx: 1,
Sean McCullough86b56862025-04-18 13:04:03 -0700248 });
249
Sean McCulloughb29f8912025-04-20 15:39:11 -0700250 // Test first message (should show icon)
251 const firstComponent = await mount(SketchTimelineMessage, {
252 props: {
253 message: firstMessage,
254 },
Sean McCullough86b56862025-04-18 13:04:03 -0700255 });
256
Sean McCulloughb29f8912025-04-20 15:39:11 -0700257 await expect(firstComponent.locator(".message-icon")).toBeVisible();
258 await expect(firstComponent.locator(".message-icon")).toHaveText("U");
Sean McCullough86b56862025-04-18 13:04:03 -0700259
Sean McCulloughb29f8912025-04-20 15:39:11 -0700260 // Test second message with previous message of same type
261 const secondComponent = await mount(SketchTimelineMessage, {
262 props: {
263 message: secondMessage,
264 previousMessage: firstMessage,
265 },
Sean McCullough86b56862025-04-18 13:04:03 -0700266 });
267
Sean McCulloughb29f8912025-04-20 15:39:11 -0700268 await expect(secondComponent.locator(".message-icon")).not.toBeVisible();
269});
Sean McCullough71941bd2025-04-18 13:31:48 -0700270
Sean McCulloughb29f8912025-04-20 15:39:11 -0700271test("formats numbers correctly", async ({ mount }) => {
272 const component = await mount(SketchTimelineMessage, {});
Sean McCullough86b56862025-04-18 13:04:03 -0700273
Sean McCulloughb29f8912025-04-20 15:39:11 -0700274 // Test accessing public method via evaluate
275 const result1 = await component.evaluate((el: SketchTimelineMessage) =>
276 el.formatNumber(1000),
277 );
278 expect(result1).toBe("1,000");
Sean McCullough86b56862025-04-18 13:04:03 -0700279
Sean McCulloughb29f8912025-04-20 15:39:11 -0700280 const result2 = await component.evaluate((el: SketchTimelineMessage) =>
281 el.formatNumber(null, "N/A"),
282 );
283 expect(result2).toBe("N/A");
Sean McCullough86b56862025-04-18 13:04:03 -0700284
Sean McCulloughb29f8912025-04-20 15:39:11 -0700285 const result3 = await component.evaluate((el: SketchTimelineMessage) =>
286 el.formatNumber(undefined, "--"),
287 );
288 expect(result3).toBe("--");
289});
Sean McCullough71941bd2025-04-18 13:31:48 -0700290
Sean McCulloughb29f8912025-04-20 15:39:11 -0700291test("formats currency values correctly", async ({ mount }) => {
292 const component = await mount(SketchTimelineMessage, {});
Sean McCullough86b56862025-04-18 13:04:03 -0700293
Sean McCulloughb29f8912025-04-20 15:39:11 -0700294 // Test with different precisions
295 const result1 = await component.evaluate((el: SketchTimelineMessage) =>
296 el.formatCurrency(10.12345, "$0.00", true),
297 );
298 expect(result1).toBe("$10.1235"); // message level (4 decimals)
Sean McCullough86b56862025-04-18 13:04:03 -0700299
Sean McCulloughb29f8912025-04-20 15:39:11 -0700300 const result2 = await component.evaluate((el: SketchTimelineMessage) =>
301 el.formatCurrency(10.12345, "$0.00", false),
302 );
303 expect(result2).toBe("$10.12"); // total level (2 decimals)
Sean McCullough71941bd2025-04-18 13:31:48 -0700304
Sean McCulloughb29f8912025-04-20 15:39:11 -0700305 const result3 = await component.evaluate((el: SketchTimelineMessage) =>
306 el.formatCurrency(null, "N/A"),
307 );
308 expect(result3).toBe("N/A");
Sean McCullough71941bd2025-04-18 13:31:48 -0700309
Sean McCulloughb29f8912025-04-20 15:39:11 -0700310 const result4 = await component.evaluate((el: SketchTimelineMessage) =>
311 el.formatCurrency(undefined, "--"),
312 );
313 expect(result4).toBe("--");
Sean McCullough86b56862025-04-18 13:04:03 -0700314});