Raw
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");