119 lines
5.6 KiB
TypeScript
119 lines
5.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { parseAnswerUiBlock } from "./contract";
|
||
|
||
const block = {
|
||
version: "answer-ui@1",
|
||
block_id: "reading",
|
||
fallback_text: "仍待核对",
|
||
spec: {
|
||
root: "note",
|
||
elements: { note: { type: "Text", props: { text: "仍待核对", tone: "neutral" }, children: [] } }
|
||
}
|
||
};
|
||
describe("untrusted answer components", () => {
|
||
it("accepts a line with seven sparse history series while enforcing sample timestamp alignment", () => {
|
||
const x_data = Array.from({ length: 13 }, (_, index) => String(index));
|
||
const series = [{ name: "当前", type: "line", data: x_data.map(() => 31) },
|
||
...Array.from({ length: 7 }, (_, index) => ({ name: `前${index + 1}日`, type: "scatter",
|
||
data: [...Array(10).fill(null), 33, 34, 35],
|
||
sample_times: [...Array(10).fill(null), ...[30, 45, 59].map((minute) => `2026-06-${String(13 - index).padStart(2, "0")}T11:${minute}:00+08:00`)] }))];
|
||
const props = { title: "压力(m)", source: "合成样例", chart_type: "line", x_data, series };
|
||
const make = (value: unknown) => ({ ...block, spec: { root: "chart", elements: { chart: { type: "Chart", props: value, children: [] } } } });
|
||
expect(parseAnswerUiBlock(make(props))).not.toBeNull();
|
||
expect(parseAnswerUiBlock(make({ ...props, series: [...series, series[0]] }))).toBeNull();
|
||
expect(parseAnswerUiBlock(make({ ...props, series: [{ ...series[1], sample_times: [null] }] }))).toBeNull();
|
||
expect(parseAnswerUiBlock(make({ ...props, series: [{ ...series[1], sample_times: Array(13).fill("2026-06-13T12:00:00+08:00") }] }))).toBeNull();
|
||
});
|
||
it("keeps explicit UI phases and rejects invalid phase values", () => {
|
||
expect(parseAnswerUiBlock(block)?.phase).toBeUndefined();
|
||
expect(parseAnswerUiBlock({ ...block, phase: "process" })?.phase).toBe("process");
|
||
expect(parseAnswerUiBlock({ ...block, phase: "final" })?.phase).toBe("final");
|
||
expect(parseAnswerUiBlock({ ...block, phase: "other" })).toBeNull();
|
||
expect(parseAnswerUiBlock({ ...block, phase: null })).toBeNull();
|
||
});
|
||
it("accepts controlled task semantics and result states without accepting arbitrary presentation code", () => {
|
||
const result = { type: "Result", props: { title: "定位结果", summary: "3 根管道已定位,高亮未确认。", status: "partial", source: "地图工具回执" }, children: [] };
|
||
const spec = {
|
||
root: "root", elements: {
|
||
root: { type: "Stack", props: { columns: "one", gap: "normal", intent: "response", density: "compact" }, children: ["section"] },
|
||
section: { type: "Section", props: { title: "本次结果", description: null, role: "result" }, children: ["result"] },
|
||
result
|
||
}
|
||
};
|
||
expect(parseAnswerUiBlock({ ...block, spec })).not.toBeNull();
|
||
for (const props of [
|
||
{ ...result.props, status: "running" },
|
||
{ ...result.props, css: "display:none" },
|
||
{ ...result.props, onClick: "fetch('/write')" }
|
||
]) expect(parseAnswerUiBlock({ ...block, spec: { ...spec, elements: { ...spec.elements, result: { ...result, props } } } })).toBeNull();
|
||
expect(parseAnswerUiBlock({ ...block, spec: { root: "note", elements: { note: { ...block.spec.elements.note, props: { text: "高亮操作未提供。", tone: "warning", title: "能力限制" } } } } })).not.toBeNull();
|
||
});
|
||
it("accepts registered text and rejects arbitrary code, URLs and unknown components", () => {
|
||
expect(parseAnswerUiBlock(block)).not.toBeNull();
|
||
for (const node of [
|
||
{ type: "iframe", props: { src: "https://example.com" }, children: [] },
|
||
{
|
||
...block.spec.elements.note,
|
||
props: { ...block.spec.elements.note.props, script: "alert(1)" }
|
||
},
|
||
{ ...block.spec.elements.note, on: { click: { action: "fetch" } } }
|
||
])
|
||
expect(
|
||
parseAnswerUiBlock({ ...block, spec: { root: "note", elements: { note: node } } })
|
||
).toBeNull();
|
||
});
|
||
it("rejects missing, duplicate, cyclic, unreachable and excessive trees", () => {
|
||
const stack = { type: "Stack", props: { columns: "one", gap: "normal" }, children: ["note"] };
|
||
for (const children of [["missing"], ["note", "note"], ["root"]])
|
||
expect(
|
||
parseAnswerUiBlock({
|
||
...block,
|
||
spec: { root: "root", elements: { root: { ...stack, children }, ...block.spec.elements } }
|
||
})
|
||
).toBeNull();
|
||
expect(
|
||
parseAnswerUiBlock({
|
||
...block,
|
||
spec: {
|
||
...block.spec,
|
||
elements: { ...block.spec.elements, unused: block.spec.elements.note }
|
||
}
|
||
})
|
||
).toBeNull();
|
||
});
|
||
it("requires aligned, finite chart values and unambiguous form fields", () => {
|
||
const chart = {
|
||
type: "Chart",
|
||
props: {
|
||
title: "演练压力",
|
||
chart_type: "line",
|
||
source: "演练数据",
|
||
x_data: ["08:00"],
|
||
series: [{ name: "压力", data: [0.3] }]
|
||
},
|
||
children: []
|
||
};
|
||
const withNode = (node: unknown) => ({
|
||
...block,
|
||
spec: { root: "note", elements: { note: node } }
|
||
});
|
||
expect(parseAnswerUiBlock(withNode(chart))).not.toBeNull();
|
||
for (const data of [[0.3, 0.2], [Infinity], [NaN]])
|
||
expect(
|
||
parseAnswerUiBlock(
|
||
withNode({ ...chart, props: { ...chart.props, series: [{ name: "压力", data }] } })
|
||
)
|
||
).toBeNull();
|
||
const field = { name: "location", label: "位置", kind: "text", required: true };
|
||
expect(
|
||
parseAnswerUiBlock(
|
||
withNode({
|
||
type: "FollowUpForm",
|
||
props: { title: "核验", description: "用户补充", fields: [field, field] },
|
||
children: []
|
||
})
|
||
)
|
||
).toBeNull();
|
||
});
|
||
});
|