diff --git a/src/client/src/components/ChatView.image.test.ts b/src/client/src/components/ChatView.image.test.ts
new file mode 100644
index 0000000..50929f7
--- /dev/null
+++ b/src/client/src/components/ChatView.image.test.ts
@@ -0,0 +1,79 @@
+import type { TemplateResult } from "lit";
+import { describe, expect, it } from "vitest";
+import type { ChatLine } from "./shared";
+import { ChatView } from "./ChatView";
+
+describe("ChatView image rendering", () => {
+ // Direct handler extraction keeps this node-environment test focused on the
+ // late image-load scroll wiring without introducing a component-wide DOM shim.
+ it("renders native image data and re-pins late loads only while already pinned", () => {
+ const view = new ChatView();
+ let scrollCalls = 0;
+ if (!Reflect.set(view, "scrollToBottom", () => { scrollCalls += 1; })) throw new Error("Could not observe ChatView.scrollToBottom");
+ const rendered = renderPart(view, { type: "image", mimeType: "image/png", data: "QUJD" });
+ const onLoad = templateEventHandler(rendered, "@load=");
+
+ expect(templateStaticMarkup(rendered)).toContain("
TemplateResult;
+type TemplateEventHandler = (event: Event) => void;
+
+function renderPart(view: ChatView, part: ChatLine["parts"][number], message?: ChatLine): TemplateResult {
+ const method: unknown = Reflect.get(view, "renderPart");
+ if (!isRenderPart(method)) throw new Error("ChatView.renderPart is not callable");
+ return method.call(view, part, message);
+}
+
+function templateEventHandler(template: TemplateResult, marker: string): TemplateEventHandler {
+ const strings = templateStrings(template);
+ const values = templateValues(template);
+ for (let index = 0; index < values.length; index += 1) {
+ const value = values[index];
+ if (strings[index]?.includes(marker) === true && isTemplateEventHandler(value)) return value;
+ }
+ throw new Error(`Expected template event handler after ${marker}`);
+}
+
+function isRenderPart(value: unknown): value is RenderPart {
+ return typeof value === "function";
+}
+
+function isTemplateEventHandler(value: unknown): value is TemplateEventHandler {
+ return typeof value === "function";
+}
+
+function templateStaticMarkup(template: TemplateResult): string {
+ return templateStrings(template).join("");
+}
+
+function templateValuesAfterMarker(template: TemplateResult, marker: string): unknown[] {
+ const strings = templateStrings(template);
+ return templateValues(template).filter((_, index) => strings[index]?.includes(marker) === true);
+}
+
+function templateStrings(template: TemplateResult): readonly string[] {
+ const strings = Reflect.get(template, "strings");
+ if (!isStringArray(strings)) throw new Error("TemplateResult strings were unavailable");
+ return strings;
+}
+
+function templateValues(template: TemplateResult): readonly unknown[] {
+ const values = Reflect.get(template, "values");
+ if (!Array.isArray(values)) throw new Error("TemplateResult values were unavailable");
+ return values.map((value: unknown) => value);
+}
+
+function isStringArray(value: unknown): value is string[] {
+ return Array.isArray(value) && value.every((item: unknown) => typeof item === "string");
+}
diff --git a/src/client/src/components/ChatView.test.ts b/src/client/src/components/ChatView.test.ts
index d3206f5..df3b167 100644
--- a/src/client/src/components/ChatView.test.ts
+++ b/src/client/src/components/ChatView.test.ts
@@ -38,29 +38,6 @@ describe("chatMessageMetadataLabel", () => {
});
});
-describe("ChatView image rendering", () => {
- // Direct handler extraction keeps this node-environment test focused on the
- // late image-load scroll wiring without introducing a component-wide DOM shim.
- it("renders native image data and re-pins late loads only while already pinned", () => {
- const view = new ChatView();
- let scrollCalls = 0;
- if (!Reflect.set(view, "scrollToBottom", () => { scrollCalls += 1; })) throw new Error("Could not observe ChatView.scrollToBottom");
- const rendered = renderPart(view, { type: "image", mimeType: "image/png", data: "QUJD" });
- const onLoad = templateEventHandler(rendered, "@load=");
-
- expect(templateStaticMarkup(rendered)).toContain("
{
const messages: ChatLine[] = [
{ role: "assistant", parts: [{ type: "toolCall", toolName: "read", summary: "inspect a file" }] },
@@ -126,17 +103,10 @@ interface GroupBodyRenderCall {
startIndex: number;
}
-type RenderPart = (this: ChatView, part: ChatLine["parts"][number], message?: ChatLine) => TemplateResult;
type RenderMessageGroup = (this: ChatView, messages: ChatLine[], startIndex: number, endIndex: number, defaultOpen: boolean) => TemplateResult;
type RenderMessageGroupBody = (this: ChatView, messages: ChatLine[], startIndex: number) => TemplateResult;
type TemplateEventHandler = (event: Event) => void;
-function renderPart(view: ChatView, part: ChatLine["parts"][number], message?: ChatLine): TemplateResult {
- const method: unknown = Reflect.get(view, "renderPart");
- if (!isRenderPart(method)) throw new Error("ChatView.renderPart is not callable");
- return method.call(view, part, message);
-}
-
function renderMessageGroup(view: ChatView, messages: ChatLine[], startIndex: number, endIndex: number, defaultOpen: boolean): TemplateResult {
const method: unknown = Reflect.get(view, "renderMessageGroup");
if (!isRenderMessageGroup(method)) throw new Error("ChatView.renderMessageGroup is not callable");
@@ -155,10 +125,6 @@ function observeGroupBodyRenders(view: ChatView): GroupBodyRenderCall[] {
return calls;
}
-function isRenderPart(value: unknown): value is RenderPart {
- return typeof value === "function";
-}
-
function isRenderMessageGroup(value: unknown): value is RenderMessageGroup {
return typeof value === "function";
}