| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 1 | import { html, fixture, expect, oneEvent } from "@open-wc/testing"; |
| 2 | import "./sketch-timeline-message"; |
| 3 | import type { SketchTimelineMessage } from "./sketch-timeline-message"; |
| 4 | import { TimelineMessage, ToolCall, GitCommit, Usage } from "../types"; |
| 5 | |
| 6 | describe("SketchTimelineMessage", () => { |
| 7 | // Helper function to create mock timeline messages |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 8 | function createMockMessage( |
| 9 | props: Partial<TimelineMessage> = {}, |
| 10 | ): TimelineMessage { |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 11 | 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 McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 22 | ...props, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 23 | }; |
| 24 | } |
| 25 | |
| 26 | it("renders with basic message content", async () => { |
| 27 | const message = createMockMessage({ |
| 28 | type: "agent", |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 29 | content: "This is a test message", |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 30 | }); |
| 31 | |
| 32 | const el: SketchTimelineMessage = await fixture(html` |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 33 | <sketch-timeline-message .message=${message}></sketch-timeline-message> |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 34 | `); |
| 35 | |
| 36 | const messageContent = el.shadowRoot!.querySelector(".message-text"); |
| 37 | expect(messageContent).to.exist; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 38 | expect(messageContent!.textContent!.trim()).to.include( |
| 39 | "This is a test message", |
| 40 | ); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 41 | }); |
| 42 | |
| 43 | it("renders with correct message type classes", async () => { |
| 44 | const messageTypes = ["user", "agent", "tool", "error"]; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 45 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 46 | for (const type of messageTypes) { |
| 47 | const message = createMockMessage({ type }); |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 48 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 49 | const el: SketchTimelineMessage = await fixture(html` |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 50 | <sketch-timeline-message .message=${message}></sketch-timeline-message> |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 51 | `); |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 52 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 53 | 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 McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 61 | end_of_turn: true, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 62 | }); |
| 63 | |
| 64 | const el: SketchTimelineMessage = await fixture(html` |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 65 | <sketch-timeline-message .message=${message}></sketch-timeline-message> |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 66 | `); |
| 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 McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 75 | timestamp: "2023-05-15T12:00:00Z", |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 76 | }); |
| 77 | |
| 78 | const el: SketchTimelineMessage = await fixture(html` |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 79 | <sketch-timeline-message .message=${message}></sketch-timeline-message> |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 80 | `); |
| 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 McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 91 | const markdownContent = |
| 92 | "# Heading\n\n- List item 1\n- List item 2\n\n`code block`"; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 93 | const message = createMockMessage({ |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 94 | content: markdownContent, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 95 | }); |
| 96 | |
| 97 | const el: SketchTimelineMessage = await fixture(html` |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 98 | <sketch-timeline-message .message=${message}></sketch-timeline-message> |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 99 | `); |
| 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 McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 114 | cache_read_input_tokens: 50, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 115 | }; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 116 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 117 | const message = createMockMessage({ |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 118 | usage, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 119 | }); |
| 120 | |
| 121 | const el: SketchTimelineMessage = await fixture(html` |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 122 | <sketch-timeline-message .message=${message}></sketch-timeline-message> |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 123 | `); |
| 124 | |
| 125 | const usageElement = el.shadowRoot!.querySelector(".message-usage"); |
| 126 | expect(usageElement).to.exist; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 127 | 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 McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 131 | }); |
| 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 McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 139 | pushed_branch: "main", |
| 140 | }, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 141 | ]; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 142 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 143 | const message = createMockMessage({ |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 144 | commits, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 145 | }); |
| 146 | |
| 147 | const el: SketchTimelineMessage = await fixture(html` |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 148 | <sketch-timeline-message .message=${message}></sketch-timeline-message> |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 149 | `); |
| 150 | |
| 151 | const commitsContainer = el.shadowRoot!.querySelector(".commits-container"); |
| 152 | expect(commitsContainer).to.exist; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 153 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 154 | const commitHeader = commitsContainer!.querySelector(".commits-header"); |
| 155 | expect(commitHeader).to.exist; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 156 | expect(commitHeader!.textContent).to.include("1 new"); |
| 157 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 158 | const commitHash = commitsContainer!.querySelector(".commit-hash"); |
| 159 | expect(commitHash).to.exist; |
| 160 | expect(commitHash!.textContent).to.equal("12345678"); // First 8 chars |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 161 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 162 | 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 McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 173 | pushed_branch: "main", |
| 174 | }, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 175 | ]; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 176 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 177 | const message = createMockMessage({ |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 178 | commits, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 179 | }); |
| 180 | |
| 181 | const el: SketchTimelineMessage = await fixture(html` |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 182 | <sketch-timeline-message .message=${message}></sketch-timeline-message> |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 183 | `); |
| 184 | |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 185 | const diffButton = el.shadowRoot!.querySelector( |
| 186 | ".commit-diff-button", |
| 187 | ) as HTMLButtonElement; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 188 | expect(diffButton).to.exist; |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 189 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 190 | // Set up listener for the event |
| 191 | setTimeout(() => diffButton!.click()); |
| 192 | const { detail } = await oneEvent(el, "show-commit-diff"); |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 193 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 194 | 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 McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 202 | idx: 0, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 203 | }); |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 204 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 205 | // Second message of same type should not show icon |
| 206 | const secondMessage = createMockMessage({ |
| 207 | type: "user", |
| Sean McCullough | 71941bd | 2025-04-18 13:31:48 -0700 | [diff] [blame] | 208 | idx: 1, |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 209 | }); |
| 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 | }); |