1
+#!/usr/bin/env node
2
+// Headless test for the webview's tool-call rendering (media/main.js).
3
+//
4
+// Loads the real webview script against a minimal fake DOM and replays a
5
+// tool_call -> tool_call_update sequence (the model-download flow), asserting
6
+// that updates sharing a toolCallId mutate ONE row in place (with a progress
7
+// bar) instead of stacking new bubbles.
8
+//
9
+// Run: node test/webview.mjs
10
+
11
+import { readFileSync } from "node:fs";
12
+import { fileURLToPath } from "node:url";
13
+import { dirname, join } from "node:path";
14
+
15
+const __dirname = dirname(fileURLToPath(import.meta.url));
16
+const src = readFileSync(join(__dirname, "..", "media", "main.js"), "utf8");
17
+
18
+function makeEl() {
19
+ return {
20
+ className: "",
21
+ hidden: false,
22
+ style: {},
23
+ textContent: "",
24
+ children: [],
25
+ value: "",
26
+ disabled: false,
27
+ scrollTop: 0,
28
+ scrollHeight: 0,
29
+ appendChild(c) {
30
+ this.children.push(c);
31
+ return c;
32
+ },
33
+ addEventListener() {},
34
+ _innerHTML: "",
35
+ set innerHTML(v) {
36
+ this._innerHTML = v;
37
+ if (!v) {
38
+ this.children = [];
39
+ }
40
+ },
41
+ get innerHTML() {
42
+ return this._innerHTML;
43
+ }
44
+ };
45
+}
46
+
47
+const messagesEl = makeEl();
48
+const byId = {
49
+ messages: messagesEl,
50
+ status: makeEl(),
51
+ composer: makeEl(),
52
+ input: makeEl(),
53
+ send: makeEl()
54
+};
55
+
56
+let messageHandler = null;
57
+globalThis.acquireVsCodeApi = () => ({ postMessage() {}, getState() {}, setState() {} });
58
+globalThis.document = {
59
+ getElementById: (id) => byId[id],
60
+ createElement: () => makeEl()
61
+};
62
+globalThis.window = {
63
+ addEventListener: (type, fn) => {
64
+ if (type === "message") {
65
+ messageHandler = fn;
66
+ }
67
+ }
68
+};
69
+
70
+// Run the real webview IIFE in this faked global context.
71
+(0, eval)(src);
72
+
73
+function send(msg) {
74
+ messageHandler({ data: msg });
75
+}
76
+function toolRows() {
77
+ return messagesEl.children.filter((c) => c.className.includes("message-tool"));
78
+}
79
+function assert(cond, msg) {
80
+ if (!cond) {
81
+ console.error(`FAIL: ${msg}`);
82
+ process.exit(1);
83
+ }
84
+}
85
+
86
+assert(typeof messageHandler === "function", "webview registered a message handler");
87
+
88
+// Same toolCallId across four updates -> exactly one row.
89
+send({ type: "tool", toolCallId: "tc1", title: "Downloading Qwen 2.5 3B", status: "in_progress" });
90
+send({ type: "tool", toolCallId: "tc1", title: "Downloading Qwen 2.5 3B (~1.80 GB) (0%)", status: "in_progress" });
91
+send({ type: "tool", toolCallId: "tc1", title: "Downloading Qwen 2.5 3B (~1.80 GB) (50%)", status: "in_progress" });
92
+send({ type: "tool", toolCallId: "tc1", status: "failed" }); // no title -> keep prior label
93
+
94
+assert(toolRows().length === 1, `4 updates should yield 1 row, got ${toolRows().length}`);
95
+console.log(`OK in-place: 4 updates -> ${toolRows().length} row`);
96
+
97
+const row = toolRows()[0];
98
+assert(row.className.includes("tool-failed"), `final status class was "${row.className}"`);
99
+console.log("OK final status -> tool-failed");
100
+
101
+const bubble = row.children[0];
102
+const labelEl = bubble.children[0];
103
+const bar = bubble.children[1].children[0];
104
+assert(
105
+ labelEl.textContent === "Downloading Qwen 2.5 3B (~1.80 GB) — failed",
106
+ `label was "${labelEl.textContent}"`
107
+);
108
+console.log(`OK label updated in place: "${labelEl.textContent}"`);
109
+assert(bar.style.width === "50%", `progress width was "${bar.style.width}"`);
110
+console.log(`OK progress bar at ${bar.style.width}`);
111
+
112
+// A different toolCallId makes a new row.
113
+send({ type: "tool", toolCallId: "tc2", title: "Reading file", status: "completed" });
114
+assert(toolRows().length === 2, `distinct id should add a row, got ${toolRows().length}`);
115
+console.log("OK distinct toolCallId -> new row");
116
+
117
+// clear() resets tracking so ids can be reused cleanly.
118
+send({ type: "clear" });
119
+send({ type: "tool", toolCallId: "tc1", title: "Again", status: "in_progress" });
120
+assert(toolRows().length === 1, `after clear expected 1 row, got ${toolRows().length}`);
121
+console.log("OK clear resets tool tracking");
122
+
123
+console.log("\nALL WEBVIEW CHECKS PASSED");