feat(charts): manage investigation charts in a scrolling workspace
Generic Container CI/CD / test-build-publish (push) Successful in 2m7s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m8s

This commit is contained in:
2026-11-10 17:56:23 +08:00
parent 865491cb23
commit 0ae0a41e61
80 changed files with 4672 additions and 470 deletions
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { columnsForLayout, reconcileChartSlots, swapChartSlots, type ChartSlot } from "./chart-window-slots";
describe("fixed chart slots", () => {
it("maps supported layouts to grid columns", () => {
expect([columnsForLayout("one"), columnsForLayout("two"), columnsForLayout("four")]).toEqual([1, 2, 2]);
});
it("keeps chart positions when publications update in another order", () => {
const slots: ChartSlot[] = ["flow", null, "pressure", "plans"];
expect(reconcileChartSlots(slots, ["plans", "pressure", "flow"], 2)).toBe(slots);
});
it("leaves deleted positions vacant without moving surviving charts", () => {
expect(reconcileChartSlots(["pressure", "flow", "plans", null], ["pressure", "plans"], 2))
.toEqual(["pressure", null, "plans", null]);
});
it("fills the earliest vacant slots before appending a padded row", () => {
expect(reconcileChartSlots(["pressure", null, "flow", null], ["new-1", "flow", "new-2", "pressure", "new-3"], 2))
.toEqual(["pressure", "new-1", "flow", "new-2", "new-3", null]);
});
it("uses vacancies created by a deletion in the same update", () => {
expect(reconcileChartSlots(["pressure", "removed", "flow", null], ["pressure", "flow", "new"], 2))
.toEqual(["pressure", "new", "flow", null]);
});
it("preserves linear positions when the column count changes", () => {
const slots: ChartSlot[] = ["a", null, "b", "c", "d", null];
expect(reconcileChartSlots(slots, ["a", "b", "c", "d"], 1)).toEqual(["a", null, "b", "c", "d"]);
expect(reconcileChartSlots(slots, ["a", "b", "c", "d"], 2)).toBe(slots);
});
it("trims only empty trailing rows, retaining leading and middle vacancies", () => {
expect(reconcileChartSlots([null, null, "a", null, null, null, null, null], ["a"], 2))
.toEqual([null, null, "a", null]);
expect(reconcileChartSlots(["a", null, null, null], ["a"], 2)).toEqual(["a", null]);
expect(reconcileChartSlots(["a", null, null, null], [], 2)).toEqual([]);
});
it("builds default slots in available order and never duplicates a chart", () => {
expect(reconcileChartSlots([], ["a", "b", "a"], 2)).toEqual(["a", "b"]);
expect(reconcileChartSlots(["a", "a", "b", null], ["a", "b", "c"], 2)).toEqual(["a", "c", "b", null]);
});
it("swaps occupied slots without inserting or shifting adjacent charts", () => {
const slots: ChartSlot[] = ["a", "b", "c", "d"];
expect(swapChartSlots(slots, 0, 2)).toEqual(["c", "b", "a", "d"]);
expect(slots).toEqual(["a", "b", "c", "d"]);
});
it("swaps across rows and permits an empty destination", () => {
expect(swapChartSlots(["a", "b", "c", "d", "e", null, null, null], 0, 5))
.toEqual([null, "b", "c", "d", "e", "a", null, null]);
});
it("keeps the original array for invalid or unchanged swaps", () => {
const slots: ChartSlot[] = ["a", null, null];
for (const [source, target] of [[-1, 1], [0, 3], [3, 0], [0.5, 1], [0, Number.NaN], [0, 0], [1, 2]]) {
expect(swapChartSlots(slots, source, target)).toBe(slots);
}
});
});