leistungsbilanz-ts/tests/external-xlsx-transport.test.ts
Julian Appel 7222398628 Typecheck tests against current types
tsx runs tests without type checking and tsconfig.json covers only src,
so 71 type errors had accumulated unnoticed. tsconfig.tests.json now
checks every test and helper, and npm run typecheck includes it.

Fixtures gain the fields the DTOs have grown since they were written
(manualQuantity, circuit apparent power/cos phi, downstream board loads,
section totals, building, cover and board info fields). Repository
lookups that return "row ?? null" now declare the null in their type,
which their callers already handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:17:45 +01:00

268 lines
8.7 KiB
TypeScript

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);
const worksharingNote = worksheet.getCell("A1").note;
assert.equal(
typeof worksharingNote === "string"
? worksharingNote
: worksharingNote?.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;
}