zcbot/tests/frontend_preview.test.mjs

65 lines
3.3 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 = {};
const frame = { setAttribute(name, value) { attrs[name] = value; } };
configureHtmlPreviewFrame(frame, "<h1>x</h1>");
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("<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("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(chatJs, /renderArtifactBarHtml\(extractArtifactRels\(p\.content, wd\), "html"\)/);
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);
});