148 lines
7.9 KiB
JavaScript
148 lines
7.9 KiB
JavaScript
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("<!doctype html><html><head><title>x</title></head><body>x</body></html>");
|
|
assert.match(document, /<head><meta http-equiv="Content-Security-Policy"/);
|
|
assert.match(HTML_PREVIEW_CSP, /script-src[^;]+https:/);
|
|
assert.match(HTML_PREVIEW_CSP, /connect-src https:/);
|
|
assert.match(document, /form-action 'none'/);
|
|
assert.match(document, /base-uri 'none'/);
|
|
assert.ok(document.indexOf("Content-Security-Policy") < document.indexOf("<title>"));
|
|
const attrs = {};
|
|
let onload = null;
|
|
let posted = null;
|
|
const frame = {
|
|
contentWindow: { postMessage(value, origin) { posted = { value, origin }; } },
|
|
setAttribute(name, value) { attrs[name] = value; },
|
|
addEventListener(type, fn) { if (type === "load") onload = fn; },
|
|
};
|
|
configureHtmlPreviewFrame(frame, "<h1>x</h1>");
|
|
assert.equal(attrs.sandbox, "allow-scripts");
|
|
assert.equal(attrs.csp, undefined);
|
|
assert.doesNotMatch(attrs.sandbox, /allow-same-origin|allow-forms|allow-top-navigation|allow-popups/);
|
|
assert.equal(frame.referrerPolicy, "no-referrer");
|
|
assert.equal(frame.src, "/static/html_preview_host.html");
|
|
assert.equal(frame.srcdoc, undefined);
|
|
onload();
|
|
assert.equal(posted.origin, "*");
|
|
assert.equal(posted.value.type, "zcbot-html-preview");
|
|
assert.match(posted.value.document, /<h1>x<\/h1>/);
|
|
});
|
|
|
|
test("HTML preview uses a same-origin message host instead of srcdoc or blob navigation", () => {
|
|
assert.match(previewJs, /configureHtmlPreviewFrame\(frame, text\)/);
|
|
const source = readFileSync(new URL("../web/static/js/preview_content.js", import.meta.url), "utf8");
|
|
const host = readFileSync(new URL("../web/static/html_preview_host.html", import.meta.url), "utf8");
|
|
assert.match(source, /HTML_PREVIEW_HOST = "\/static\/html_preview_host\.html"/);
|
|
assert.match(source, /contentWindow\.postMessage/);
|
|
assert.doesNotMatch(source, /URL\.createObjectURL|frame\.srcdoc\s*=/);
|
|
assert.match(host, /event\.source !== parent/);
|
|
assert.match(host, /document\.write\(event\.data\.document\)/);
|
|
});
|
|
|
|
test("HTML fragments receive a restrictive head before their content", () => {
|
|
const document = htmlPreviewDocument("<h1>报告</h1>");
|
|
assert.match(document, /^<head><meta http-equiv="Content-Security-Policy"/);
|
|
assert.match(document, /<\/head><h1>报告<\/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("PDF previews use the bundled PDF.js canvas renderer for WebView compatibility", () => {
|
|
assert.match(previewJs, /vendor\/pdfjs\/pdf\.min\.js/);
|
|
assert.match(previewJs, /pdfjsLib\.getDocument\(\{ data \}\)/);
|
|
assert.match(previewJs, /page\.render\(\{/);
|
|
assert.doesNotMatch(previewJs, /iframe class="preview-frame" src="\$\{[^}]*application\/pdf/);
|
|
assert.match(pageHtml, /\.pdf-viewport/);
|
|
assert.match(pageHtml, /\.pdf-canvas/);
|
|
});
|
|
|
|
test("PDF previews render a continuously scrollable lazy page list", () => {
|
|
assert.match(previewJs, /class="pdf-pages-list"/);
|
|
assert.match(previewJs, /new IntersectionObserver/);
|
|
assert.match(previewJs, /root: viewportHost/);
|
|
assert.match(previewJs, /viewportHost\.addEventListener\("scroll"/);
|
|
assert.doesNotMatch(previewJs, /class="small pdf-(?:prev|next)"/);
|
|
assert.match(pageHtml, /\.pdf-preview\s*\{[^}]*display:\s*flex[^}]*flex-direction:\s*column[^}]*min-height:\s*0/s);
|
|
assert.match(pageHtml, /\.pdf-viewport\s*\{[^}]*flex:\s*1[^}]*min-height:\s*0[^}]*overflow:\s*auto/s);
|
|
assert.match(pageHtml, /\.pdf-pages-list\s*\{[^}]*flex-direction:\s*column/s);
|
|
});
|
|
|
|
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, _fpCurrentArtifactId\)/);
|
|
assert.match(mediaJs, /data-artifact-id/);
|
|
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("sending a message closes every open file preview", () => {
|
|
assert.match(previewJs, /export function closeAllPreviews\(\)/);
|
|
assert.match(previewJs, /closeFilePreview\(\)[\s\S]*closeMiniPreview\(\)/);
|
|
assert.match(chatJs, /closeAllPreviews,[\s\S]*from "\.\/preview\.js"/);
|
|
|
|
const sendMessage = chatJs.indexOf("async function sendMessage");
|
|
const closePreviews = chatJs.indexOf("closeAllPreviews();", sendMessage);
|
|
const postMessage = chatJs.indexOf("await postMessageWithRetry", sendMessage);
|
|
assert.ok(sendMessage >= 0 && closePreviews > sendMessage && closePreviews < postMessage);
|
|
});
|
|
|
|
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/);
|
|
});
|