import assert from "node:assert/strict"; import { describe, it } from "node:test"; import ExcelJS from "exceljs"; import { createExternalXlsxReturn, ExternalXlsxError, parseExternalXlsx, } from "../src/external-model/xlsx/external-xlsx-transport.js"; import { createRevitXlsxFixture, externalXlsxTestConfiguration, firstWorksharingId, secondWorksharingId, } from "./fixtures/revit-xlsx-fixtures.js"; describe("external XLSX transport", () => { it("reads typed cells and classifies ID-less rows without blocking summaries", async () => { const document = await parseExternalXlsx({ fileName: "linear.xlsx", bytes: await createRevitXlsxFixture(), configuration: externalXlsxTestConfiguration, }); assert.equal(document.sheetName, "Linear Export"); assert.equal(document.headerRowNumber, 1); assert.equal(document.firstDataRowNumber, 3); assert.equal(document.firstDataColumnNumber, 1); assert.deepEqual( document.rows.map((row) => row.classification), ["object", "object", "suspect-object", "passthrough"] ); assert.equal(document.rows[0].cells[5].value, 2); assert.equal(document.rows[0].cells[15].value, true); assert.equal(document.columns[0].comment, "Kommentar Worksharing-ID"); assert.equal(document.columns[0].width, 12); assert.deepEqual( document.issues.map((issue) => issue.code), ["suspect-object-skipped"] ); }); it("resolves editable layout positions without a separate ID column", async () => { const configuration = { ...externalXlsxTestConfiguration, layout: { headerRow: 2, firstDataRow: 5, firstDataColumn: 2 }, }; const document = await parseExternalXlsx({ fileName: "linear.xlsx", bytes: await createRevitXlsxFixture(configuration.layout), configuration, }); assert.equal(document.headerRowNumber, 2); assert.equal(document.firstDataRowNumber, 5); assert.equal(document.firstDataColumnNumber, 2); assert.equal(document.columns[1].header, "Worksharing-ID"); assert.equal(document.rows[0].cells[1].value, firstWorksharingId); assert.deepEqual( document.rows.map((row) => row.classification), ["object", "object", "suspect-object", "passthrough"] ); }); it("compares Worksharing-ID and IfcGUID exactly and case-sensitively", async () => { const lowerCaseId = secondWorksharingId.toLowerCase(); const document = await parseExternalXlsx({ fileName: "linear.xlsx", bytes: await createRevitXlsxFixture({ secondWorksharingId: lowerCaseId, firstIfcGuid: "IfcGUID", secondIfcGuid: "IFCGUID", }), configuration: externalXlsxTestConfiguration, }); assert.equal( document.rows.filter((row) => row.classification === "object").length, 2 ); assert.equal( document.issues.some((issue) => issue.code === "duplicate-ifc-guid"), false ); await assert.rejects( parseExternalXlsx({ fileName: "linear.xlsx", bytes: await createRevitXlsxFixture({ duplicateWorksharingId: true }), configuration: externalXlsxTestConfiguration, }), isXlsxError("duplicate-worksharing-id") ); const duplicateIfcDocument = await parseExternalXlsx({ fileName: "linear.xlsx", bytes: await createRevitXlsxFixture({ firstIfcGuid: "same", secondIfcGuid: "same", }), configuration: externalXlsxTestConfiguration, }); assert.equal( duplicateIfcDocument.issues.filter( (issue) => issue.code === "duplicate-ifc-guid" ).length, 1 ); }); it("uses cached formula values without evaluating formulas", async () => { const document = await parseExternalXlsx({ fileName: "linear.xlsx", bytes: await createRevitXlsxFixture({ includeFormula: true }), configuration: externalXlsxTestConfiguration, }); const powerCell = document.rows[0].cells[11]; assert.equal(powerCell.value, 250); assert.equal(powerCell.formula, "125*2"); assert.equal(powerCell.usedCachedFormulaValue, true); assert.equal( document.issues.some( (issue) => issue.code === "cached-formula-value-used" ), true ); }); it("blocks a required formula without a cached result", async () => { await assert.rejects( parseExternalXlsx({ fileName: "linear.xlsx", bytes: await createRevitXlsxFixture({ includeRequiredFormulaWithoutResult: true, }), configuration: externalXlsxTestConfiguration, }), isXlsxError("required-formula-result-missing") ); }); it("writes selected object rows in source order and preserves workbook structure", async () => { const source = await createRevitXlsxFixture({ includeFormula: true }); const result = await createExternalXlsxReturn({ fileName: "linear.xlsx", bytes: source, configuration: externalXlsxTestConfiguration, selectedWorksharingIds: [secondWorksharingId, firstWorksharingId], updates: [ { worksharingId: firstWorksharingId, values: { circuitCombined: "UV99-3F7", electricalPowerW: 321.5, voltageV: 400, currentA: 0.46, }, }, ], }); assert.equal(result.exportedObjectCount, 2); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(result.bytes as never); const worksheet = workbook.worksheets[0]; assert.equal(worksheet.name, "Linear Export"); assert.equal(worksheet.rowCount, 4); assert.equal(worksheet.getCell("A3").value, firstWorksharingId); assert.equal(worksheet.getCell("A4").value, secondWorksharingId); assert.equal(worksheet.getCell("H3").value, "UV99-3F7"); assert.equal(worksheet.getCell("L3").value, 321.5); assert.equal(worksheet.getCell("M3").value, 400); assert.equal(worksheet.getCell("N3").value, 0.46); assert.equal(worksheet.getCell("P4").value, "bleibt"); assert.equal(worksheet.getCell("L3").numFmt, "0.00"); assert.equal(worksheet.getColumn(1).width, 12); assert.equal( typeof worksheet.getCell("A1").note === "string" ? worksheet.getCell("A1").note : worksheet.getCell("A1").note?.texts?.[0]?.text, "Kommentar Worksharing-ID" ); const commentedCells: string[] = []; worksheet.eachRow({ includeEmpty: true }, (row) => { row.eachCell({ includeEmpty: true }, (cell) => { if (cell.note !== undefined) commentedCells.push(cell.address); }); }); assert.deepEqual( commentedCells, Array.from({ length: 16 }, (_, index) => worksheet.getCell(1, index + 1).address ) ); }); it("warns for missing optional return columns and blocks missing circuit columns", async () => { const source = await createRevitXlsxFixture(); const optionalMissing = { ...externalXlsxTestConfiguration, columns: { ...externalXlsxTestConfiguration.columns, designation: "Nicht vorhanden", }, }; const result = await createExternalXlsxReturn({ fileName: "linear.xlsx", bytes: source, configuration: optionalMissing, }); assert.equal( result.issues.some( (issue) => issue.code === "optional-return-column-missing" && issue.field === "designation" ), true ); await assert.rejects( createExternalXlsxReturn({ fileName: "linear.xlsx", bytes: source, configuration: { ...externalXlsxTestConfiguration, columns: { ...externalXlsxTestConfiguration.columns, circuitCombined: "Nicht vorhanden", }, }, }), isXlsxError("return-circuit-column-missing") ); }); it("enforces extension, byte size and post-read sheet limits", async () => { const source = await createRevitXlsxFixture(); await assert.rejects( parseExternalXlsx({ fileName: "linear.xlsm", bytes: source, configuration: externalXlsxTestConfiguration, }), isXlsxError("invalid-file-extension") ); await assert.rejects( parseExternalXlsx({ fileName: "linear.xlsx", bytes: new Uint8Array(10 * 1024 * 1024 + 1), configuration: externalXlsxTestConfiguration, }), isXlsxError("file-too-large") ); await assert.rejects( parseExternalXlsx({ fileName: "linear.xlsx", bytes: source, configuration: externalXlsxTestConfiguration, limits: { maxRows: 3, maxColumns: 100, maxCells: 1_000 }, }), isXlsxError("worksheet-too-large") ); }); }); function isXlsxError(code: ExternalXlsxError["code"]) { return (error: unknown) => error instanceof ExternalXlsxError && error.code === code; }