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("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"], ); });