blob: eb1b788e28fce945c26b680b61f84676a07ed984 [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 McCullough86b56862025-04-18 13:04:03 -07003import { TimelineMessage, ToolCall, GitCommit, Usage } from "../types";
4
Sean McCulloughb29f8912025-04-20 15:39:11 -07005// Helper function to create mock timeline messages
6function createMockMessage(
7 props: Partial<TimelineMessage> = {},
8): 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
24test("renders with basic message content", async ({ mount }) => {
25 const message = createMockMessage({
26 type: "agent",
27 content: "This is a test message",
28 });
29
30 const component = await mount(SketchTimelineMessage, {
31 props: {
32 message: message,
33 },
34 });
35
36 await expect(component.locator(".message-text")).toBeVisible();
37 await expect(component.locator(".message-text")).toContainText(
38 "This is a test message",
39 );
40});
41
42test.skip("renders with correct message type classes", async ({ mount }) => {
43 const messageTypes = ["user", "agent", "tool", "error"];
44
45 for (const type of messageTypes) {
46 const message = createMockMessage({ type });
47
48 const component = await mount(SketchTimelineMessage, {
49 props: {
50 message: message,
51 },
52 });
53
54 await expect(component.locator(".message")).toBeVisible();
55 await expect(component.locator(`.message.${type}`)).toBeVisible();
Sean McCullough86b56862025-04-18 13:04:03 -070056 }
Sean McCulloughb29f8912025-04-20 15:39:11 -070057});
Sean McCullough86b56862025-04-18 13:04:03 -070058
Sean McCulloughb29f8912025-04-20 15:39:11 -070059test("renders end-of-turn marker correctly", async ({ mount }) => {
60 const message = createMockMessage({
61 end_of_turn: true,
62 });
63
64 const component = await mount(SketchTimelineMessage, {
65 props: {
66 message: message,
67 },
68 });
69
70 await expect(component.locator(".message")).toBeVisible();
71 await expect(component.locator(".message.end-of-turn")).toBeVisible();
72});
73
74test("formats timestamps correctly", async ({ mount }) => {
75 const message = createMockMessage({
76 timestamp: "2023-05-15T12:00:00Z",
77 });
78
79 const component = await mount(SketchTimelineMessage, {
80 props: {
81 message: message,
82 },
83 });
84
85 await expect(component.locator(".message-timestamp")).toBeVisible();
86 // Should include a formatted date like "May 15, 2023"
87 await expect(component.locator(".message-timestamp")).toContainText(
88 "May 15, 2023",
89 );
90 // Should include elapsed time
91 await expect(component.locator(".message-timestamp")).toContainText(
92 "(1.50s)",
93 );
94});
95
96test("renders markdown content correctly", async ({ mount }) => {
97 const markdownContent =
98 "# Heading\n\n- List item 1\n- List item 2\n\n`code block`";
99 const message = createMockMessage({
100 content: markdownContent,
101 });
102
103 const component = await mount(SketchTimelineMessage, {
104 props: {
105 message: message,
106 },
107 });
108
109 await expect(component.locator(".markdown-content")).toBeVisible();
110
111 // Check HTML content
112 const html = await component
113 .locator(".markdown-content")
114 .evaluate((element) => element.innerHTML);
115 expect(html).toContain("<h1>Heading</h1>");
116 expect(html).toContain("<ul>");
117 expect(html).toContain("<li>List item 1</li>");
118 expect(html).toContain("<code>code block</code>");
119});
120
121test("displays usage information when available", async ({ mount }) => {
122 const usage: Usage = {
123 input_tokens: 150,
124 output_tokens: 300,
125 cost_usd: 0.025,
126 cache_read_input_tokens: 50,
127 };
128
129 const message = createMockMessage({
130 usage,
131 });
132
133 const component = await mount(SketchTimelineMessage, {
134 props: {
135 message: message,
136 },
137 });
138
139 await expect(component.locator(".message-usage")).toBeVisible();
140 await expect(component.locator(".message-usage")).toContainText("150"); // In
141 await expect(component.locator(".message-usage")).toContainText("300"); // Out
142 await expect(component.locator(".message-usage")).toContainText("50"); // Cache
143 await expect(component.locator(".message-usage")).toContainText("$0.03"); // Cost
144});
145
146test("renders commit information correctly", async ({ mount }) => {
147 const commits: GitCommit[] = [
148 {
149 hash: "1234567890abcdef",
150 subject: "Fix bug in application",
151 body: "This fixes a major bug in the application\n\nSigned-off-by: Developer",
152 pushed_branch: "main",
153 },
154 ];
155
156 const message = createMockMessage({
157 commits,
158 });
159
160 const component = await mount(SketchTimelineMessage, {
161 props: {
162 message: message,
163 },
164 });
165
166 await expect(component.locator(".commits-container")).toBeVisible();
167 await expect(component.locator(".commits-header")).toBeVisible();
168 await expect(component.locator(".commits-header")).toContainText("1 new");
169
170 await expect(component.locator(".commit-hash")).toBeVisible();
171 await expect(component.locator(".commit-hash")).toHaveText("12345678"); // First 8 chars
172
173 await expect(component.locator(".pushed-branch")).toBeVisible();
174 await expect(component.locator(".pushed-branch")).toContainText("main");
175});
176
177test("dispatches show-commit-diff event when commit diff button is clicked", async ({
178 mount,
179}) => {
180 const commits: GitCommit[] = [
181 {
182 hash: "1234567890abcdef",
183 subject: "Fix bug in application",
184 body: "This fixes a major bug in the application",
185 pushed_branch: "main",
186 },
187 ];
188
189 const message = createMockMessage({
190 commits,
191 });
192
193 const component = await mount(SketchTimelineMessage, {
194 props: {
195 message: message,
196 },
197 });
198
199 await expect(component.locator(".commit-diff-button")).toBeVisible();
200
201 // Set up promise to wait for the event
202 const eventPromise = component.evaluate((el) => {
203 return new Promise((resolve) => {
204 el.addEventListener(
205 "show-commit-diff",
206 (event) => {
207 resolve((event as CustomEvent).detail);
208 },
209 { once: true },
210 );
Sean McCullough86b56862025-04-18 13:04:03 -0700211 });
Sean McCullough86b56862025-04-18 13:04:03 -0700212 });
213
Sean McCulloughb29f8912025-04-20 15:39:11 -0700214 // Click the diff button
215 await component.locator(".commit-diff-button").click();
Sean McCullough71941bd2025-04-18 13:31:48 -0700216
Sean McCulloughb29f8912025-04-20 15:39:11 -0700217 // Wait for the event and check its details
218 const detail = await eventPromise;
219 expect(detail["commitHash"]).toBe("1234567890abcdef");
220});
Sean McCullough71941bd2025-04-18 13:31:48 -0700221
Sean McCulloughb29f8912025-04-20 15:39:11 -0700222test.skip("handles message type icon display correctly", async ({ mount }) => {
223 // First message of a type should show icon
224 const firstMessage = createMockMessage({
225 type: "user",
226 idx: 0,
Sean McCullough86b56862025-04-18 13:04:03 -0700227 });
228
Sean McCulloughb29f8912025-04-20 15:39:11 -0700229 // Second message of same type should not show icon
230 const secondMessage = createMockMessage({
231 type: "user",
232 idx: 1,
Sean McCullough86b56862025-04-18 13:04:03 -0700233 });
234
Sean McCulloughb29f8912025-04-20 15:39:11 -0700235 // Test first message (should show icon)
236 const firstComponent = await mount(SketchTimelineMessage, {
237 props: {
238 message: firstMessage,
239 },
Sean McCullough86b56862025-04-18 13:04:03 -0700240 });
241
Sean McCulloughb29f8912025-04-20 15:39:11 -0700242 await expect(firstComponent.locator(".message-icon")).toBeVisible();
243 await expect(firstComponent.locator(".message-icon")).toHaveText("U");
Sean McCullough86b56862025-04-18 13:04:03 -0700244
Sean McCulloughb29f8912025-04-20 15:39:11 -0700245 // Test second message with previous message of same type
246 const secondComponent = await mount(SketchTimelineMessage, {
247 props: {
248 message: secondMessage,
249 previousMessage: firstMessage,
250 },
Sean McCullough86b56862025-04-18 13:04:03 -0700251 });
252
Sean McCulloughb29f8912025-04-20 15:39:11 -0700253 await expect(secondComponent.locator(".message-icon")).not.toBeVisible();
254});
Sean McCullough71941bd2025-04-18 13:31:48 -0700255
Sean McCulloughb29f8912025-04-20 15:39:11 -0700256test("formats numbers correctly", async ({ mount }) => {
257 const component = await mount(SketchTimelineMessage, {});
Sean McCullough86b56862025-04-18 13:04:03 -0700258
Sean McCulloughb29f8912025-04-20 15:39:11 -0700259 // Test accessing public method via evaluate
260 const result1 = await component.evaluate((el: SketchTimelineMessage) =>
261 el.formatNumber(1000),
262 );
263 expect(result1).toBe("1,000");
Sean McCullough86b56862025-04-18 13:04:03 -0700264
Sean McCulloughb29f8912025-04-20 15:39:11 -0700265 const result2 = await component.evaluate((el: SketchTimelineMessage) =>
266 el.formatNumber(null, "N/A"),
267 );
268 expect(result2).toBe("N/A");
Sean McCullough86b56862025-04-18 13:04:03 -0700269
Sean McCulloughb29f8912025-04-20 15:39:11 -0700270 const result3 = await component.evaluate((el: SketchTimelineMessage) =>
271 el.formatNumber(undefined, "--"),
272 );
273 expect(result3).toBe("--");
274});
Sean McCullough71941bd2025-04-18 13:31:48 -0700275
Sean McCulloughb29f8912025-04-20 15:39:11 -0700276test("formats currency values correctly", async ({ mount }) => {
277 const component = await mount(SketchTimelineMessage, {});
Sean McCullough86b56862025-04-18 13:04:03 -0700278
Sean McCulloughb29f8912025-04-20 15:39:11 -0700279 // Test with different precisions
280 const result1 = await component.evaluate((el: SketchTimelineMessage) =>
281 el.formatCurrency(10.12345, "$0.00", true),
282 );
283 expect(result1).toBe("$10.1235"); // message level (4 decimals)
Sean McCullough86b56862025-04-18 13:04:03 -0700284
Sean McCulloughb29f8912025-04-20 15:39:11 -0700285 const result2 = await component.evaluate((el: SketchTimelineMessage) =>
286 el.formatCurrency(10.12345, "$0.00", false),
287 );
288 expect(result2).toBe("$10.12"); // total level (2 decimals)
Sean McCullough71941bd2025-04-18 13:31:48 -0700289
Sean McCulloughb29f8912025-04-20 15:39:11 -0700290 const result3 = await component.evaluate((el: SketchTimelineMessage) =>
291 el.formatCurrency(null, "N/A"),
292 );
293 expect(result3).toBe("N/A");
Sean McCullough71941bd2025-04-18 13:31:48 -0700294
Sean McCulloughb29f8912025-04-20 15:39:11 -0700295 const result4 = await component.evaluate((el: SketchTimelineMessage) =>
296 el.formatCurrency(undefined, "--"),
297 );
298 expect(result4).toBe("--");
Sean McCullough86b56862025-04-18 13:04:03 -0700299});