blob: d5c8dfb7f8883adb1070604962f683bef665b50b [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();
Josh Bleecher Snyder35889972025-04-24 20:48:16 +0000152 await expect(component.locator(".message-usage")).toContainText("200"); // In (150 + 50 cache)
Sean McCulloughb29f8912025-04-20 15:39:11 -0700153 await expect(component.locator(".message-usage")).toContainText("300"); // Out
Sean McCulloughb29f8912025-04-20 15:39:11 -0700154 await expect(component.locator(".message-usage")).toContainText("$0.03"); // Cost
155});
156
157test("renders commit information correctly", async ({ mount }) => {
158 const commits: GitCommit[] = [
159 {
160 hash: "1234567890abcdef",
161 subject: "Fix bug in application",
162 body: "This fixes a major bug in the application\n\nSigned-off-by: Developer",
163 pushed_branch: "main",
164 },
165 ];
166
167 const message = createMockMessage({
168 commits,
169 });
170
171 const component = await mount(SketchTimelineMessage, {
172 props: {
173 message: message,
174 },
175 });
176
177 await expect(component.locator(".commits-container")).toBeVisible();
178 await expect(component.locator(".commits-header")).toBeVisible();
179 await expect(component.locator(".commits-header")).toContainText("1 new");
180
181 await expect(component.locator(".commit-hash")).toBeVisible();
182 await expect(component.locator(".commit-hash")).toHaveText("12345678"); // First 8 chars
183
184 await expect(component.locator(".pushed-branch")).toBeVisible();
185 await expect(component.locator(".pushed-branch")).toContainText("main");
186});
187
188test("dispatches show-commit-diff event when commit diff button is clicked", async ({
189 mount,
190}) => {
191 const commits: GitCommit[] = [
192 {
193 hash: "1234567890abcdef",
194 subject: "Fix bug in application",
195 body: "This fixes a major bug in the application",
196 pushed_branch: "main",
197 },
198 ];
199
200 const message = createMockMessage({
201 commits,
202 });
203
204 const component = await mount(SketchTimelineMessage, {
205 props: {
206 message: message,
207 },
208 });
209
210 await expect(component.locator(".commit-diff-button")).toBeVisible();
211
212 // Set up promise to wait for the event
213 const eventPromise = component.evaluate((el) => {
214 return new Promise((resolve) => {
215 el.addEventListener(
216 "show-commit-diff",
217 (event) => {
218 resolve((event as CustomEvent).detail);
219 },
220 { once: true },
221 );
Sean McCullough86b56862025-04-18 13:04:03 -0700222 });
Sean McCullough86b56862025-04-18 13:04:03 -0700223 });
224
Sean McCulloughb29f8912025-04-20 15:39:11 -0700225 // Click the diff button
226 await component.locator(".commit-diff-button").click();
Sean McCullough71941bd2025-04-18 13:31:48 -0700227
Sean McCulloughb29f8912025-04-20 15:39:11 -0700228 // Wait for the event and check its details
229 const detail = await eventPromise;
230 expect(detail["commitHash"]).toBe("1234567890abcdef");
231});
Sean McCullough71941bd2025-04-18 13:31:48 -0700232
Sean McCulloughb29f8912025-04-20 15:39:11 -0700233test.skip("handles message type icon display correctly", async ({ mount }) => {
234 // First message of a type should show icon
235 const firstMessage = createMockMessage({
236 type: "user",
237 idx: 0,
Sean McCullough86b56862025-04-18 13:04:03 -0700238 });
239
Sean McCulloughb29f8912025-04-20 15:39:11 -0700240 // Second message of same type should not show icon
241 const secondMessage = createMockMessage({
242 type: "user",
243 idx: 1,
Sean McCullough86b56862025-04-18 13:04:03 -0700244 });
245
Sean McCulloughb29f8912025-04-20 15:39:11 -0700246 // Test first message (should show icon)
247 const firstComponent = await mount(SketchTimelineMessage, {
248 props: {
249 message: firstMessage,
250 },
Sean McCullough86b56862025-04-18 13:04:03 -0700251 });
252
Sean McCulloughb29f8912025-04-20 15:39:11 -0700253 await expect(firstComponent.locator(".message-icon")).toBeVisible();
254 await expect(firstComponent.locator(".message-icon")).toHaveText("U");
Sean McCullough86b56862025-04-18 13:04:03 -0700255
Sean McCulloughb29f8912025-04-20 15:39:11 -0700256 // Test second message with previous message of same type
257 const secondComponent = await mount(SketchTimelineMessage, {
258 props: {
259 message: secondMessage,
260 previousMessage: firstMessage,
261 },
Sean McCullough86b56862025-04-18 13:04:03 -0700262 });
263
Sean McCulloughb29f8912025-04-20 15:39:11 -0700264 await expect(secondComponent.locator(".message-icon")).not.toBeVisible();
265});
Sean McCullough71941bd2025-04-18 13:31:48 -0700266
Sean McCulloughb29f8912025-04-20 15:39:11 -0700267test("formats numbers correctly", async ({ mount }) => {
268 const component = await mount(SketchTimelineMessage, {});
Sean McCullough86b56862025-04-18 13:04:03 -0700269
Sean McCulloughb29f8912025-04-20 15:39:11 -0700270 // Test accessing public method via evaluate
271 const result1 = await component.evaluate((el: SketchTimelineMessage) =>
272 el.formatNumber(1000),
273 );
274 expect(result1).toBe("1,000");
Sean McCullough86b56862025-04-18 13:04:03 -0700275
Sean McCulloughb29f8912025-04-20 15:39:11 -0700276 const result2 = await component.evaluate((el: SketchTimelineMessage) =>
277 el.formatNumber(null, "N/A"),
278 );
279 expect(result2).toBe("N/A");
Sean McCullough86b56862025-04-18 13:04:03 -0700280
Sean McCulloughb29f8912025-04-20 15:39:11 -0700281 const result3 = await component.evaluate((el: SketchTimelineMessage) =>
282 el.formatNumber(undefined, "--"),
283 );
284 expect(result3).toBe("--");
285});
Sean McCullough71941bd2025-04-18 13:31:48 -0700286
Sean McCulloughb29f8912025-04-20 15:39:11 -0700287test("formats currency values correctly", async ({ mount }) => {
288 const component = await mount(SketchTimelineMessage, {});
Sean McCullough86b56862025-04-18 13:04:03 -0700289
Sean McCulloughb29f8912025-04-20 15:39:11 -0700290 // Test with different precisions
291 const result1 = await component.evaluate((el: SketchTimelineMessage) =>
292 el.formatCurrency(10.12345, "$0.00", true),
293 );
294 expect(result1).toBe("$10.1235"); // message level (4 decimals)
Sean McCullough86b56862025-04-18 13:04:03 -0700295
Sean McCulloughb29f8912025-04-20 15:39:11 -0700296 const result2 = await component.evaluate((el: SketchTimelineMessage) =>
297 el.formatCurrency(10.12345, "$0.00", false),
298 );
299 expect(result2).toBe("$10.12"); // total level (2 decimals)
Sean McCullough71941bd2025-04-18 13:31:48 -0700300
Sean McCulloughb29f8912025-04-20 15:39:11 -0700301 const result3 = await component.evaluate((el: SketchTimelineMessage) =>
302 el.formatCurrency(null, "N/A"),
303 );
304 expect(result3).toBe("N/A");
Sean McCullough71941bd2025-04-18 13:31:48 -0700305
Sean McCulloughb29f8912025-04-20 15:39:11 -0700306 const result4 = await component.evaluate((el: SketchTimelineMessage) =>
307 el.formatCurrency(undefined, "--"),
308 );
309 expect(result4).toBe("--");
Sean McCullough86b56862025-04-18 13:04:03 -0700310});