import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { HTML_PREVIEW_CSP, configureHtmlPreviewFrame, htmlPreviewDocument, } from "../web/static/js/preview_content.js"; // preview.js has browser-only top-level bindings, so exercise the integration contract // through its shipped source and markup while keeping this test dependency-free. const previewJs = readFileSync(new URL("../web/static/js/preview.js", import.meta.url), "utf8"); const pageHtml = readFileSync(new URL("../web/static/dev.html", import.meta.url), "utf8"); const chatJs = readFileSync(new URL("../web/static/js/chat.js", import.meta.url), "utf8"); test("HTML is a renderable category rather than plain text", () => { assert.match(previewJs, /html:\s+new Set\(\["html","htm"\]\)/); assert.match(previewJs, /configureHtmlPreviewFrame\(frame, text\)/); }); test("HTML preview allows HTTPS scripts while blocking host privileges and navigation", () => { const document = htmlPreviewDocument("xx"); assert.match(document, /")); const attrs = {}; const frame = { setAttribute(name, value) { attrs[name] = value; } }; configureHtmlPreviewFrame(frame, "

x

"); assert.equal(attrs.sandbox, "allow-scripts"); assert.doesNotMatch(attrs.sandbox, /allow-same-origin|allow-forms|allow-top-navigation|allow-popups/); assert.equal(frame.referrerPolicy, "no-referrer"); }); test("HTML fragments receive a restrictive head before their content", () => { const document = htmlPreviewDocument("

报告

"); assert.match(document, /^

报告<\/h1>$/); }); test("main and mini previews expose preview/source mode controls", () => { for (const prefix of ["fp", "mp"]) { assert.match(pageHtml, new RegExp(`id="${prefix}-mode-preview"`)); assert.match(pageHtml, new RegExp(`id="${prefix}-mode-source"`)); assert.match(previewJs, new RegExp(`_showRenderableText\\("${prefix}", cat, text\\)`)); } }); test("assistant HTML artifacts render inline with lazy loading and an expand action", () => { const mediaJs = readFileSync(new URL("../web/static/js/media.js", import.meta.url), "utf8"); assert.match(mediaJs, /cat === "html"/); assert.match(mediaJs, /class="art-html-open"/); assert.match(mediaJs, /new IntersectionObserver/); assert.match(mediaJs, /configureHtmlPreviewFrame\(frame, source/); assert.match(pageHtml, /\.art-html-frame/); assert.match(pageHtml, /\.msg\.assistant:has\(\.art-html\)/); assert.match(pageHtml, /\.art-html\s*\{[^}]*width:\s*100%[^}]*max-width:\s*none/s); assert.match(chatJs, /renderArtifactBarHtml\(extractArtifactRels\(p\.content, wd\), "html", state\.taskId/); assert.match(chatJs, /Array\.isArray\(m\.artifact_refs\)/); assert.match(chatJs, /renderArtifactBarHtml\(m\.artifact_refs, true, state\.taskId/); assert.match(previewJs, /\/v1\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/files\/download/); assert.match(previewJs, /downloadFile\(_fpCurrentRel, _fpCurrentTaskId, _fpCurrentLegacy\)/); assert.match(chatJs, /dataset\.legacyPath === "1"/); const clickHandler = chatJs.indexOf('$("chat-stream").addEventListener("click"'); const expandHandler = chatJs.indexOf('e.target.closest(".art-html-open[data-rel]")'); const sendMessage = chatJs.indexOf("async function sendMessage"); assert.ok(clickHandler >= 0 && expandHandler > clickHandler && expandHandler < sendMessage); }); test("artifact chips expose a compact file type and preview affordance", () => { const mediaJs = readFileSync(new URL("../web/static/js/media.js", import.meta.url), "utf8"); assert.match(mediaJs, /class="art-chip-icon"/); assert.match(mediaJs, /class="art-chip-name"/); assert.match(mediaJs, /class="art-chip-open">预览/); assert.match(pageHtml, /\.art-chip-icon/); assert.match(pageHtml, /\.art-chip:focus-visible/); }); test("message actions copy raw content and revise by appending a new message", () => { assert.match(chatJs, /class="msg-action msg-copy"[^>]+aria-label="复制消息"/); assert.match(chatJs, /class="msg-action msg-revise"[^>]+aria-label="引用并修改消息"/); assert.match(chatJs, /state\.loadedMessages\.find/); assert.match(chatJs, /navigator\.clipboard\.writeText\(text\)/); assert.match(chatJs, /function loadMessageIntoComposer\(text\)/); assert.match(chatJs, /修改后发送将作为一条新消息追加/); assert.match(pageHtml, /\.msg:hover \.msg-actions, \.msg:focus-within \.msg-actions/); assert.match(pageHtml, /\.msg-action:focus-visible/); });