← Files DataARCHIVED FILE
tests/inline-chart-presentation.test.mjs
20.6 KB · Sep 30, 2026 · 23:19 UTC
import assert from "node:assert/strict";
import test from "node:test";
import { changeChartEditorSpec } from "../templates/data-app/base/src/charting/chart-editor-state.js";
import { chartTypes } from "../templates/data-app/base/src/charting/chart-theme.js";
import {
inlineChartEditorCapabilities,
originalInlineChartPresentation,
validateInlineChartPresentation,
} from "../templates/data-app/inline/chart-presentation.js";
import { normalizeInlineChartInput } from "../skills/visualize-data/scripts/inline-chart-input.mjs";
function reviewedFixture() {
return normalizeInlineChartInput({
schemaVersion: 1,
id: "reviewed-trend",
title: "Reviewed activity",
description: "Recorded observations, not a forecast.",
chart: { type: "line", x: "week", y: "activeUsers", series: "plan", stackable: false },
columns: ["baseline", "weekLabel"],
rows: [
{ week: "2026-07-27", weekLabel: "Week one", plan: "Free", activeUsers: 120, baseline: 100, privateSalary: 9 },
{ week: "2026-07-27", weekLabel: "Week one", plan: "Team", activeUsers: 80, baseline: 70, privateSalary: 8 },
{ week: "2026-08-03", weekLabel: "Week two", plan: "Free", activeUsers: null, baseline: 110, privateSalary: 7 },
{ week: "2026-08-03", weekLabel: "Week two", plan: "Team", activeUsers: 95, baseline: 75, privateSalary: 6 },
],
source: {
label: "Reviewed fixture",
metricDefinitions: [
{ field: "activeUsers", label: "Active users", definition: "Recorded active users." },
{ field: "baseline", label: "Baseline", definition: "Reviewed comparison." },
{ field: "privateSalary", label: "Private salary", definition: "Must not be exposed." },
],
caveats: ["The missing observation is not zero."],
},
});
}
const presentation = (component, changes = {}) => ({
...originalInlineChartPresentation(component),
...changes,
chart: { ...originalInlineChartPresentation(component).chart, ...changes.chart },
});
test("inline choices use only approved fields and compatible, nonaggregating chart types", () => {
const { component, rows } = reviewedFixture();
const choices = inlineChartEditorCapabilities(component, rows);
assert.deepEqual(choices.types, ["line", "area", "bar", "horizontalBar", "sparkline"]);
assert.equal(choices.fieldsEditable, true);
assert.deepEqual(choices.yFields, ["activeUsers", "baseline"]);
assert.ok(choices.xFields.includes("weekLabel"));
assert.ok(!choices.xFields.includes("plan"));
assert.ok(!choices.seriesFields.includes(""));
assert.ok(!choices.seriesFields.includes("weekLabel"));
assert.ok(!JSON.stringify(choices).includes("privateSalary"));
});
test("applying a compatible edit changes presentation and never the reviewed snapshot", () => {
const payload = reviewedFixture();
const before = JSON.stringify(payload);
const next = validateInlineChartPresentation(
payload.component,
payload.rows,
presentation(payload.component, {
title: " Comparison ",
description: " Same reviewed rows. ",
chart: {
type: "bar",
y: "baseline",
fields: ["baseline"],
showLegend: false,
xLabel: "Reporting week",
colors: { Free: "#123456" },
},
}),
);
assert.equal(next.title, "Comparison");
assert.equal(next.description, "Same reviewed rows.");
assert.equal(next.chart.type, "bar");
assert.equal(next.chart.y, "baseline");
assert.deepEqual(next.chart.colors, { Free: "#123456" });
assert.deepEqual(Object.keys(next), ["chart", "title", "description"]);
assert.equal(JSON.stringify(payload), before);
assert.equal(payload.rows[2].activeUsers, null);
assert.ok(!JSON.stringify(next).includes("privateSalary"));
assert.notEqual(next.chart, payload.component.chart);
});
test("new category or split choices cannot collapse reviewed observations", () => {
const { component, rows } = reviewedFixture();
assert.throws(
() => validateInlineChartPresentation(component, rows, presentation(component, { chart: { series: "" } })),
/collapse reviewed rows/u,
);
assert.throws(
() =>
validateInlineChartPresentation(component, rows, presentation(component, { chart: { x: "plan", series: "" } })),
/collapse reviewed rows/u,
);
const remapped = validateInlineChartPresentation(
component,
rows,
presentation(component, { chart: { x: "weekLabel" } }),
);
assert.equal(remapped.chart.x, "weekLabel");
assert.equal(remapped.chart.series, "plan");
});
test("new split choices cannot overwrite canonical pivot object properties", () => {
const component = { title: "Reviewed values", chart: { type: "line", x: "week", y: "wau" } };
for (const unsafe of ["__proto__", "prototype", "constructor", "week"]) {
const rows = [
{ week: "one", wau: 10, group: unsafe },
{ week: "two", wau: 20, group: "Team" },
];
assert.ok(!inlineChartEditorCapabilities(component, rows).seriesFields.includes("group"), unsafe);
assert.throws(
() => validateInlineChartPresentation(component, rows, presentation(component, { chart: { series: "group" } })),
/collapse reviewed rows/u,
unsafe,
);
}
});
test("a grain-safe split change replaces derived series labels with the reviewed measure", () => {
const component = {
title: "Reviewed split",
chart: { type: "line", x: "week", y: "value", series: "plan", fields: ["Free", "Team"] },
};
const rows = [
{ week: "one", value: 10, plan: "Free" },
{ week: "two", value: 20, plan: "Team" },
];
assert.ok(inlineChartEditorCapabilities(component, rows).seriesFields.includes(""));
const cleared = validateInlineChartPresentation(
component,
rows,
presentation(component, { chart: { series: "", fields: ["value"] } }),
);
assert.deepEqual(cleared.chart.fields, ["value"]);
assert.equal(cleared.chart.series, "");
assert.deepEqual(
validateInlineChartPresentation(component, rows, originalInlineChartPresentation(component)),
originalInlineChartPresentation(component),
);
});
test("a reviewed wide chart keeps every measure through a safe split round-trip", () => {
const { component, rows } = normalizeInlineChartInput({
schemaVersion: 1,
title: "Reviewed actual and target",
chart: { type: "line", x: "week", y: "actual", fields: ["actual", "target"] },
columns: ["plan"],
rows: [
{ week: "one", actual: 10, target: 12, plan: "Free", privateValue: 999 },
{ week: "two", actual: 15, target: 18, plan: "Team", privateValue: 999 },
],
source: { label: "Reviewed fixture" },
});
const fields = { columns: [...new Set(rows.flatMap(Object.keys))] };
assert.ok(inlineChartEditorCapabilities(component, rows).seriesFields.includes("plan"));
const split = changeChartEditorSpec(component.chart, "series", "plan", fields);
assert.deepEqual(split.fields, ["actual", "target"]);
validateInlineChartPresentation(component, rows, presentation(component, { chart: split }));
assert.ok(inlineChartEditorCapabilities(component, rows, split).seriesFields.includes(""));
const restored = changeChartEditorSpec(split, "series", "", fields);
const accepted = validateInlineChartPresentation(component, rows, presentation(component, { chart: restored }));
assert.deepEqual(accepted.chart.fields, ["actual", "target"]);
assert.equal(accepted.chart.y, "actual");
assert.ok(!JSON.stringify(accepted).includes("privateValue"));
});
test("unsupported data, source, schema, and aggregation edits fail closed", () => {
const { component, rows } = reviewedFixture();
const invalid = [
[{ queryId: "another-query" }, /only chart presentation/u],
[{ rows: [] }, /only chart presentation/u],
[{ chart: { type: "histogram" } }, /not compatible/u],
[{ chart: { type: "heatmap" } }, /not compatible/u],
[{ chart: { type: "stackedBar" } }, /not compatible/u],
[{ chart: { y: "privateSalary", fields: ["privateSalary"] } }, /unavailable|numeric reviewed/u],
[{ chart: { source: "privateSalary" } }, /fixed by the reviewed/u],
[{ chart: { stackable: true } }, /fixed by the reviewed/u],
[{ chart: { sql: "SELECT secret" } }, /unsupported chart option/u],
[{ chart: { showLegend: "yes" } }, /must be a boolean/u],
[{ chart: { sortOrder: "random" } }, /supported sort order/u],
[{ chart: { xLabel: "x".repeat(501) } }, /at most 500/u],
[{ chart: { colors: { Free: "url(https://example.com/private)" } } }, /supported reviewed series color/u],
[{ chart: { colors: { privateSalary: "#123456" } } }, /supported reviewed series color/u],
[{ title: " " }, /nonempty text/u],
[{ description: "x".repeat(2001) }, /at most 2000/u],
];
const before = JSON.stringify({ component, rows });
for (const [changes, expected] of invalid) {
assert.throws(() => validateInlineChartPresentation(component, rows, presentation(component, changes)), expected);
}
assert.equal(JSON.stringify({ component, rows }), before);
});
test("original omissions survive copy-only edits and exact reset", () => {
const component = { id: "plain", title: "Original", chart: { type: "line", x: "week", y: "wau" } };
const rows = [
{ week: "one", wau: 120 },
{ week: "two", wau: null },
{ week: "three", wau: 145 },
];
const original = originalInlineChartPresentation(component);
const copyOnly = validateInlineChartPresentation(component, rows, {
...original,
title: "Renamed",
chart: { ...original.chart, fields: ["wau"], series: "" },
});
assert.deepEqual(copyOnly.chart, component.chart);
assert.deepEqual(validateInlineChartPresentation(component, rows, original), original);
original.chart.type = "bar";
assert.equal(component.chart.type, "line");
assert.deepEqual(
rows.map((row) => row.wau),
[120, null, 145],
);
});
test("all-null original measures remain editable without inventing zeroes", () => {
const component = { title: "Missing series", chart: { type: "line", x: "week", y: "wau" } };
const rows = [
{ week: "one", wau: null },
{ week: "two", wau: null },
];
assert.deepEqual(inlineChartEditorCapabilities(component, rows).yFields, ["wau"]);
assert.equal(
validateInlineChartPresentation(component, rows, presentation(component, { chart: { type: "bar" } })).chart.type,
"bar",
);
assert.deepEqual(
rows.map((row) => row.wau),
[null, null],
);
});
test("an authored numeric grouping role remains valid for copy edits and reset", () => {
const component = { title: "Cohorts", chart: { type: "line", x: "week", y: "wau", series: "cohortYear" } };
const rows = [
{ week: "one", wau: 10, cohortYear: 2025 },
{ week: "one", wau: 20, cohortYear: 2026 },
];
const choices = inlineChartEditorCapabilities(component, rows);
assert.ok(choices.types.includes("line"));
assert.ok(choices.seriesFields.includes("cohortYear"));
const original = originalInlineChartPresentation(component);
assert.deepEqual(validateInlineChartPresentation(component, rows, original), original);
assert.equal(
validateInlineChartPresentation(component, rows, { ...original, title: "Cohort activity" }).title,
"Cohort activity",
);
});
test("legacy numeric-string measures retain their original presentation without new coercion", () => {
const { component, rows } = normalizeInlineChartInput({
schemaVersion: 1,
title: "Reviewed decimal strings",
chart: { type: "bar", x: "category", y: "amount" },
rows: [
{ category: "A", amount: "1.25" },
{ category: "B", amount: "2.5" },
],
source: { label: "Reviewed decimal export" },
});
const original = originalInlineChartPresentation(component);
const choices = inlineChartEditorCapabilities(component, rows);
assert.deepEqual(choices.types, ["bar"]);
assert.equal(choices.fieldsEditable, false);
assert.deepEqual(validateInlineChartPresentation(component, rows, original), original);
assert.equal(
validateInlineChartPresentation(component, rows, { ...original, title: "Renamed decimals" }).title,
"Renamed decimals",
);
assert.deepEqual(
rows.map((row) => row.amount),
["1.25", "2.5"],
);
});
test("stacking requires the original reviewed semantics and never introduces percentage normalization", () => {
const rows = [{ category: "A", a: 2, b: 3, conversionRate: 0.4 }];
const component = {
title: "Additive measures",
chart: { type: "bar", x: "category", y: "a", fields: ["a", "b"], stackable: true },
};
const choices = inlineChartEditorCapabilities(component, rows);
assert.ok(choices.types.includes("stackedArea"));
assert.ok(choices.types.includes("stackedBar"));
assert.ok(!choices.types.includes("stackedBar100"));
const percentage = { ...component, chart: { ...component.chart, type: "stackedBar100" } };
assert.deepEqual(inlineChartEditorCapabilities(percentage, rows).types, ["stackedBar100", "horizontalStackedBar100"]);
assert.throws(
() => validateInlineChartPresentation(percentage, rows, presentation(percentage, { chart: { type: "bar" } })),
/not compatible/u,
);
assert.equal(inlineChartEditorCapabilities(percentage, rows).fieldsEditable, false);
assert.ok(!inlineChartEditorCapabilities(percentage, rows).yFields.includes("conversionRate"));
assert.throws(
() =>
validateInlineChartPresentation(
percentage,
rows,
presentation(percentage, { chart: { y: "conversionRate", fields: ["conversionRate"] } }),
),
/original reviewed data mapping/u,
);
const stacked = { ...component, chart: { ...component.chart, type: "stackedBar" } };
const rateChart = { ...stacked.chart, type: "bar", y: "conversionRate", fields: ["conversionRate"] };
assert.equal(
validateInlineChartPresentation(stacked, rows, presentation(stacked, { chart: rateChart })).chart.y,
"conversionRate",
);
assert.ok(!inlineChartEditorCapabilities(stacked, rows, rateChart).types.includes("stackedBar"));
assert.throws(
() =>
validateInlineChartPresentation(
stacked,
rows,
presentation(stacked, { chart: { ...rateChart, type: "stackedBar" } }),
),
/original reviewed data mapping/u,
);
});
test("new color tokens are bounded and refer only to the shared palette", () => {
const component = { title: "Heatmap", chart: { type: "heatmap", x: "day", y: "value", series: "team" } };
const rows = [{ day: "one", team: "A", value: 2 }];
for (const baseColor of ["var(--chart-9)", `var(--chart-${"1".repeat(10000)})`, "url(https://example.com)"]) {
assert.throws(
() => validateInlineChartPresentation(component, rows, presentation(component, { chart: { baseColor } })),
/supported chart color/u,
);
}
assert.equal(
validateInlineChartPresentation(
component,
rows,
presentation(component, { chart: { baseColor: "var(--chart-8)" } }),
).chart.baseColor,
"var(--chart-8)",
);
});
test("specialized and composed charts retain their reviewed type and mapping", () => {
const histogram = { title: "Latency", chart: { type: "histogram", y: "latencyMs" } };
const rows = [
{ latencyMs: 12, other: 3 },
{ latencyMs: null, other: 4 },
];
const choices = inlineChartEditorCapabilities(histogram, rows);
const beforeHistogramEdit = JSON.stringify({ histogram, rows });
assert.deepEqual(choices.types, ["histogram"]);
assert.equal(choices.fieldsEditable, false);
assert.throws(
() =>
validateInlineChartPresentation(
histogram,
rows,
presentation(histogram, { chart: { y: "other", fields: ["other"] } }),
),
/mapping is fixed/u,
);
const histogramLabels = {
xLabel: "Latency (ms)",
yLabel: "Requests",
showXAxisLabel: true,
showYAxisLabel: false,
showValues: true,
};
assert.deepEqual(
validateInlineChartPresentation(histogram, rows, presentation(histogram, { chart: histogramLabels })).chart,
{ ...histogram.chart, ...histogramLabels },
);
assert.throws(
() => validateInlineChartPresentation(histogram, rows, presentation(histogram, { chart: { type: "bar" } })),
/not compatible/u,
);
assert.deepEqual(
validateInlineChartPresentation(histogram, rows, originalInlineChartPresentation(histogram)),
originalInlineChartPresentation(histogram),
);
assert.equal(JSON.stringify({ histogram, rows }), beforeHistogramEdit);
const composed = {
title: "Actual and plan",
chart: { type: "line", x: "week", y: "actual", fields: ["actual"], barFields: ["plan"] },
};
const composedRows = [{ week: "one", actual: 2, plan: 3 }];
assert.deepEqual(inlineChartEditorCapabilities(composed, composedRows).types, ["line"]);
assert.equal(inlineChartEditorCapabilities(composed, composedRows).fieldsEditable, false);
});
test("axis and annotation controls retain the reviewed data and annotation definitions", () => {
const component = { title: "Actual and target", chart: {
type: "line", x: "week", y: "actual", fields: ["actual", "target"],
annotations: [{ id: "launch", kind: "event", at: "2026-08-01", field: "actual", label: "Launch" }],
} };
const rows = [{ week: "2026-08-01", actual: 10, target: 20 }];
const before = JSON.stringify({ component, rows });
const accepted = validateInlineChartPresentation(component, rows, presentation(component, {
chart: { yAxisPosition: "right", rightAxisFields: ["target"], showAnnotations: false },
}));
assert.equal(accepted.chart.yAxisPosition, "right");
assert.deepEqual(accepted.chart.rightAxisFields, ["target"]);
assert.equal(accepted.chart.showAnnotations, false);
assert.deepEqual(accepted.chart.annotations, component.chart.annotations);
for (const chart of [
{ yAxisPosition: "outside" },
{ rightAxisFields: ["privateValue"] },
{ rightAxisFields: ["target", "target"] },
{ showAnnotations: "yes" },
{ annotations: [] },
]) {
assert.throws(() => validateInlineChartPresentation(component, rows, presentation(component, { chart })),
/axis|boolean|fixed by the reviewed/u);
}
assert.equal(JSON.stringify({ component, rows }), before);
});
test("specialized bar presentations preserve their reviewed nested mapping", () => {
const component = { title: "Progress", chart: {
type: "horizontalBar", presentation: "progress", x: "category", y: "value",
barOptions: { track: { max: "goal" } },
} };
const rows = [{ category: "A", value: 10, goal: 20, other: 30 }];
const choices = inlineChartEditorCapabilities(component, rows);
assert.equal(choices.fieldsEditable, false);
assert.deepEqual(choices.types, ["horizontalBar"]);
assert.match(choices.notice, /title and description/u);
assert.equal(validateInlineChartPresentation(component, rows, presentation(component, {
title: "Reviewed progress",
})).title, "Reviewed progress");
assert.throws(() => validateInlineChartPresentation(component, rows, presentation(component, {
chart: { barOptions: { track: { max: "other" } } },
})), /barOptions is fixed/u);
assert.throws(() => validateInlineChartPresentation(component, rows, presentation(component, {
chart: { y: "other" },
})), /mapping is fixed/u);
});
test("legacy chart aliases use canonical editor choices without mutating authored input", () => {
const rows = [{ category: "A", value: 10 }];
for (const [type, canonical] of [["leaderboard", "rankedList"], ["horizontal-bar", "horizontalBar"]]) {
const component = { title: "Reviewed values", chart: { type, x: "category", y: "value" } };
const original = originalInlineChartPresentation(component);
assert.equal(original.chart.type, canonical);
assert.ok(inlineChartEditorCapabilities(component, rows).types.includes(canonical));
assert.deepEqual(validateInlineChartPresentation(component, rows, original), original);
assert.equal(component.chart.type, type);
}
});
test("every canonical original chart family can keep its reviewed presentation", () => {
for (const type of chartTypes) {
const chart =
type === "histogram"
? { type, y: "value" }
: type === "sankey"
? { type, y: "value", stages: ["from", "to"] }
: { type, x: "category", y: "value", ...(type === "heatmap" ? { series: "group" } : {}) };
const component = { title: type, chart };
const rows = [{ category: "A", value: 2, group: "G", from: "A", to: "B" }];
const original = originalInlineChartPresentation(component);
assert.ok(inlineChartEditorCapabilities(component, rows).types.includes(original.chart.type), type);
assert.deepEqual(validateInlineChartPresentation(component, rows, original), original, type);
}
const summary = { title: "Reviewed quartiles", chart: { type: "boxPlot", x: "group", y: "latency" } };
const rows = [{ group: "A", minimum: 1, lowerQuartile: 2, median: 3, upperQuartile: 4, maximum: 5 }];
assert.deepEqual(
validateInlineChartPresentation(summary, rows, originalInlineChartPresentation(summary)),
originalInlineChartPresentation(summary),
);
});
SHA-256: f0aed3848cda04d56021ab025a5c0c276405c7101fe2b1fa3dd2f9a71886c6dd