import assert from "node:assert/strict" import { readFile } from "node:fs/promises" import test from "node:test" import ts from "typescript" import vm from "node:vm" function createElement(tagName) { return { tagName, children: [], dataset: {}, style: {}, attributes: {}, listeners: {}, parentNode: null, contentWindow: {}, setAttribute(name, value) { this.attributes[name] = String(value) }, appendChild(child) { child.parentNode = this this.children.push(child) return child }, removeChild(child) { this.children = this.children.filter((item) => item !== child) child.parentNode = null return child }, addEventListener(type, handler) { this.listeners[type] = handler }, click() { this.listeners.click?.() }, } } async function loadSdk(config) { const source = await readFile(new URL("./agent-desk-sdk.ts", import.meta.url), "utf8") const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.ESNext, importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove, }, fileName: "agent-desk-sdk.ts", }) const body = createElement("body") const sandbox = { URL, console, fetch: async () => ({ json: async () => ({ success: true, data: { title: "\u5728\u7ebf\u5ba2\u670d", themeColor: "#2563eb", }, }), }), document: { body, currentScript: { src: "https://chat.example/sdk/agent-desk-sdk.min.js", }, createElement, createElementNS: (_namespace, tagName) => createElement(tagName), }, window: { AgentDeskConfig: config, location: { origin: "https://host.example", }, addEventListener() {}, clearTimeout() {}, setTimeout(handler) { handler() return 1 }, }, } sandbox.window.window = sandbox.window sandbox.window.document = sandbox.document sandbox.window.fetch = sandbox.fetch sandbox.window.console = console sandbox.window.URL = URL sandbox.window.setTimeout = sandbox.window.setTimeout sandbox.window.clearTimeout = sandbox.window.clearTimeout const compiledCode = compiled.outputText.replace(/\nexport\s*\{\};?\s*$/, "") vm.runInNewContext(compiledCode, sandbox) await Promise.resolve() await Promise.resolve() return sandbox } async function flushPromises(count = 5) { for (let i = 0; i < count; i += 1) { await Promise.resolve() } } test("getChatUrl resolves a fresh userToken for each call", async () => { let calls = 0 const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", getUserToken: async () => `token_${++calls}`, }) assert.equal(typeof sandbox.window.AgentDeskWidget.getChatUrl, "function") const first = await sandbox.window.AgentDeskWidget.getChatUrl() const second = await sandbox.window.AgentDeskWidget.getChatUrl() assert.equal(new URL(first).pathname, "/support/chat/") assert.equal(new URL(second).pathname, "/support/chat/") assert.equal(new URL(first).searchParams.get("userToken"), "token_1") assert.equal(new URL(second).searchParams.get("userToken"), "token_2") assert.equal(calls, 2) }) test("launcher click creates chat iframe with a freshly resolved userToken", async () => { const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", getUserToken: async () => "click_token", }) await flushPromises() const launcher = sandbox.document.body.children.find( (child) => child.dataset.agentDeskWidget === "launcher" ) assert.ok(launcher) launcher.click() await flushPromises() const frame = sandbox.document.body.children.find( (child) => child.dataset.agentDeskWidget === "frame" ) assert.ok(frame) assert.equal(new URL(frame.src).pathname, "/support/chat/") assert.equal(new URL(frame.src).searchParams.get("userToken"), "click_token") }) test("chat iframe layout uses dynamic viewport height for iOS browser chrome", async () => { const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", }) await flushPromises() const launcher = sandbox.document.body.children.find( (child) => child.dataset.agentDeskWidget === "launcher" ) launcher.click() await flushPromises() const frame = sandbox.document.body.children.find( (child) => child.dataset.agentDeskWidget === "frame" ) assert.ok(frame) assert.match(frame.style.height, /100dvh/) assert.match(frame.style.maxWidth, /100vw/) })