把正文组成收成数据契约,程序投影与旁观维护不再靠模型临场猜。
有变量真值时固定开旁观填表,开场与游玩只填模型生成区。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+148
-1
@@ -4,6 +4,7 @@ import {
|
||||
PRESENT_FRAME_SANDBOX,
|
||||
buildPresentSrcdoc,
|
||||
isFrontendBundle,
|
||||
presentFrameSlotHtml,
|
||||
presentSourceBytes,
|
||||
previewPacket,
|
||||
} from "../web/present-frame.js";
|
||||
@@ -24,7 +25,8 @@ describe("present frame srcdoc", () => {
|
||||
});
|
||||
expect(srcdoc).toContain(`<div id="app"></div>`);
|
||||
expect(srcdoc).toContain(`#app { color: #e1e7f0; }`);
|
||||
expect(srcdoc).toContain("present.onData");
|
||||
expect(srcdoc).toContain("present-host");
|
||||
expect(srcdoc).toContain("applyPacket");
|
||||
expect(srcdoc).toContain("示例正文");
|
||||
expect(srcdoc).toContain(`default-src 'none'`);
|
||||
expect(srcdoc.indexOf("window.present")).toBeLessThan(srcdoc.indexOf("data.blocks.body"));
|
||||
@@ -54,6 +56,112 @@ describe("present frame srcdoc", () => {
|
||||
expect(PRESENT_BYTE_LIMIT).toBe(60 * 1024);
|
||||
});
|
||||
|
||||
it("sizes the frame to content and fills contract slots", () => {
|
||||
const srcdoc = buildPresentSrcdoc({
|
||||
html: `<article data-block="body"></article>`,
|
||||
css: `html,body{height:100vh;overflow:auto}`,
|
||||
js: `present.onData(function () {});`,
|
||||
packet: { schema: "present.v1", blocks: { body: "正文", forum_feed: ["一帖"] } },
|
||||
slots: [
|
||||
{ id: "body", type: "markdown" },
|
||||
{ id: "forum_feed", type: "list" },
|
||||
],
|
||||
frameId: "pf3",
|
||||
});
|
||||
expect(srcdoc).not.toContain("100vh");
|
||||
expect(srcdoc).not.toContain("overflow:auto");
|
||||
expect(srcdoc).toContain("overflow:hidden!important");
|
||||
expect(srcdoc).toContain("fillSlots");
|
||||
expect(srcdoc).toContain("isProgramOwned");
|
||||
expect(srcdoc).toContain("forum_feed");
|
||||
expect(srcdoc).toContain('"type":"list"');
|
||||
});
|
||||
|
||||
it("turns markdown newlines into paragraphs even if the shell wrote textContent first", () => {
|
||||
const srcdoc = buildPresentSrcdoc({
|
||||
html: `<article id="body" data-block="body"></article>`,
|
||||
css: "",
|
||||
js: `present.onData(function (data) {
|
||||
document.getElementById("body").textContent = data.blocks.body;
|
||||
});`,
|
||||
packet: {
|
||||
schema: "present.v1",
|
||||
blocks: { body: "第一段\\n\\n第二段 **加粗**" },
|
||||
},
|
||||
slots: [{ id: "body", type: "markdown" }],
|
||||
frameId: "pf4",
|
||||
});
|
||||
// packet JSON keeps real newlines; template sample above used escaped for the test string —
|
||||
// rebuild with actual newlines:
|
||||
const withBreaks = buildPresentSrcdoc({
|
||||
html: `<article id="body" data-block="body"></article>`,
|
||||
css: "",
|
||||
js: `present.onData(function (data) {
|
||||
document.getElementById("body").textContent = data.blocks.body;
|
||||
});`,
|
||||
packet: {
|
||||
schema: "present.v1",
|
||||
blocks: { body: "第一段\n\n第二段 **加粗**" },
|
||||
},
|
||||
slots: [{ id: "body", type: "markdown" }],
|
||||
frameId: "pf4b",
|
||||
});
|
||||
const match = withBreaks.match(/function markdown\(raw\) \{[\s\S]*?\n \}/);
|
||||
expect(match).toBeTruthy();
|
||||
const markdown = new Function(`${match![0]}; return markdown;`)();
|
||||
const html = markdown("第一段\n\n第二段 **加粗**");
|
||||
expect(html).toContain("<p>第一段</p>");
|
||||
expect(html).toContain("<p>第二段 <strong>加粗</strong></p>");
|
||||
expect(withBreaks).toContain("isProgramOwned");
|
||||
expect(srcdoc).toContain("isProgramOwned");
|
||||
});
|
||||
|
||||
it("applies program fill after shell onData so textContent cannot keep a wall of text", () => {
|
||||
const srcdoc = buildPresentSrcdoc({
|
||||
html: `<article id="body" data-block="body"></article>`,
|
||||
css: "",
|
||||
js: `present.onData(function (data) {
|
||||
document.getElementById("body").textContent = data.blocks.body;
|
||||
});`,
|
||||
packet: { schema: "present.v1", blocks: { body: "a\\nb" } },
|
||||
slots: [{ id: "body", type: "markdown" }],
|
||||
frameId: "pf5",
|
||||
});
|
||||
const sealIdx = srcdoc.indexOf("present.__seal");
|
||||
const ownedIdx = srcdoc.indexOf("isProgramOwned");
|
||||
expect(ownedIdx).toBeGreaterThan(-1);
|
||||
expect(sealIdx).toBeGreaterThan(-1);
|
||||
// applyPacket: renderCb then fillSlots
|
||||
const applyIdx = srcdoc.indexOf("function applyPacket");
|
||||
const applyChunk = srcdoc.slice(applyIdx, applyIdx + 450);
|
||||
expect(applyChunk.indexOf("renderCb")).toBeLessThan(applyChunk.lastIndexOf("fillSlots"));
|
||||
expect(srcdoc).toContain('t === "kv" || t === "table" || t === "topology"');
|
||||
});
|
||||
|
||||
it("renders list objects by text/label instead of JSON.stringify", () => {
|
||||
const srcdoc = buildPresentSrcdoc({
|
||||
html: `<ol data-block="options"></ol>`,
|
||||
css: "",
|
||||
js: `present.onData(function () {});`,
|
||||
packet: { schema: "present.v1", blocks: { options: [] } },
|
||||
slots: [{ id: "options", type: "list" }],
|
||||
frameId: "pf6",
|
||||
});
|
||||
const match = srcdoc.match(/function listItemText\(item\) \{[\s\S]*?\n \}/);
|
||||
expect(match).toBeTruthy();
|
||||
const listItemText = new Function(`${match![0]}; return listItemText;`)();
|
||||
expect(listItemText({ type: "继续推进1", text: "顺着旧话试探" })).toBe("顺着旧话试探");
|
||||
expect(listItemText({ label: "换方向" })).toBe("换方向");
|
||||
expect(listItemText("纯字符串")).toBe("纯字符串");
|
||||
expect(listItemText('{"type":"x","text":"从字符串JSON取出"}')).toBe("从字符串JSON取出");
|
||||
});
|
||||
|
||||
it("keeps the previous frame height so a remount does not collapse the page", () => {
|
||||
expect(presentFrameSlotHtml("pf9", 1280)).toContain('style="min-height:1280px"');
|
||||
expect(presentFrameSlotHtml("pf9")).not.toContain("min-height");
|
||||
expect(presentFrameSlotHtml("pf9", 4)).not.toContain("min-height");
|
||||
});
|
||||
|
||||
it("normalizes sample data into present.v1 blocks", () => {
|
||||
expect(previewPacket({ schema: "present.v1", blocks: { body: "x" } })).toEqual({
|
||||
schema: "present.v1",
|
||||
@@ -61,4 +169,43 @@ describe("present frame srcdoc", () => {
|
||||
});
|
||||
expect(previewPacket(null)).toEqual({ schema: "present.v1", blocks: {} });
|
||||
});
|
||||
|
||||
it("accepts Chinese block ids and expands nested matrix JSON into a table", () => {
|
||||
const srcdoc = buildPresentSrcdoc({
|
||||
html: `<section data-block="卷宗表"></section><article data-block="正文"></article>`,
|
||||
css: "",
|
||||
js: "present.onData(function () {});",
|
||||
packet: {
|
||||
schema: "present.v1",
|
||||
blocks: {
|
||||
卷宗表: {
|
||||
source: "heroines_matrix",
|
||||
rows: [
|
||||
{ name: "塞琳娜", 亲密度: -10, 服从度: 10 },
|
||||
{ name: "崔岚", 亲密度: -10, 服从度: 10 },
|
||||
],
|
||||
},
|
||||
正文: "第一段\n\n第二段",
|
||||
},
|
||||
},
|
||||
slots: [
|
||||
{ id: "卷宗表", type: "table" },
|
||||
{ id: "正文", type: "markdown" },
|
||||
],
|
||||
frameId: "pf-zh",
|
||||
});
|
||||
expect(srcdoc).toContain("function isBlockId");
|
||||
expect(srcdoc).toContain("present-data-table");
|
||||
expect(srcdoc).toContain("matrixMapToRows");
|
||||
expect(srcdoc).not.toContain("/^[A-Za-z_]");
|
||||
|
||||
const writeMatch = srcdoc.match(/function writeDataTable\(el, rows, columns\) \{[\s\S]*?\n \}/);
|
||||
expect(writeMatch).toBeTruthy();
|
||||
const idMatch = srcdoc.match(/function isBlockId\(id\) \{[\s\S]*?\n \}/);
|
||||
expect(idMatch).toBeTruthy();
|
||||
const isBlockId = new Function(`${idMatch![0]}; return isBlockId;`)();
|
||||
expect(isBlockId("正文")).toBe(true);
|
||||
expect(isBlockId("body")).toBe(true);
|
||||
expect(isBlockId('bad"id')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user