import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { ProjectVersionHistory } from "../src/frontend/components/project-version-history.js"; import { ProjectDeviceModal } from "../src/frontend/components/project-device-modal.js"; import { ProjectSettingsModal } from "../src/frontend/components/project-settings-modal.js"; import type { ProjectRevisionSummaryDto, ProjectSnapshotMetadataDto, } from "../src/frontend/types.js"; import { createNamedProjectSnapshot, getProjectHistory, listProjectRevisions, listProjectSnapshots, redoProjectCommand, restoreProjectSnapshot, undoProjectCommand, } from "../src/frontend/utils/api.js"; import { getProjectRevisionDescription, getProjectRevisionSourceLabel, getProjectSnapshotKindLabel, getProjectSnapshotRevisionDescription, mergeProjectRevisionPages, } from "../src/frontend/utils/project-version-history.js"; function revision( revisionNumber: number, overrides: Partial = {} ): ProjectRevisionSummaryDto { return { revisionId: `revision-${revisionNumber}`, changeSetId: `change-${revisionNumber}`, revisionNumber, createdAtIso: "2026-07-25T12:00:00.000Z", actorId: null, source: "user", description: null, commandType: "circuit.update", payloadSchemaVersion: 1, ...overrides, }; } function snapshot( sourceRevision: number, sourceRevisionMetadata: ProjectRevisionSummaryDto | null ): ProjectSnapshotMetadataDto { return { id: `snapshot-${sourceRevision}`, projectId: "project-1", sourceRevision, sourceRevisionMetadata, schemaVersion: 5, kind: "automatic", name: `Revision ${sourceRevision}`, description: null, payloadSha256: "checksum", createdAtIso: "2026-07-25T12:00:00.000Z", createdByActorId: null, }; } describe("project version history presentation", () => { it("renders a compact collapsed German entry point", () => { const markup = renderToStaticMarkup( createElement(ProjectVersionHistory, { projectId: "project-1", currentRevision: 42, onProjectStateChange: () => undefined, }) ); assert.match(markup, /Versionen und Sicherungspunkte/); assert.match(markup, /Aktuelle Revision 42/); assert.match(markup, /Rückgängig/); assert.match(markup, /Wiederholen/); assert.match(markup, /Anzeigen/); assert.doesNotMatch(markup, /Stand speichern/); }); it("uses German source and command descriptions", () => { assert.equal(getProjectRevisionSourceLabel("user"), "Bearbeitung"); assert.equal(getProjectRevisionSourceLabel("undo"), "Rückgängig"); assert.equal(getProjectRevisionSourceLabel("redo"), "Wiederholt"); assert.equal( getProjectRevisionSourceLabel("restore"), "Wiederherstellung" ); assert.equal(getProjectRevisionSourceLabel("migration"), "Migration"); assert.equal( getProjectRevisionDescription(revision(1)), "Stromkreis bearbeitet" ); assert.equal( getProjectRevisionDescription( revision(2, { description: " Eigene Beschreibung " }) ), "Eigene Beschreibung" ); assert.equal( getProjectRevisionDescription( revision(3, { commandType: "future.command" }) ), "future.command" ); assert.equal( getProjectRevisionDescription( revision(4, { source: "undo", description: "Undo project.update-settings", commandType: "project.update-settings", }) ), "Projekteinstellungen bearbeitet" ); assert.equal(getProjectSnapshotKindLabel("named"), "Benannt"); assert.equal( getProjectSnapshotKindLabel("automatic"), "Automatisch" ); }); it("merges paginated revisions without duplicates in descending order", () => { assert.deepEqual( mergeProjectRevisionPages( [revision(5), revision(4)], [revision(4), revision(3)] ).map((entry) => entry.revisionNumber), [5, 4, 3] ); }); it("describes the immutable source revision of a snapshot", () => { assert.equal( getProjectSnapshotRevisionDescription( snapshot( 25, revision(25, { description: "Verteilung UV-01 angelegt", }) ) ), "Bearbeitung: Verteilung UV-01 angelegt" ); assert.equal( getProjectSnapshotRevisionDescription(snapshot(0, null)), "Projektstart" ); assert.equal( getProjectSnapshotRevisionDescription(snapshot(25, null)), "Änderungsdetails nicht verfügbar" ); }); }); describe("project settings presentation", () => { it("shows all supply types and marks types already in use", () => { const markup = renderToStaticMarkup( createElement(ProjectSettingsModal, { isSaving: false, onClose: () => undefined, onExport: async () => undefined, onImport: async () => undefined, onSave: async () => undefined, project: { id: "project-1", name: "Projekt", internalProjectNumber: null, externalProjectNumber: null, buildingOwner: null, description: null, singlePhaseVoltageV: 230, threePhaseVoltageV: 400, enabledDistributionBoardSupplyTypes: [ "AV", "MSR", "SiBe", ], currentRevision: 0, }, usedDistributionBoardSupplyTypes: ["SiBe"], }) ); assert.match(markup, /MSR/); assert.match(markup, /SiBe/); assert.match(markup, /in Verwendung/); }); }); describe("project device modal presentation", () => { it("renders explicit German labels for every editable device field", () => { const markup = renderToStaticMarkup( createElement(ProjectDeviceModal, { globalDevices: [], isSaving: false, onClose: () => undefined, onImportGlobal: async () => undefined, onSave: async () => undefined, }) ); for (const label of [ "Projektgerät hinzufügen", "Interner Name", "Anzeigename", "Kategorie", "Anschlussart", "Kostengruppe", "Phasenart", "Anzahl", "Leistung je Stück [kW]", "Gleichzeitigkeitsfaktor", "Bemerkung", ]) { assert.match(markup, new RegExp(label.replace("[", "\\[").replace("]", "\\]"))); } assert.doesNotMatch(markup, /Spannung \[V\]/); }); }); describe("project version history API", () => { it("uses the revision-safe timeline and snapshot routes", async () => { const requests: Array<{ url: string; method: string; body: unknown; }> = []; const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { requests.push({ url: String(input), method: init?.method ?? "GET", body: init?.body ? JSON.parse(String(init.body)) : null, }); return new Response(JSON.stringify({}), { status: 200, headers: { "Content-Type": "application/json" }, }); }; try { await getProjectHistory("project-1"); await listProjectRevisions("project-1", { limit: 10, beforeRevision: 21, }); await listProjectSnapshots("project-1"); await createNamedProjectSnapshot( "project-1", 22, "Planfreigabe", "Vor Ausführung" ); await restoreProjectSnapshot("project-1", "snapshot-1", 22); await undoProjectCommand("project-1", 23); await redoProjectCommand("project-1", 24); } finally { globalThis.fetch = originalFetch; } assert.deepEqual(requests, [ { url: "/api/projects/project-1/history", method: "GET", body: null, }, { url: "/api/projects/project-1/history/revisions?limit=10&beforeRevision=21", method: "GET", body: null, }, { url: "/api/projects/project-1/snapshots", method: "GET", body: null, }, { url: "/api/projects/project-1/snapshots", method: "POST", body: { expectedRevision: 22, name: "Planfreigabe", description: "Vor Ausführung", }, }, { url: "/api/projects/project-1/snapshots/snapshot-1/restore", method: "POST", body: { expectedRevision: 22 }, }, { url: "/api/projects/project-1/history/undo", method: "POST", body: { expectedRevision: 23 }, }, { url: "/api/projects/project-1/history/redo", method: "POST", body: { expectedRevision: 24 }, }, ]); }); });