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 (url) => ({ json: async () => String(url).endsWith("/api/config") ? { success: true, data: { language: "en-US", }, } : { success: true, data: { 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() {}, removeEventListener() {}, clearTimeout() {}, setTimeout(handler, delay) { if (!delay || delay < 1000) { 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 = 30) { 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(new URL(first).searchParams.get("parentOrigin"), "https://host.example") assert.equal(calls, 2) }) test("getChatUrl forwards a signed externalId with its timestamp", async () => { const seen = [] const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", externalId: "u_10001", externalName: "Ivan", signExternalId: async (externalId) => { seen.push(externalId) return { signature: `sig_${seen.length}`, issuedAt: 1700000000 + seen.length } }, }) const first = await sandbox.window.AgentDeskWidget.getChatUrl() const second = await sandbox.window.AgentDeskWidget.getChatUrl() assert.equal(new URL(first).searchParams.get("externalId"), "u_10001") assert.equal(new URL(first).searchParams.get("externalIdSignature"), "sig_1") assert.equal(new URL(first).searchParams.get("externalIdSignedAt"), "1700000001") assert.equal(new URL(second).searchParams.get("externalIdSignature"), "sig_2") assert.equal(new URL(second).searchParams.get("externalIdSignedAt"), "1700000002") assert.deepEqual(seen, ["u_10001", "u_10001"]) }) test("no signer means no identity parameters", async () => { const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", externalId: "u_10001", }) const url = await sandbox.window.AgentDeskWidget.getChatUrl() assert.equal(new URL(url).searchParams.get("externalId"), "u_10001") assert.equal(new URL(url).searchParams.has("externalIdSignature"), false) assert.equal(new URL(url).searchParams.has("externalIdSignedAt"), false) }) test("signer without an externalId is not called", async () => { let calls = 0 const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", signExternalId: async () => { calls += 1 return { signature: "sig", issuedAt: 1700000000 } }, }) const url = await sandbox.window.AgentDeskWidget.getChatUrl() assert.equal(calls, 0) assert.equal(new URL(url).searchParams.has("externalIdSignature"), false) }) // A signature expires, so reopening on a long-lived host page must hand the frame // a fresh one instead of leaving the stale pair in its URL. test("reopening reloads the frame when a fresh signature was issued", async () => { let issued = 0 const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", externalId: "u_10001", signExternalId: async () => { issued += 1 return { signature: `sig_${issued}`, issuedAt: 1700000000 + issued } }, }) await flushPromises() const widget = sandbox.window.AgentDeskWidget await widget.open() const frame = sandbox.document.body.children.find( (child) => child.dataset.agentDeskWidget === "frame" ) assert.ok(frame, "expected the launcher click to create the frame") const firstSrc = frame.src assert.ok(firstSrc.includes("externalIdSignature=sig_1"), `frame src = ${firstSrc}`) widget.close() await widget.open() const reloaded = sandbox.document.body.children.find( (child) => child.dataset.agentDeskWidget === "frame" ) assert.ok(reloaded.src.includes("externalIdSignature=sig_2"), `frame src after reopen = ${reloaded.src}`) assert.notEqual(reloaded.src, firstSrc) }) 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) assert.equal(launcher.children.at(-1)?.textContent, "Support") 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("requireUserToken rejects opening when getUserToken is missing or empty", async () => { const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", requireUserToken: true, getUserToken: async () => " ", }) await flushPromises() await assert.rejects( () => sandbox.window.AgentDeskWidget.open(), /Support user token is required/ ) const frame = sandbox.document.body.children.find( (child) => child.dataset.agentDeskWidget === "frame" ) assert.equal(frame, undefined) }) test("ru-RU language configures Russian launcher and default title", async () => { const sandbox = await loadSdk({ channelId: "ch_1", baseUrl: "https://api.example", language: "ru-RU", }) await flushPromises() const launcher = sandbox.document.body.children.find( (child) => child.dataset.agentDeskWidget === "launcher" ) assert.ok(launcher) assert.equal(launcher.attributes["aria-label"], "Поддержка") assert.equal(launcher.children.at(-1)?.textContent, "Чат") }) 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/) assert.equal(frame.style.borderRadius, "16px") })