63 lines
5.2 KiB
TypeScript
63 lines
5.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Spec } from "@json-render/core";
|
|
import { answerChartTableData, answerViewStateDirtyPaths, migrateAnswerViewState, normalizeAnswerViewState } from "./answer-view-state";
|
|
|
|
function spec(type: string, props: Record<string, unknown>): Spec {
|
|
return { root: "node", elements: { node: { type, props, children: [] } } };
|
|
}
|
|
const chart = (labels = ["a", "b", "c"], names = ["入口", "出口"]) => spec("Chart", { chart_type: "line", x_data: labels, series: names.map((name) => ({ name, data: labels.map((_, index) => index) })) });
|
|
const table = (rows = [["a", "1"], ["b", "2"]], columns = ["对象", "读数"]) => spec("Table", { rows, columns });
|
|
const form = (fields = [{ name: "record", kind: "text" }]) => spec("FollowUpForm", { fields });
|
|
|
|
describe("answer view state compatibility", () => {
|
|
it("uses the same UTC8 sample display in chart tables while preserving stored samples", () => {
|
|
const series = [{ name: "压力", data: [20], sample_times: ["2026-07-16T00:00:00Z"] }];
|
|
const current = spec("Chart", { chart_type: "line", x_data: ["08:00"],
|
|
series });
|
|
const state = normalizeAnswerViewState(current).nodes.node;
|
|
if (state.type !== "Chart") throw new Error("Expected chart state");
|
|
expect(answerChartTableData(current.elements.node, state).rows[0][1]).toBe("20 (2026-07-16 08:00:00+08:00)");
|
|
expect(series[0].sample_times[0]).toBe("2026-07-16T00:00:00Z");
|
|
});
|
|
it("ignores legacy row selection, retains search, and clears sort when columns change", () => {
|
|
const before = table();
|
|
const state = normalizeAnswerViewState(before, { nodes: { node: { type: "Table", query: "a", sort: { column: 1, descending: true }, selection: [0] } } });
|
|
expect(migrateAnswerViewState(before, table([["b", "2"], ["a", "1"]]), state).nodes.node).toEqual({ type: "Table", query: "a", sort: { column: 1, descending: true }, selection: [] });
|
|
expect(migrateAnswerViewState(before, table(undefined, ["读数", "对象"]), state).nodes.node).toMatchObject({ query: "a", sort: null, selection: [] });
|
|
expect(migrateAnswerViewState(before, structuredClone(before), state)).toEqual(state);
|
|
});
|
|
|
|
it("relocates chart ranges and points only by unique labels and resets hidden series after renaming", () => {
|
|
const before = chart();
|
|
const state = normalizeAnswerViewState(before, { nodes: { node: { type: "Chart", view: "bar", hidden: [1], range: [1, 2], point: 2, inspecting: true, table: { query: "b", sort: { column: 1, descending: false }, selection: [0] } } } });
|
|
expect(migrateAnswerViewState(before, chart(["z", "a", "b", "c"]), state).nodes.node).toMatchObject({ view: "bar", hidden: [1], range: [2, 3], point: 3, inspecting: true, table: { query: "b", selection: [] } });
|
|
expect(migrateAnswerViewState(before, chart(undefined, ["出口", "入口"]), state).nodes.node).toMatchObject({ view: "bar", hidden: [], table: { query: "b", sort: null, selection: [] } });
|
|
for (const labels of [["a", "b", "b"], ["a", "b"]]) expect(migrateAnswerViewState(before, chart(labels), state).nodes.node).toMatchObject({ range: [0, labels.length - 1], point: 0, inspecting: false });
|
|
});
|
|
|
|
it("preserves same-name same-kind drafts across equivalent streaming schemas and drops replaced fields", () => {
|
|
const before = form([{ name: "record", kind: "text" }, { name: "deleted", kind: "text" }]);
|
|
const state = normalizeAnswerViewState(before, { nodes: { node: { type: "FollowUpForm", values: { record: "尚未提交", deleted: "旧值" } } } });
|
|
expect(migrateAnswerViewState(before, structuredClone(before), state)).toEqual(state);
|
|
expect(migrateAnswerViewState(before, form([{ name: "record", kind: "number" }, { name: "added", kind: "text" }]), state).nodes.node).toEqual({ type: "FollowUpForm", values: {} });
|
|
expect(migrateAnswerViewState(before, form(), state).nodes.node).toEqual({ type: "FollowUpForm", values: { record: "尚未提交" } });
|
|
});
|
|
|
|
it("only accepts supported state, bounds indices, preserves draft text, and resets missing evidence filters", () => {
|
|
const current = table();
|
|
const result = normalizeAnswerViewState(current, { nodes: { node: { type: "Table", query: "查".repeat(2000), sort: { column: 99, descending: true }, selection: [-1, 0, 0, 2] }, unknown: { type: "Evidence", open: true } }, block: { evidenceFilter: "gone" }, action: "ignored" });
|
|
expect(result).toEqual({ nodes: { node: { type: "Table", query: "查".repeat(2000), sort: null, selection: [] } }, block: { evidenceFilter: "all" } });
|
|
});
|
|
|
|
it("tracks form and nested chart-table edits at field granularity", () => {
|
|
const current = form([{ name: "record", kind: "text" }, { name: "other", kind: "text" }]);
|
|
const before = normalizeAnswerViewState(current);
|
|
const after = normalizeAnswerViewState(current, { nodes: { node: { type: "FollowUpForm", values: { record: "草稿" } } } });
|
|
expect(answerViewStateDirtyPaths(before, after)).toEqual(["/nodes/node/values/record"]);
|
|
const chartState = normalizeAnswerViewState(chart());
|
|
const updated = structuredClone(chartState);
|
|
if (updated.nodes.node.type === "Chart") updated.nodes.node.table!.query = "a";
|
|
expect(answerViewStateDirtyPaths(chartState, updated)).toEqual(["/nodes/node/table/query"]);
|
|
});
|
|
});
|