import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { describe, it } from "node:test"; import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js"; import { applyExternalFollowUpImport, applyExternalInitialImport, createExternalReturnWorkbook, deleteMissingExternalProjectObjects, detectExternalModelWorkbook, getExternalInitialImportStatus, getExternalModelConfiguration, getExternalProjectObjects, planExternalFollowUpImport, planExternalInitialImport, updateExternalModelConfiguration, updateExternalProjectObjects } from "../src/frontend/utils/api.js"; describe("Revit XLSX settings", () => { it("uses the project-scoped configuration endpoint with expectedRevision", 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)) : undefined }); return new Response(JSON.stringify({ configuration: null, history: { currentRevision: 8 } }), { status: 200, headers: { "Content-Type": "application/json" } }); }; try { const configuration = createDefaultExternalModelConfiguration(); await getExternalModelConfiguration("project-1"); await updateExternalModelConfiguration("project-1", 7, configuration); await detectExternalModelWorkbook("project-1", new File([new Uint8Array([1, 2])], "sample.xlsx"), configuration); assert.deepEqual(requests, [ { url: "/api/projects/project-1/external-model/configuration", method: "GET", body: undefined }, { url: "/api/projects/project-1/external-model/configuration", method: "PUT", body: { expectedRevision: 7, configuration } }, { url: "/api/projects/project-1/external-model/configuration/detect", method: "POST", body: { fileName: "sample.xlsx", contentBase64: "AQI=", configuration } }, ]); } finally { globalThis.fetch = originalFetch; } }); it("uses stateless preview and retransmits the workbook for apply", async () => { const requests: Array<{ url: string; method: string; body: Record | undefined }> = []; 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)) : undefined }); return new Response(JSON.stringify({ imported: false, familyGroups: [], sourceRooms: [], objects: [], warnings: [], issueCounts: {}, existing: {}, history: { currentRevision: 9 }, import: { objectCount: 2 } }), { status: 200, headers: { "Content-Type": "application/json" } }); }; try { const configuration = createDefaultExternalModelConfiguration(); const file = new File([new Uint8Array([1, 2])], "linear.xlsx"); await getExternalInitialImportStatus("project-1"); await planExternalInitialImport("project-1", file, configuration, 8); await applyExternalInitialImport("project-1", file, { expectedRevision: 8, expectedSha256: "a".repeat(64), sourceName: "Revit", configuration, objectsWithoutRoomDefaultDistributionBoardId: null, roomDecisions: [], familyProjectDeviceDecisions: [] }); assert.equal(requests[0].url, "/api/projects/project-1/external-model/import-status"); assert.equal(requests[1].url, "/api/projects/project-1/external-model/initial-import/plan"); assert.equal(requests[2].url, "/api/projects/project-1/external-model/initial-import/apply"); assert.equal(requests[1].body?.contentBase64, "AQI="); assert.equal(requests[1].body?.expectedRevision, 8); assert.equal(requests[2].body?.contentBase64, "AQI="); assert.equal(requests[2].body?.expectedRevision, 8); } finally { globalThis.fetch = originalFetch; } }); it("exposes the Revit route and removes the old project modal", () => { const projectPage = readFileSync("src/app/projects/[projectId]/page.tsx", "utf8"); const revitPage = readFileSync("src/app/projects/[projectId]/revit/page.tsx", "utf8"); const settings = readFileSync("src/frontend/components/revit-xlsx-settings-modal.tsx", "utf8"); const wizard = readFileSync("src/frontend/components/revit-initial-import-wizard.tsx", "utf8"); const followUpWizard = readFileSync("src/frontend/components/revit-follow-up-import-wizard.tsx", "utf8"); const objectTable = readFileSync("src/frontend/components/revit-object-table.tsx", "utf8"); assert.match(projectPage, /href={`\/projects\/\$\{projectId\}\/revit`}/); assert.doesNotMatch(projectPage, /RevitCsvModal|Revit-CSV/); assert.match(revitPage, /Importstatus/); assert.match(settings, /Musterdatei prüfen/); assert.match(settings, /Kombinierte Stromkreisspalte/); assert.match(settings, /isNewConfiguration/); assert.match(revitPage, /isNewConfiguration={snapshot === null}/); assert.match(wizard, /Schritt.*von/); assert.match(wizard, /keine Gerätezeilen, Stromkreise oder BMKs/); assert.match(wizard, /GZF 1/); assert.match(revitPage, /Folgeimport/); assert.match(followUpWizard, /in Revit noch nicht aktualisiert/); assert.match(followUpWizard, /Fehlende Objekte werden nicht gelöst oder gelöscht/); assert.match(objectTable, /viewportHeight/); assert.match(objectTable, /Sammeländerung anwenden/); assert.match(objectTable, /Rückgabe-Excel herunterladen/); assert.match(objectTable, /Nur geänderte Objekte/); assert.match(objectTable, /Ausgewählte fehlende löschen/); }); it("deletes missing objects only after explicit confirmation", 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)) : undefined }); return new Response(JSON.stringify({ history: { currentRevision: 6 } }), { status: 200, headers: { "Content-Type": "application/json" } }); }; try { await deleteMissingExternalProjectObjects("project-1", 5, ["object-1"]); } finally { globalThis.fetch = originalFetch; } assert.deepEqual(requests, [{ url: "/api/projects/project-1/external-model/objects/delete-missing", method: "POST", body: { expectedRevision: 5, objectIds: ["object-1"], confirmed: true } }]); }); it("loads one project object list and sends revision-safe bulk updates", 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)) : undefined }); return new Response(JSON.stringify({ currentRevision: 4, sourceName: null, distributionBoards: [], projectDevices: [], objects: [], history: { currentRevision: 5 } }), { status: 200, headers: { "Content-Type": "application/json" } }); }; try { await getExternalProjectObjects("project-1"); await updateExternalProjectObjects("project-1", 4, ["object-1"], { simultaneityFactor: 0.7 }); } finally { globalThis.fetch = originalFetch; } assert.deepEqual(requests, [{ url: "/api/projects/project-1/external-model/objects", method: "GET", body: undefined }, { url: "/api/projects/project-1/external-model/objects", method: "PUT", body: { expectedRevision: 4, objectIds: ["object-1"], patch: { simultaneityFactor: 0.7 } } }]); }); it("creates a revision-safe return workbook for the requested scope", 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)) : undefined }); return new Response(JSON.stringify({ fileName: "return.xlsx", contentBase64: "", sha256: "a".repeat(64), objectCount: 1, warnings: [] }), { status: 200, headers: { "Content-Type": "application/json" } }); }; try { await createExternalReturnWorkbook("project-1", 7, "selected", ["object-1"]); } finally { globalThis.fetch = originalFetch; } assert.deepEqual(requests, [{ url: "/api/projects/project-1/external-model/return-workbook", method: "POST", body: { expectedRevision: 7, scope: "selected", selectedObjectIds: ["object-1"] } }]); }); it("previews and retransmits the complete workbook for follow-up apply", async () => { const requests: Array<{ url: string; method: string; body: Record | undefined }> = []; 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)) : undefined }); return new Response(JSON.stringify({ history: { currentRevision: 10 }, import: { objectCount: 2 } }), { status: 200, headers: { "Content-Type": "application/json" } }); }; try { const configuration = createDefaultExternalModelConfiguration(); const file = new File([new Uint8Array([1, 2])], "follow.xlsx"); await planExternalFollowUpImport("project-1", file, configuration, 9); await applyExternalFollowUpImport("project-1", file, { expectedRevision: 9, expectedSha256: "a".repeat(64), configuration, objectsWithoutRoomDefaultDistributionBoardId: null, sourceChangeDecisions: [], roomDecisions: [], familyProjectDeviceDecisions: [] }); } finally { globalThis.fetch = originalFetch; } assert.equal(requests[0].url, "/api/projects/project-1/external-model/follow-up-import/plan"); assert.equal(requests[1].url, "/api/projects/project-1/external-model/follow-up-import/apply"); assert.equal(requests[0].body?.contentBase64, "AQI="); assert.equal(requests[1].body?.contentBase64, "AQI="); assert.equal(requests[1].body?.expectedRevision, 9); }); });