blob: d768f02df5972dcc84e0514827f68549e9559944 [file] [log] [blame]
import { html, fixture, expect, oneEvent } from "@open-wc/testing";
import "./sketch-timeline-message";
import type { SketchTimelineMessage } from "./sketch-timeline-message";
import { TimelineMessage, ToolCall, GitCommit, Usage } from "../types";
describe("SketchTimelineMessage", () => {
// Helper function to create mock timeline messages
function createMockMessage(
props: Partial<TimelineMessage> = {},
): TimelineMessage {
return {
idx: props.idx || 0,
type: props.type || "agent",
content: props.content || "Hello world",
timestamp: props.timestamp || "2023-05-15T12:00:00Z",
elapsed: props.elapsed || 1500000000, // 1.5 seconds in nanoseconds
end_of_turn: props.end_of_turn || false,
conversation_id: props.conversation_id || "conv123",
tool_calls: props.tool_calls || [],
commits: props.commits || [],
usage: props.usage,
...props,
};
}
it("renders with basic message content", async () => {
const message = createMockMessage({
type: "agent",
content: "This is a test message",
});
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const messageContent = el.shadowRoot!.querySelector(".message-text");
expect(messageContent).to.exist;
expect(messageContent!.textContent!.trim()).to.include(
"This is a test message",
);
});
it("renders with correct message type classes", async () => {
const messageTypes = ["user", "agent", "tool", "error"];
for (const type of messageTypes) {
const message = createMockMessage({ type });
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const messageElement = el.shadowRoot!.querySelector(".message");
expect(messageElement).to.exist;
expect(messageElement!.classList.contains(type)).to.be.true;
}
});
it("renders end-of-turn marker correctly", async () => {
const message = createMockMessage({
end_of_turn: true,
});
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const messageElement = el.shadowRoot!.querySelector(".message");
expect(messageElement).to.exist;
expect(messageElement!.classList.contains("end-of-turn")).to.be.true;
});
it("formats timestamps correctly", async () => {
const message = createMockMessage({
timestamp: "2023-05-15T12:00:00Z",
});
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const timestamp = el.shadowRoot!.querySelector(".message-timestamp");
expect(timestamp).to.exist;
// Should include a formatted date like "May 15, 2023"
expect(timestamp!.textContent).to.include("May 15, 2023");
// Should include elapsed time
expect(timestamp!.textContent).to.include("(1.50s)");
});
it("renders markdown content correctly", async () => {
const markdownContent =
"# Heading\n\n- List item 1\n- List item 2\n\n`code block`";
const message = createMockMessage({
content: markdownContent,
});
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const contentElement = el.shadowRoot!.querySelector(".markdown-content");
expect(contentElement).to.exist;
expect(contentElement!.innerHTML).to.include("<h1>Heading</h1>");
expect(contentElement!.innerHTML).to.include("<ul>");
expect(contentElement!.innerHTML).to.include("<li>List item 1</li>");
expect(contentElement!.innerHTML).to.include("<code>code block</code>");
});
it("displays usage information when available", async () => {
const usage: Usage = {
input_tokens: 150,
output_tokens: 300,
cost_usd: 0.025,
cache_read_input_tokens: 50,
};
const message = createMockMessage({
usage,
});
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const usageElement = el.shadowRoot!.querySelector(".message-usage");
expect(usageElement).to.exist;
expect(usageElement!.textContent).to.include("150"); // In
expect(usageElement!.textContent).to.include("300"); // Out
expect(usageElement!.textContent).to.include("50"); // Cache
expect(usageElement!.textContent).to.include("$0.03"); // Cost
});
it("renders commit information correctly", async () => {
const commits: GitCommit[] = [
{
hash: "1234567890abcdef",
subject: "Fix bug in application",
body: "This fixes a major bug in the application\n\nSigned-off-by: Developer",
pushed_branch: "main",
},
];
const message = createMockMessage({
commits,
});
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const commitsContainer = el.shadowRoot!.querySelector(".commits-container");
expect(commitsContainer).to.exist;
const commitHeader = commitsContainer!.querySelector(".commits-header");
expect(commitHeader).to.exist;
expect(commitHeader!.textContent).to.include("1 new");
const commitHash = commitsContainer!.querySelector(".commit-hash");
expect(commitHash).to.exist;
expect(commitHash!.textContent).to.equal("12345678"); // First 8 chars
const pushedBranch = commitsContainer!.querySelector(".pushed-branch");
expect(pushedBranch).to.exist;
expect(pushedBranch!.textContent).to.include("main");
});
it("dispatches show-commit-diff event when commit diff button is clicked", async () => {
const commits: GitCommit[] = [
{
hash: "1234567890abcdef",
subject: "Fix bug in application",
body: "This fixes a major bug in the application",
pushed_branch: "main",
},
];
const message = createMockMessage({
commits,
});
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const diffButton = el.shadowRoot!.querySelector(
".commit-diff-button",
) as HTMLButtonElement;
expect(diffButton).to.exist;
// Set up listener for the event
setTimeout(() => diffButton!.click());
const { detail } = await oneEvent(el, "show-commit-diff");
expect(detail).to.exist;
expect(detail.commitHash).to.equal("1234567890abcdef");
});
it("handles message type icon display correctly", async () => {
// First message of a type should show icon
const firstMessage = createMockMessage({
type: "user",
idx: 0,
});
// Second message of same type should not show icon
const secondMessage = createMockMessage({
type: "user",
idx: 1,
});
// Test first message (should show icon)
const firstEl: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message
.message=${firstMessage}
></sketch-timeline-message>
`);
const firstIcon = firstEl.shadowRoot!.querySelector(".message-icon");
expect(firstIcon).to.exist;
expect(firstIcon!.textContent!.trim()).to.equal("U");
// Test second message with previous message of same type
const secondEl: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message
.message=${secondMessage}
.previousMessage=${firstMessage}
></sketch-timeline-message>
`);
const secondIcon = secondEl.shadowRoot!.querySelector(".message-icon");
expect(secondIcon).to.not.exist;
});
it("formats numbers correctly", async () => {
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message></sketch-timeline-message>
`);
// Test accessing private method via the component instance
expect(el.formatNumber(1000)).to.equal("1,000");
expect(el.formatNumber(null, "N/A")).to.equal("N/A");
expect(el.formatNumber(undefined, "--")).to.equal("--");
});
it("formats currency values correctly", async () => {
const el: SketchTimelineMessage = await fixture(html`
<sketch-timeline-message></sketch-timeline-message>
`);
// Test with different precisions
expect(el.formatCurrency(10.12345, "$0.00", true)).to.equal("$10.1235"); // message level (4 decimals)
expect(el.formatCurrency(10.12345, "$0.00", false)).to.equal("$10.12"); // total level (2 decimals)
expect(el.formatCurrency(null, "N/A")).to.equal("N/A");
expect(el.formatCurrency(undefined, "--")).to.equal("--");
});
});