blob: bc742025c8e11d6206d102c8697133b6bacdaded [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();
152 await expect(component.locator(".message-usage")).toContainText("150"); // In
153 await expect(component.locator(".message-usage")).toContainText("300"); // Out
154 await expect(component.locator(".message-usage")).toContainText("50"); // Cache
155 await expect(component.locator(".message-usage")).toContainText("$0.03"); // Cost
156});
157
158test("renders commit information correctly", async ({ mount }) => {
159 const commits: GitCommit[] = [
160 {
161 hash: "1234567890abcdef",
162 subject: "Fix bug in application",
163 body: "This fixes a major bug in the application\n\nSigned-off-by: Developer",
164 pushed_branch: "main",
165 },
166 ];
167
168 const message = createMockMessage({
169 commits,
170 });
171
172 const component = await mount(SketchTimelineMessage, {
173 props: {
174 message: message,
175 },
176 });
177
178 await expect(component.locator(".commits-container")).toBeVisible();
179 await expect(component.locator(".commits-header")).toBeVisible();
180 await expect(component.locator(".commits-header")).toContainText("1 new");
181
182 await expect(component.locator(".commit-hash")).toBeVisible();
183 await expect(component.locator(".commit-hash")).toHaveText("12345678"); // First 8 chars
184
185 await expect(component.locator(".pushed-branch")).toBeVisible();
186 await expect(component.locator(".pushed-branch")).toContainText("main");
187});
188
189test("dispatches show-commit-diff event when commit diff button is clicked", async ({
190 mount,
191}) => {
192 const commits: GitCommit[] = [
193 {
194 hash: "1234567890abcdef",
195 subject: "Fix bug in application",
196 body: "This fixes a major bug in the application",
197 pushed_branch: "main",
198 },
199 ];
200
201 const message = createMockMessage({
202 commits,
203 });
204
205 const component = await mount(SketchTimelineMessage, {
206 props: {
207 message: message,
208 },
209 });
210
211 await expect(component.locator(".commit-diff-button")).toBeVisible();
212
213 // Set up promise to wait for the event
214 const eventPromise = component.evaluate((el) => {
215 return new Promise((resolve) => {
216 el.addEventListener(
217 "show-commit-diff",
218 (event) => {
219 resolve((event as CustomEvent).detail);
220 },
221 { once: true },
222 );
Sean McCullough86b56862025-04-18 13:04:03 -0700223 });
Sean McCullough86b56862025-04-18 13:04:03 -0700224 });
225
Sean McCulloughb29f8912025-04-20 15:39:11 -0700226 // Click the diff button
227 await component.locator(".commit-diff-button").click();
Sean McCullough71941bd2025-04-18 13:31:48 -0700228
Sean McCulloughb29f8912025-04-20 15:39:11 -0700229 // Wait for the event and check its details
230 const detail = await eventPromise;
231 expect(detail["commitHash"]).toBe("1234567890abcdef");
232});
Sean McCullough71941bd2025-04-18 13:31:48 -0700233
Sean McCulloughb29f8912025-04-20 15:39:11 -0700234test.skip("handles message type icon display correctly", async ({ mount }) => {
235 // First message of a type should show icon
236 const firstMessage = createMockMessage({
237 type: "user",
238 idx: 0,
Sean McCullough86b56862025-04-18 13:04:03 -0700239 });
240
Sean McCulloughb29f8912025-04-20 15:39:11 -0700241 // Second message of same type should not show icon
242 const secondMessage = createMockMessage({
243 type: "user",
244 idx: 1,
Sean McCullough86b56862025-04-18 13:04:03 -0700245 });
246
Sean McCulloughb29f8912025-04-20 15:39:11 -0700247 // Test first message (should show icon)
248 const firstComponent = await mount(SketchTimelineMessage, {
249 props: {
250 message: firstMessage,
251 },
Sean McCullough86b56862025-04-18 13:04:03 -0700252 });
253
Sean McCulloughb29f8912025-04-20 15:39:11 -0700254 await expect(firstComponent.locator(".message-icon")).toBeVisible();
255 await expect(firstComponent.locator(".message-icon")).toHaveText("U");
Sean McCullough86b56862025-04-18 13:04:03 -0700256
Sean McCulloughb29f8912025-04-20 15:39:11 -0700257 // Test second message with previous message of same type
258 const secondComponent = await mount(SketchTimelineMessage, {
259 props: {
260 message: secondMessage,
261 previousMessage: firstMessage,
262 },
Sean McCullough86b56862025-04-18 13:04:03 -0700263 });
264
Sean McCulloughb29f8912025-04-20 15:39:11 -0700265 await expect(secondComponent.locator(".message-icon")).not.toBeVisible();
266});
Sean McCullough71941bd2025-04-18 13:31:48 -0700267
Sean McCulloughb29f8912025-04-20 15:39:11 -0700268test("formats numbers correctly", async ({ mount }) => {
269 const component = await mount(SketchTimelineMessage, {});
Sean McCullough86b56862025-04-18 13:04:03 -0700270
Sean McCulloughb29f8912025-04-20 15:39:11 -0700271 // Test accessing public method via evaluate
272 const result1 = await component.evaluate((el: SketchTimelineMessage) =>
273 el.formatNumber(1000),
274 );
275 expect(result1).toBe("1,000");
Sean McCullough86b56862025-04-18 13:04:03 -0700276
Sean McCulloughb29f8912025-04-20 15:39:11 -0700277 const result2 = await component.evaluate((el: SketchTimelineMessage) =>
278 el.formatNumber(null, "N/A"),
279 );
280 expect(result2).toBe("N/A");
Sean McCullough86b56862025-04-18 13:04:03 -0700281
Sean McCulloughb29f8912025-04-20 15:39:11 -0700282 const result3 = await component.evaluate((el: SketchTimelineMessage) =>
283 el.formatNumber(undefined, "--"),
284 );
285 expect(result3).toBe("--");
286});
Sean McCullough71941bd2025-04-18 13:31:48 -0700287
Sean McCulloughb29f8912025-04-20 15:39:11 -0700288test("formats currency values correctly", async ({ mount }) => {
289 const component = await mount(SketchTimelineMessage, {});
Sean McCullough86b56862025-04-18 13:04:03 -0700290
Sean McCulloughb29f8912025-04-20 15:39:11 -0700291 // Test with different precisions
292 const result1 = await component.evaluate((el: SketchTimelineMessage) =>
293 el.formatCurrency(10.12345, "$0.00", true),
294 );
295 expect(result1).toBe("$10.1235"); // message level (4 decimals)
Sean McCullough86b56862025-04-18 13:04:03 -0700296
Sean McCulloughb29f8912025-04-20 15:39:11 -0700297 const result2 = await component.evaluate((el: SketchTimelineMessage) =>
298 el.formatCurrency(10.12345, "$0.00", false),
299 );
300 expect(result2).toBe("$10.12"); // total level (2 decimals)
Sean McCullough71941bd2025-04-18 13:31:48 -0700301
Sean McCulloughb29f8912025-04-20 15:39:11 -0700302 const result3 = await component.evaluate((el: SketchTimelineMessage) =>
303 el.formatCurrency(null, "N/A"),
304 );
305 expect(result3).toBe("N/A");
Sean McCullough71941bd2025-04-18 13:31:48 -0700306
Sean McCulloughb29f8912025-04-20 15:39:11 -0700307 const result4 = await component.evaluate((el: SketchTimelineMessage) =>
308 el.formatCurrency(undefined, "--"),
309 );
310 expect(result4).toBe("--");
Sean McCullough86b56862025-04-18 13:04:03 -0700311});