// @vitest-environment jsdom
import { describe, it, expect } from "vitest";
import { morphMessages } from "./morph";
function makeContainer(html: string): HTMLElement {
const div = document.createElement("div");
div.innerHTML = html;
return div;
}
function makeNewContent(html: string): HTMLElement {
const div = document.createElement("div");
div.innerHTML = html;
return div;
}
describe("morphMessages", () => {
describe("data-preserve-animation", () => {
it("preserves animated element when old and new both have data-preserve-animation with same text", () => {
const container = makeContainer(
'Calling tool... 0.1s'
);
const oldSpan = container.querySelector("span")!;
morphMessages(
container,
makeNewContent(
'Calling tool... 0.1s'
)
);
expect(container.querySelector("span")).toBe(oldSpan);
});
it("allows morph when new node drops data-preserve-animation (tool completed)", () => {
const container = makeContainer(
'Calling tool... 0.5s'
);
morphMessages(
container,
makeNewContent("Finished tool 0.5s")
);
expect(container.querySelector("span")!.textContent).toBe(
"Finished tool 0.5s"
);
expect(
container.querySelector("span")!.hasAttribute("data-preserve-animation")
).toBe(false);
});
it("allows morph when text content changes despite both having data-preserve-animation", () => {
const container = makeContainer(
'Calling tool... 0.1s'
);
morphMessages(
container,
makeNewContent(
'Calling UCP Search Catalog... 0.2s'
)
);
expect(container.querySelector("span")!.textContent).toBe(
"Calling UCP Search Catalog... 0.2s"
);
});
it("does not preserve when preserveTypingAnimation is false", () => {
const container = makeContainer(
'Old text'
);
morphMessages(
container,
makeNewContent(
'New text'
),
{ preserveTypingAnimation: false }
);
expect(container.querySelector("span")!.textContent).toBe("New text");
});
});
describe("data-tool-elapsed (live duration counter)", () => {
it("preserves the live span's timer-owned text while the same tool is active", () => {
// The 100ms global timer wrote "0.3s"; a re-render arrives carrying the
// render-time value "<0.1s". Morphing it in would make the boundary
// flicker, so the still-live span (same startedAt) must be left alone.
const container = makeContainer(
'0.3s'
);
const oldSpan = container.querySelector("span")!;
morphMessages(
container,
makeNewContent('<0.1s')
);
expect(container.querySelector("span")).toBe(oldSpan);
expect(container.querySelector("span")!.textContent).toBe("0.3s");
});
it("allows morph to the final static duration once the tool completes", () => {
const container = makeContainer(
'0.7s'
);
morphMessages(container, makeNewContent("0.8s"));
const span = container.querySelector("span")!;
expect(span.textContent).toBe("0.8s");
expect(span.hasAttribute("data-tool-elapsed")).toBe(false);
});
it("allows morph when the slot is reused by a different tool (startedAt changed)", () => {
const container = makeContainer(
'0.5s'
);
morphMessages(
container,
makeNewContent('<0.1s')
);
const span = container.querySelector("span")!;
expect(span.getAttribute("data-tool-elapsed")).toBe("2000");
expect(span.textContent).toBe("<0.1s");
});
});
});