feat: add configurable view-aware dashboards
This commit is contained in:
170
webui/tests/dashboard-layout.test.ts
Normal file
170
webui/tests/dashboard-layout.test.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
appendPlacement,
|
||||
dashboardMasonryRowSpan,
|
||||
defaultDashboardLayout,
|
||||
layoutUpdatePayload,
|
||||
reconcileDashboardLayout,
|
||||
removePlacement,
|
||||
reorderPlacement
|
||||
} from "../src/features/dashboard/dashboardLayout.ts";
|
||||
import { dashboardGridPreview } from "../src/features/dashboard/dashboardEditorTypes.ts";
|
||||
|
||||
const widgets = [
|
||||
{
|
||||
id: "one",
|
||||
surfaceId: "one.widget",
|
||||
title: "One",
|
||||
moduleId: "one",
|
||||
defaultSize: "small" as const,
|
||||
supportedSizes: ["small", "medium"] as const,
|
||||
defaultConfiguration: { count: 3 },
|
||||
render: () => null
|
||||
},
|
||||
{
|
||||
id: "two",
|
||||
surfaceId: "two.widget",
|
||||
title: "Two",
|
||||
moduleId: "two",
|
||||
defaultVisible: false,
|
||||
defaultSize: "medium" as const,
|
||||
render: () => null
|
||||
}
|
||||
];
|
||||
|
||||
const defaults = defaultDashboardLayout(widgets);
|
||||
assert.deepEqual(defaults.knownWidgetIds, ["one", "two"]);
|
||||
assert.equal(defaults.placements.length, 1);
|
||||
assert.equal(defaults.placements[0].widgetId, "one");
|
||||
assert.deepEqual(defaults.placements[0].configuration, { count: 3 });
|
||||
|
||||
const withTwo = appendPlacement(defaults, widgets[1]);
|
||||
assert.deepEqual(
|
||||
withTwo.placements.map((placement) => placement.widgetId),
|
||||
["one", "two"]
|
||||
);
|
||||
assert.equal(
|
||||
appendPlacement(withTwo, widgets[1]).placements.length,
|
||||
2,
|
||||
"single-instance widgets must not be duplicated"
|
||||
);
|
||||
|
||||
const reordered = reorderPlacement(
|
||||
withTwo,
|
||||
withTwo.placements[1].instanceId,
|
||||
withTwo.placements[0].instanceId,
|
||||
"before"
|
||||
);
|
||||
assert.deepEqual(
|
||||
reordered.placements.map((placement) => placement.widgetId),
|
||||
["two", "one"]
|
||||
);
|
||||
|
||||
const removed = removePlacement(reordered, reordered.placements[0].instanceId);
|
||||
assert.deepEqual(
|
||||
removed.placements.map((placement) => placement.widgetId),
|
||||
["one"]
|
||||
);
|
||||
|
||||
const placementPreview = dashboardGridPreview(
|
||||
withTwo.placements,
|
||||
{
|
||||
kind: "placement",
|
||||
instanceId: withTwo.placements[0].instanceId
|
||||
},
|
||||
{
|
||||
kind: "placement",
|
||||
instanceId: withTwo.placements[1].instanceId,
|
||||
edge: "after"
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
placementPreview.map((item) =>
|
||||
item.kind === "placement"
|
||||
? `${item.placement.widgetId}:${item.placeholder}`
|
||||
: item.widgetId
|
||||
),
|
||||
["two:false", "one:true"],
|
||||
"placement previews must show the prospective order"
|
||||
);
|
||||
|
||||
const cataloguePreview = dashboardGridPreview(
|
||||
withTwo.placements,
|
||||
{ kind: "catalogue", widgetId: "three" },
|
||||
{
|
||||
kind: "placement",
|
||||
instanceId: withTwo.placements[0].instanceId,
|
||||
edge: "after"
|
||||
},
|
||||
"wide"
|
||||
);
|
||||
assert.deepEqual(
|
||||
cataloguePreview.map((item) =>
|
||||
item.kind === "placement"
|
||||
? item.placement.widgetId
|
||||
: `${item.widgetId}:${item.size}`
|
||||
),
|
||||
["one", "three:wide", "two"],
|
||||
"catalogue previews must reserve the prospective widget size"
|
||||
);
|
||||
|
||||
const unavailablePlacement = {
|
||||
instanceId: "unavailable-instance",
|
||||
widgetId: "temporarily-disabled",
|
||||
size: "wide" as const,
|
||||
configuration: { mode: "summary" }
|
||||
};
|
||||
const reconciled = reconcileDashboardLayout(
|
||||
{
|
||||
layoutVersion: 1,
|
||||
revision: 4,
|
||||
knownWidgetIds: ["temporarily-disabled"],
|
||||
placements: [unavailablePlacement]
|
||||
},
|
||||
widgets
|
||||
);
|
||||
assert.equal(
|
||||
reconciled.placements[0],
|
||||
unavailablePlacement,
|
||||
"temporarily unavailable contributions must retain their placement"
|
||||
);
|
||||
assert.equal(
|
||||
reconciled.placements[1].widgetId,
|
||||
"one",
|
||||
"new default-visible widgets are announced into existing layouts"
|
||||
);
|
||||
assert.equal(reconciled.revision, 4);
|
||||
|
||||
const payload = layoutUpdatePayload(reconciled);
|
||||
assert.equal(payload.expected_revision, 4);
|
||||
assert.equal(payload.placements[0].instance_id, "unavailable-instance");
|
||||
assert.equal(payload.placements[0].configuration.mode, "summary");
|
||||
|
||||
const repeatingWidget = {
|
||||
...widgets[0],
|
||||
id: "repeating",
|
||||
allowMultiple: true,
|
||||
defaultVisible: false
|
||||
};
|
||||
let bounded = defaultDashboardLayout([repeatingWidget]);
|
||||
for (let index = 0; index < 105; index += 1) {
|
||||
bounded = appendPlacement(bounded, repeatingWidget);
|
||||
}
|
||||
assert.equal(
|
||||
bounded.placements.length,
|
||||
100,
|
||||
"client layout helpers must enforce the server placement limit"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
dashboardMasonryRowSpan(142, 4, 18),
|
||||
40,
|
||||
"masonry spans must reserve the widget height and inter-widget gap"
|
||||
);
|
||||
assert.equal(
|
||||
dashboardMasonryRowSpan(0, Number.NaN, Number.NaN),
|
||||
1,
|
||||
"masonry spans must remain valid while a widget is mounting"
|
||||
);
|
||||
|
||||
console.log("Dashboard layout tests passed.");
|
||||
Reference in New Issue
Block a user