blob: d768f02df5972dcc84e0514827f68549e9559944 [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
Sean McCullough71941bd2025-04-18 13:31:48 -07008 function createMockMessage(
9 props: Partial<TimelineMessage> = {},
10 ): TimelineMessage {
Sean McCullough86b56862025-04-18 13:04:03 -070011 return {
12 idx: props.idx || 0,
13 type: props.type || "agent",
14 content: props.content || "Hello world",
15 timestamp: props.timestamp || "2023-05-15T12:00:00Z",
16 elapsed: props.elapsed || 1500000000, // 1.5 seconds in nanoseconds
17 end_of_turn: props.end_of_turn || false,
18 conversation_id: props.conversation_id || "conv123",
19 tool_calls: props.tool_calls || [],
20 commits: props.commits || [],
21 usage: props.usage,
Sean McCullough71941bd2025-04-18 13:31:48 -070022 ...props,
Sean McCullough86b56862025-04-18 13:04:03 -070023 };
24 }
25
26 it("renders with basic message content", async () => {
27 const message = createMockMessage({
28 type: "agent",
Sean McCullough71941bd2025-04-18 13:31:48 -070029 content: "This is a test message",
Sean McCullough86b56862025-04-18 13:04:03 -070030 });
31
32 const el: SketchTimelineMessage = await fixture(html`
Sean McCullough71941bd2025-04-18 13:31:48 -070033 <sketch-timeline-message .message=${message}></sketch-timeline-message>
Sean McCullough86b56862025-04-18 13:04:03 -070034 `);
35
36 const messageContent = el.shadowRoot!.querySelector(".message-text");
37 expect(messageContent).to.exist;
Sean McCullough71941bd2025-04-18 13:31:48 -070038 expect(messageContent!.textContent!.trim()).to.include(
39 "This is a test message",
40 );
Sean McCullough86b56862025-04-18 13:04:03 -070041 });
42
43 it("renders with correct message type classes", async () => {
44 const messageTypes = ["user", "agent", "tool", "error"];
Sean McCullough71941bd2025-04-18 13:31:48 -070045
Sean McCullough86b56862025-04-18 13:04:03 -070046 for (const type of messageTypes) {
47 const message = createMockMessage({ type });
Sean McCullough71941bd2025-04-18 13:31:48 -070048
Sean McCullough86b56862025-04-18 13:04:03 -070049 const el: SketchTimelineMessage = await fixture(html`
Sean McCullough71941bd2025-04-18 13:31:48 -070050 <sketch-timeline-message .message=${message}></sketch-timeline-message>
Sean McCullough86b56862025-04-18 13:04:03 -070051 `);
Sean McCullough71941bd2025-04-18 13:31:48 -070052
Sean McCullough86b56862025-04-18 13:04:03 -070053 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({
Sean McCullough71941bd2025-04-18 13:31:48 -070061 end_of_turn: true,
Sean McCullough86b56862025-04-18 13:04:03 -070062 });
63
64 const el: SketchTimelineMessage = await fixture(html`
Sean McCullough71941bd2025-04-18 13:31:48 -070065 <sketch-timeline-message .message=${message}></sketch-timeline-message>
Sean McCullough86b56862025-04-18 13:04:03 -070066 `);
67
68 const messageElement = el.shadowRoot!.querySelector(".message");
69 expect(messageElement).to.exist;
70 expect(messageElement!.classList.contains("end-of-turn")).to.be.true;
71 });
72
73 it("formats timestamps correctly", async () => {
74 const message = createMockMessage({
Sean McCullough71941bd2025-04-18 13:31:48 -070075 timestamp: "2023-05-15T12:00:00Z",
Sean McCullough86b56862025-04-18 13:04:03 -070076 });
77
78 const el: SketchTimelineMessage = await fixture(html`
Sean McCullough71941bd2025-04-18 13:31:48 -070079 <sketch-timeline-message .message=${message}></sketch-timeline-message>
Sean McCullough86b56862025-04-18 13:04:03 -070080 `);
81
82 const timestamp = el.shadowRoot!.querySelector(".message-timestamp");
83 expect(timestamp).to.exist;
84 // Should include a formatted date like "May 15, 2023"
85 expect(timestamp!.textContent).to.include("May 15, 2023");
86 // Should include elapsed time
87 expect(timestamp!.textContent).to.include("(1.50s)");
88 });
89
90 it("renders markdown content correctly", async () => {
Sean McCullough71941bd2025-04-18 13:31:48 -070091 const markdownContent =
92 "# Heading\n\n- List item 1\n- List item 2\n\n`code block`";
Sean McCullough86b56862025-04-18 13:04:03 -070093 const message = createMockMessage({
Sean McCullough71941bd2025-04-18 13:31:48 -070094 content: markdownContent,
Sean McCullough86b56862025-04-18 13:04:03 -070095 });
96
97 const el: SketchTimelineMessage = await fixture(html`
Sean McCullough71941bd2025-04-18 13:31:48 -070098 <sketch-timeline-message .message=${message}></sketch-timeline-message>
Sean McCullough86b56862025-04-18 13:04:03 -070099 `);
100
101 const contentElement = el.shadowRoot!.querySelector(".markdown-content");
102 expect(contentElement).to.exist;
103 expect(contentElement!.innerHTML).to.include("<h1>Heading</h1>");
104 expect(contentElement!.innerHTML).to.include("<ul>");
105 expect(contentElement!.innerHTML).to.include("<li>List item 1</li>");
106 expect(contentElement!.innerHTML).to.include("<code>code block</code>");
107 });
108
109 it("displays usage information when available", async () => {
110 const usage: Usage = {
111 input_tokens: 150,
112 output_tokens: 300,
113 cost_usd: 0.025,
Sean McCullough71941bd2025-04-18 13:31:48 -0700114 cache_read_input_tokens: 50,
Sean McCullough86b56862025-04-18 13:04:03 -0700115 };
Sean McCullough71941bd2025-04-18 13:31:48 -0700116
Sean McCullough86b56862025-04-18 13:04:03 -0700117 const message = createMockMessage({
Sean McCullough71941bd2025-04-18 13:31:48 -0700118 usage,
Sean McCullough86b56862025-04-18 13:04:03 -0700119 });
120
121 const el: SketchTimelineMessage = await fixture(html`
Sean McCullough71941bd2025-04-18 13:31:48 -0700122 <sketch-timeline-message .message=${message}></sketch-timeline-message>
Sean McCullough86b56862025-04-18 13:04:03 -0700123 `);
124
125 const usageElement = el.shadowRoot!.querySelector(".message-usage");
126 expect(usageElement).to.exist;
Sean McCullough71941bd2025-04-18 13:31:48 -0700127 expect(usageElement!.textContent).to.include("150"); // In
128 expect(usageElement!.textContent).to.include("300"); // Out
129 expect(usageElement!.textContent).to.include("50"); // Cache
130 expect(usageElement!.textContent).to.include("$0.03"); // Cost
Sean McCullough86b56862025-04-18 13:04:03 -0700131 });
132
133 it("renders commit information correctly", async () => {
134 const commits: GitCommit[] = [
135 {
136 hash: "1234567890abcdef",
137 subject: "Fix bug in application",
138 body: "This fixes a major bug in the application\n\nSigned-off-by: Developer",
Sean McCullough71941bd2025-04-18 13:31:48 -0700139 pushed_branch: "main",
140 },
Sean McCullough86b56862025-04-18 13:04:03 -0700141 ];
Sean McCullough71941bd2025-04-18 13:31:48 -0700142
Sean McCullough86b56862025-04-18 13:04:03 -0700143 const message = createMockMessage({
Sean McCullough71941bd2025-04-18 13:31:48 -0700144 commits,
Sean McCullough86b56862025-04-18 13:04:03 -0700145 });
146
147 const el: SketchTimelineMessage = await fixture(html`
Sean McCullough71941bd2025-04-18 13:31:48 -0700148 <sketch-timeline-message .message=${message}></sketch-timeline-message>
Sean McCullough86b56862025-04-18 13:04:03 -0700149 `);
150
151 const commitsContainer = el.shadowRoot!.querySelector(".commits-container");
152 expect(commitsContainer).to.exist;
Sean McCullough71941bd2025-04-18 13:31:48 -0700153
Sean McCullough86b56862025-04-18 13:04:03 -0700154 const commitHeader = commitsContainer!.querySelector(".commits-header");
155 expect(commitHeader).to.exist;
Sean McCullough71941bd2025-04-18 13:31:48 -0700156 expect(commitHeader!.textContent).to.include("1 new");
157
Sean McCullough86b56862025-04-18 13:04:03 -0700158 const commitHash = commitsContainer!.querySelector(".commit-hash");
159 expect(commitHash).to.exist;
160 expect(commitHash!.textContent).to.equal("12345678"); // First 8 chars
Sean McCullough71941bd2025-04-18 13:31:48 -0700161
Sean McCullough86b56862025-04-18 13:04:03 -0700162 const pushedBranch = commitsContainer!.querySelector(".pushed-branch");
163 expect(pushedBranch).to.exist;
164 expect(pushedBranch!.textContent).to.include("main");
165 });
166
167 it("dispatches show-commit-diff event when commit diff button is clicked", async () => {
168 const commits: GitCommit[] = [
169 {
170 hash: "1234567890abcdef",
171 subject: "Fix bug in application",
172 body: "This fixes a major bug in the application",
Sean McCullough71941bd2025-04-18 13:31:48 -0700173 pushed_branch: "main",
174 },
Sean McCullough86b56862025-04-18 13:04:03 -0700175 ];
Sean McCullough71941bd2025-04-18 13:31:48 -0700176
Sean McCullough86b56862025-04-18 13:04:03 -0700177 const message = createMockMessage({
Sean McCullough71941bd2025-04-18 13:31:48 -0700178 commits,
Sean McCullough86b56862025-04-18 13:04:03 -0700179 });
180
181 const el: SketchTimelineMessage = await fixture(html`
Sean McCullough71941bd2025-04-18 13:31:48 -0700182 <sketch-timeline-message .message=${message}></sketch-timeline-message>
Sean McCullough86b56862025-04-18 13:04:03 -0700183 `);
184
Sean McCullough71941bd2025-04-18 13:31:48 -0700185 const diffButton = el.shadowRoot!.querySelector(
186 ".commit-diff-button",
187 ) as HTMLButtonElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700188 expect(diffButton).to.exist;
Sean McCullough71941bd2025-04-18 13:31:48 -0700189
Sean McCullough86b56862025-04-18 13:04:03 -0700190 // Set up listener for the event
191 setTimeout(() => diffButton!.click());
192 const { detail } = await oneEvent(el, "show-commit-diff");
Sean McCullough71941bd2025-04-18 13:31:48 -0700193
Sean McCullough86b56862025-04-18 13:04:03 -0700194 expect(detail).to.exist;
195 expect(detail.commitHash).to.equal("1234567890abcdef");
196 });
197
198 it("handles message type icon display correctly", async () => {
199 // First message of a type should show icon
200 const firstMessage = createMockMessage({
201 type: "user",
Sean McCullough71941bd2025-04-18 13:31:48 -0700202 idx: 0,
Sean McCullough86b56862025-04-18 13:04:03 -0700203 });
Sean McCullough71941bd2025-04-18 13:31:48 -0700204
Sean McCullough86b56862025-04-18 13:04:03 -0700205 // Second message of same type should not show icon
206 const secondMessage = createMockMessage({
207 type: "user",
Sean McCullough71941bd2025-04-18 13:31:48 -0700208 idx: 1,
Sean McCullough86b56862025-04-18 13:04:03 -0700209 });
210
211 // Test first message (should show icon)
212 const firstEl: SketchTimelineMessage = await fixture(html`
213 <sketch-timeline-message
214 .message=${firstMessage}
215 ></sketch-timeline-message>
216 `);
217
218 const firstIcon = firstEl.shadowRoot!.querySelector(".message-icon");
219 expect(firstIcon).to.exist;
220 expect(firstIcon!.textContent!.trim()).to.equal("U");
221
222 // Test second message with previous message of same type
223 const secondEl: SketchTimelineMessage = await fixture(html`
224 <sketch-timeline-message
225 .message=${secondMessage}
226 .previousMessage=${firstMessage}
227 ></sketch-timeline-message>
228 `);
229
230 const secondIcon = secondEl.shadowRoot!.querySelector(".message-icon");
231 expect(secondIcon).to.not.exist;
232 });
233
234 it("formats numbers correctly", async () => {
235 const el: SketchTimelineMessage = await fixture(html`
236 <sketch-timeline-message></sketch-timeline-message>
237 `);
238
239 // Test accessing private method via the component instance
240 expect(el.formatNumber(1000)).to.equal("1,000");
241 expect(el.formatNumber(null, "N/A")).to.equal("N/A");
242 expect(el.formatNumber(undefined, "--")).to.equal("--");
243 });
244
245 it("formats currency values correctly", async () => {
246 const el: SketchTimelineMessage = await fixture(html`
247 <sketch-timeline-message></sketch-timeline-message>
248 `);
249
250 // Test with different precisions
251 expect(el.formatCurrency(10.12345, "$0.00", true)).to.equal("$10.1235"); // message level (4 decimals)
252 expect(el.formatCurrency(10.12345, "$0.00", false)).to.equal("$10.12"); // total level (2 decimals)
253 expect(el.formatCurrency(null, "N/A")).to.equal("N/A");
254 expect(el.formatCurrency(undefined, "--")).to.equal("--");
255 });
256});