Files
NODEDC_DEVICE_CORE/apps/device-manager/server/device-manager-presentation.test.mjs

248 lines
9.8 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import {
createDeviceManagerPresentationStore,
defaultPresentation,
normalizeEnvironmentPresentation,
} from "./device-manager-presentation.mjs";
test("Device Core environment defaults to the product-level canonical identity", () => {
const presentation = defaultPresentation();
assert.equal(presentation.environment.theme, "dark");
assert.equal(presentation.environment.accentHex, "#f5f5f5");
assert.deepEqual(presentation.environment.overview, {
headerLabel: "Device Core",
eyebrow: "NODEDC / DEVICE CORE",
title: "Device Core",
description: "Единый контур подключения, учёта и управления устройствами.",
primarySection: "devices",
secondarySection: null,
background: {
enabled: false,
imageDurationSeconds: 10,
items: [],
},
});
});
test("environment accent keeps a valid user choice and falls back to neutral white", () => {
assert.equal(
normalizeEnvironmentPresentation({ accentHex: "#8A2BE2" }).accentHex,
"#8a2be2",
);
assert.equal(
normalizeEnvironmentPresentation({ accentHex: "not-a-color" }).accentHex,
"#f5f5f5",
);
});
test("production Compose provides exact persistent writable presentation storage", async () => {
const compose = await readFile(
new URL("../../../docker-compose.device-manager.yml", import.meta.url),
"utf8",
);
for (const expected of [
"NODEDC_DEVICE_MANAGER_PRESENTATION_PATH: /var/lib/nodedc-device-manager/device-manager-presentation.json",
"NODEDC_DEVICE_MANAGER_MEDIA_ROOT: /var/lib/nodedc-device-manager/media",
"source: /volume1/docker/nodedc-device-plane/data/device-manager",
"target: /var/lib/nodedc-device-manager",
"read_only: false",
"create_host_path: false",
]) assert.ok(compose.includes(expected), expected);
const client = await readFile(
new URL("../src/DeviceManagerApp.tsx", import.meta.url),
"utf8",
);
assert.ok(client.includes('accentHex: "#f5f5f5"'));
assert.ok(!client.includes("#b9ff4a"));
});
test("Device Core overview follows the Mission Core landing-stage geometry", async () => {
const styles = await readFile(
new URL("../src/styles.css", import.meta.url),
"utf8",
);
for (const expected of [
".device-manager-stage {\n height: 100%;\n overflow: hidden;",
"left: clamp(2rem, 5vw, 6rem);",
"width: min(39rem, 50%);",
"transform: translateY(-55%);",
"linear-gradient(90deg, rgb(5 6 8 / 0.82) 0%, rgb(5 6 8 / 0.54) 46%, rgb(5 6 8 / 0.24) 100%)",
"font-size: clamp(3rem, 7vw, 7.6rem);",
]) assert.ok(styles.includes(expected), expected);
assert.ok(!/\.device-manager-home\s*\{[^}]*max-width:\s*1420px/s.test(styles));
const client = await readFile(
new URL("../src/DeviceManagerApp.tsx", import.meta.url),
"utf8",
);
assert.ok(client.includes('className="device-manager-home"'));
assert.ok(client.includes('data-has-media={hasMedia ? "true" : undefined}'));
assert.ok(!client.includes('<GlassSurface className="device-manager-home"'));
});
test("VPS inventory and monitoring follow the Mission Core system workspace geometry", async () => {
const client = await readFile(
new URL("../src/DeviceControlViews.tsx", import.meta.url),
"utf8",
);
const styles = await readFile(
new URL("../src/styles.css", import.meta.url),
"utf8",
);
const infrastructureSource = client.slice(
client.indexOf("function HostsView"),
client.indexOf("function SessionsView"),
);
for (const expected of [
'className="infrastructure-system-workspace"',
'className="infrastructure-system-workspace host-monitoring-workspace"',
'className="infrastructure-overview-block"',
'className="infrastructure-section infrastructure-hosts-block"',
'className="infrastructure-host-list"',
'className="infrastructure-host-card__summary"',
'className="infrastructure-host-card__freshness"',
'className="infrastructure-host-card__toggle"',
'className="infrastructure-host-card__details"',
'className="infrastructure-host-relations"',
'aria-expanded={expanded}',
'const [expandedHostRefs, setExpandedHostRefs] = useState<Set<string>>',
'topology.endpoints.filter((item) => item.hostRef === host.hostRef)',
'topology.deployments.filter((item) => item.hostRef === host.hostRef)',
'topology.serviceInstances.filter((item) => item.hostRef === host.hostRef)',
'className="host-monitoring-series-grid"',
'className="host-monitoring-hardware"',
'className="host-monitoring-hardware-facts"',
'className="infrastructure-runtime-grid"',
'percentageTelemetryWindow(cpuHistory, 5)',
'percentageTelemetryWindow(memoryHistory, 4)',
'className="host-monitoring-series__range"',
'resource={networkRate.received == null ? "нет данных" : "входящий трафик"}',
'if (!counters.length) return null;',
]) assert.ok(infrastructureSource.includes(expected), expected);
for (const expected of [
".infrastructure-system-workspace {",
".infrastructure-overview-block,",
".infrastructure-hosts-block,",
".infrastructure-host-card__summary {",
"align-items: center;",
".infrastructure-host-card__freshness[data-freshness=\"fresh\"] {",
".infrastructure-host-card[data-expanded=\"true\"] .infrastructure-host-card__toggle svg {",
".infrastructure-host-relations {",
"grid-template-columns: repeat(3, minmax(0, 1fr));",
"grid-template-columns: repeat(4, minmax(0, 1fr));",
"background: var(--infrastructure-panel-soft);",
".host-monitoring-series polyline {",
"stroke: var(--nodedc-text-primary);",
".host-monitoring-hardware-facts {",
"grid-template-columns: repeat(2, minmax(0, 1fr));",
]) assert.ok(styles.includes(expected), expected);
assert.ok(!infrastructureSource.includes("<ResourceCard"));
assert.ok(!infrastructureSource.includes('title="Сервисы хостов"'));
assert.ok(!infrastructureSource.includes('title="Deployments и endpoints"'));
assert.ok(!infrastructureSource.includes("ceiling={100}"));
assert.ok(!infrastructureSource.includes("Канонический ontology catalog"));
assert.ok(!styles.includes(".host-telemetry-metric,"));
assert.ok(!styles.includes(".host-telemetry-facts"));
});
test("legacy single teaser migrates into the environment media playlist", () => {
const environment = normalizeEnvironmentPresentation({
defaultTeaser: {
source: "file",
fileName: "legacy.mov",
fileSrc: "/device-manager-media/background-00000000-0000-4000-8000-000000000000.mov",
},
});
assert.equal(environment.overview.background.enabled, true);
assert.equal(environment.overview.background.items.length, 1);
assert.equal(environment.overview.background.items[0].mediaKind, "video");
});
test("environment media accepts MOV even when the browser omits or varies its MIME", async (t) => {
const root = await mkdtemp(join(tmpdir(), "nodedc-device-presentation-"));
t.after(() => rm(root, { recursive: true, force: true }));
const store = createDeviceManagerPresentationStore({
layoutPath: join(root, "presentation.json"),
uploadRoot: join(root, "media"),
});
for (const [index, contentType] of ["video/quicktime", "video/x-quicktime", ""].entries()) {
const uploaded = await store.saveMedia({
bytes: Buffer.from(`mov-${index}`),
contentType,
originalName: `background-${index}.mov`,
kind: "background",
});
assert.match(uploaded.fileSrc, /^\/device-manager-media\/background-[0-9a-f-]+\.mov$/);
assert.equal(await readFile(store.resolveMedia(uploaded.fileSrc), "utf8"), `mov-${index}`);
}
});
test("environment media persists an MP4 upload in the configured data root", async (t) => {
const root = await mkdtemp(join(tmpdir(), "nodedc-device-presentation-mp4-"));
t.after(() => rm(root, { recursive: true, force: true }));
const store = createDeviceManagerPresentationStore({
layoutPath: join(root, "device-manager-presentation.json"),
uploadRoot: join(root, "media"),
});
const uploaded = await store.saveMedia({
bytes: Buffer.from("mp4-payload"),
contentType: "video/mp4",
originalName: "environment.mp4",
kind: "background",
});
assert.match(uploaded.fileSrc, /^\/device-manager-media\/background-[0-9a-f-]+\.mp4$/);
assert.equal(await readFile(store.resolveMedia(uploaded.fileSrc), "utf8"), "mp4-payload");
});
test("environment presentation persists ordered mixed media and image duration", async (t) => {
const root = await mkdtemp(join(tmpdir(), "nodedc-device-presentation-"));
t.after(() => rm(root, { recursive: true, force: true }));
const store = createDeviceManagerPresentationStore({
layoutPath: join(root, "presentation.json"),
uploadRoot: join(root, "media"),
});
const next = defaultPresentation();
next.environment.theme = "light";
next.environment.accentHex = "#8a2be2";
next.environment.overview.background = {
enabled: true,
imageDurationSeconds: 17,
items: [
{
id: "video-first",
source: "url",
url: "https://media.example/device.mov",
fileName: null,
fileSrc: null,
mediaKind: "video",
},
{
id: "image-second",
source: "url",
url: "https://media.example/device.webp",
fileName: null,
fileSrc: null,
mediaKind: "image",
},
],
};
await store.write(next);
const restored = await store.read();
assert.equal(restored.environment.theme, "light");
assert.equal(restored.environment.accentHex, "#8a2be2");
assert.equal(restored.environment.overview.background.imageDurationSeconds, 17);
assert.deepEqual(
restored.environment.overview.background.items.map((item) => item.id),
["video-first", "image-second"],
);
});