NODEDC_MISSION_CORE/apps/control-station/test/workspaceLayout.test.mjs

244 lines
8.4 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let decodeObservationWorkspaceLayoutProfile;
let encodeObservationWorkspaceLayoutProfile;
let fetchObservationWorkspaceLayoutProfile;
let normalizeObservationWindowRect;
let projectObservationLayoutSnapshot;
let saveObservationWorkspaceLayoutProfile;
let WorkspaceLayoutApiError;
let WorkspaceLayoutContractError;
let observationPresentationSourceAfterLayoutApply;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
decodeObservationWorkspaceLayoutProfile,
encodeObservationWorkspaceLayoutProfile,
fetchObservationWorkspaceLayoutProfile,
normalizeObservationWindowRect,
projectObservationLayoutSnapshot,
saveObservationWorkspaceLayoutProfile,
WorkspaceLayoutApiError,
WorkspaceLayoutContractError,
} = await server.ssrLoadModule("/src/core/observation/workspaceLayout.ts"));
({ observationPresentationSourceAfterLayoutApply } = await server.ssrLoadModule(
"/src/core/observation/useObservationLayout.ts",
));
});
after(async () => {
await server?.close();
});
function wireProfile(overrides = {}) {
return {
version: 2,
revision: 7,
workspace_id: "observation.spatial",
scene_settings: {
projection: "3d",
point_size: 2.5,
color_mode: "intensity",
palette: "turbo",
custom_color: "#35d7c1",
accumulation_seconds: 12,
show_points: true,
show_trajectory: true,
show_grid: true,
show_labels: false,
show_camera_frustums: true,
},
visible_source_ids: ["spatial.point-cloud.live", "camera.left"],
active_floating_source_id: "camera.left",
window_rects: {
"camera.left": { x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
"camera.right": { x: 0.55, y: 0.1, width: 0.4, height: 0.4 },
},
viewport_size: { width: 1_000, height: 500 },
...overrides,
};
}
test("workspace layout v2 excludes transient tool-window state", () => {
const wire = wireProfile();
const profile = decodeObservationWorkspaceLayoutProfile(wire);
assert.deepEqual(profile.visibleSourceIds, ["spatial.point-cloud.live", "camera.left"]);
assert.equal(profile.sceneSettings.pointSize, 2.5);
assert.deepEqual(encodeObservationWorkspaceLayoutProfile(profile), wire);
assert.equal("tool_windows" in encodeObservationWorkspaceLayoutProfile(profile), false);
});
test("workspace layout rejects unknown fields, transient transport data and invalid schema values", () => {
assert.throws(
() => decodeObservationWorkspaceLayoutProfile({
...wireProfile(),
source_url: "ws://192.168.68.52:9877",
}),
WorkspaceLayoutContractError,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({ version: 1 })),
/Неподдерживаемая версия/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({ revision: 1.5 })),
/revision/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({
visible_source_ids: ["../camera"],
active_floating_source_id: null,
})),
/стабильный идентификатор/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({
window_rects: { "camera.left": { x: 0.8, y: 0, width: 0.4, height: 0.5 } },
})),
/выходит за нормализованные границы/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile({
...wireProfile(),
tool_windows: {
sources_open: true,
display_open: true,
layers_open: true,
order: ["sources", "display", "layers"],
},
}),
/неверная схема/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({
scene_settings: { ...wireProfile().scene_settings, point_size: Number.POSITIVE_INFINITY },
})),
/point_size/,
);
});
test("restored desired layout survives an empty catalog and reveals only known stable sources later", () => {
const profile = decodeObservationWorkspaceLayoutProfile(wireProfile());
const snapshot = {
visibleSourceIds: profile.visibleSourceIds,
activeFloatingSourceId: profile.activeFloatingSourceId,
windowRects: profile.windowRects,
viewportSize: profile.viewportSize,
};
const empty = projectObservationLayoutSnapshot(snapshot, new Set(), { width: 500, height: 1_000 });
assert.deepEqual(empty, {
visibleSourceIds: [],
activeFloatingSourceId: null,
windowRects: {},
});
assert.deepEqual(snapshot.visibleSourceIds, ["spatial.point-cloud.live", "camera.left"]);
const cameraAppeared = projectObservationLayoutSnapshot(
snapshot,
new Set(["camera.left"]),
{ width: 500, height: 1_000 },
);
assert.deepEqual(cameraAppeared.visibleSourceIds, ["camera.left"]);
assert.equal(cameraAppeared.activeFloatingSourceId, "camera.left");
assert.deepEqual(cameraAppeared.windowRects["camera.left"], {
x: 50,
y: 100,
width: 200,
height: 400,
});
});
test("window rectangles normalize once and project proportionally into a different viewport", () => {
assert.deepEqual(
normalizeObservationWindowRect(
{ x: 100, y: 50, width: 400, height: 200 },
{ width: 1_000, height: 500 },
),
{ x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
);
});
test("fullscreen presentation survives the viewport resize it causes", () => {
assert.equal(
observationPresentationSourceAfterLayoutApply("camera.left", "preserve"),
"camera.left",
);
assert.equal(
observationPresentationSourceAfterLayoutApply("camera.left", "reset"),
null,
);
});
test("workspace layout API uses the canonical endpoint and optimistic revision", async () => {
const calls = [];
const current = decodeObservationWorkspaceLayoutProfile(wireProfile());
const saved = await saveObservationWorkspaceLayoutProfile(current, {
fetcher: async (input, init) => {
calls.push({ input: String(input), init, body: JSON.parse(String(init.body)) });
return new Response(JSON.stringify(wireProfile({ revision: 8 })), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
});
assert.equal(calls.length, 1);
assert.equal(calls[0].input, "/api/v1/workspace-layouts/observation.spatial");
assert.equal(calls[0].init.method, "PUT");
assert.equal(calls[0].init.headers["If-Match"], '"7"');
assert.equal(calls[0].body.revision, 7);
assert.equal(calls[0].body.workspace_id, "observation.spatial");
assert.equal(saved.revision, 8);
});
test("workspace layout API treats 404 as no profile and exposes revision conflicts", async () => {
assert.equal(
await fetchObservationWorkspaceLayoutProfile({
fetcher: async () => new Response(null, { status: 404 }),
}),
null,
);
const current = decodeObservationWorkspaceLayoutProfile(wireProfile());
await assert.rejects(
saveObservationWorkspaceLayoutProfile(current, {
fetcher: async () => new Response(JSON.stringify({ detail: "revision mismatch" }), {
status: 412,
headers: { "Content-Type": "application/json" },
}),
}),
(error) => error instanceof WorkspaceLayoutApiError &&
error.conflict && error.status === 412 && error.message === "revision mismatch",
);
});
test("scene tool windows are route-scoped and toolbar actions are not duplicated", async () => {
const appSource = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
const utilityStart = appSource.indexOf("const contentActions");
const utilityEnd = appSource.indexOf("const header", utilityStart);
const utilitySource = appSource.slice(utilityStart, utilityEnd);
assert.match(appSource, /open=\{spatialWorkspaceActive && sourceWindowOpen\}/);
assert.match(appSource, /open=\{spatialWorkspaceActive && displayWindowOpen\}/);
assert.match(appSource, /open=\{spatialWorkspaceActive && layerInspectorOpen\}/);
assert.match(
appSource,
/if \(spatialWorkspaceActive\) return;[\s\S]*setSceneWindowOrder\(\[\]\)/,
);
assert.doesNotMatch(utilitySource, /Настроить визуальный движок/);
assert.doesNotMatch(utilitySource, /Настроить отображение/);
assert.doesNotMatch(utilitySource, /Открыть слои/);
});