267 lines
8.7 KiB
TypeScript
267 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);
|
|
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;
|
|
}
|