import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { buildCellClipboard, formatCellEditText, planCellWrites, resolveClipboardCells, resolvePasteWrites, } from "../src/frontend/utils/circuit-grid-clipboard.js"; import { buildVisibleGridRowsWithStructure } from "../src/frontend/utils/circuit-grid-projection.js"; import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto, CircuitTreeSectionDto, } from "../src/frontend/types.js"; function makeDevice(id: string, powerPerUnit: number): CircuitTreeDeviceRowDto { return { id, sortOrder: 10, name: `Gerät ${id}`, displayName: `Gerät ${id}`, phaseType: "single_phase", quantity: 1, manualQuantity: 1, powerPerUnit, simultaneityFactor: 1, rowTotalPower: powerPerUnit, }; } function makeCircuit( id: string, deviceRows: CircuitTreeDeviceRowDto[], extra: Partial = {} ): CircuitTreeCircuitDto { return { id, circuitListId: "list-1", sectionId: "section-1", equipmentIdentifier: `-2F${id.slice(-1)}`, displayName: id, sortOrder: 10, isReserve: deviceRows.length === 0, circuitTotalPower: 0, circuitApparentPower: 0, circuitCosPhi: 1, deviceRows, downstreamBoardLoads: [], ...extra, }; } const source = makeCircuit("circuit-1", [makeDevice("row-1", 0.25)], { cableType: "NYM-J", cableCrossSection: "3x2,5", cableLength: 18, protectionDevice: { type: "LS", ratedCurrentA: 16, tripCharacteristic: "B" }, }); const target = makeCircuit("circuit-2", [makeDevice("row-2", 0.1)], { protectionDevice: { type: "LS", ratedCurrentA: 10, tripCharacteristic: "B" }, }); const multi = makeCircuit("circuit-3", [ makeDevice("row-3a", 0.1), makeDevice("row-3b", 0.2), ]); const reserve = makeCircuit("circuit-4", []); const sections: CircuitTreeSectionDto[] = [ { id: "section-1", key: "single_phase", displayName: "Einphasig", prefix: "-2F", sortOrder: 10, sectionTotalPower: 0, components: [], circuits: [source, target, multi, reserve], }, ]; const rows = buildVisibleGridRowsWithStructure(sections, { headerComponents: [], footerComponents: [], }); const rowByKey = (rowKey: string) => { const row = rows.find((entry) => entry.rowKey === rowKey); assert.ok(row, rowKey); return row; }; describe("circuit grid clipboard", () => { it("formats numbers with a decimal comma for editing and copying", () => { assert.equal(formatCellEditText(0.25), "0,25"); assert.equal(formatCellEditText(1500), "1500"); assert.equal(formatCellEditText(true), "ja"); assert.equal(formatCellEditText(undefined), ""); }); it("copies protection and cable summaries as complete circuit records", () => { const clipboard = buildCellClipboard( [rowByKey("circuitCompact:circuit-1")], ["protectionSummary"] ); assert.equal(clipboard.cells[0][0].kind, "protection"); const plan = planCellWrites([ { row: rowByKey("circuitCompact:circuit-2"), cellKey: "protectionSummary", value: clipboard.cells[0][0], }, { row: rowByKey("circuitCompact:circuit-2"), cellKey: "cableSummary", value: buildCellClipboard( [rowByKey("circuitCompact:circuit-1")], ["cableSummary"] ).cells[0][0], }, ]); assert.equal(plan.changedCellCount, 2); assert.deepEqual( plan.updates.map((update) => [update.type, update.payload]), [ [ "circuit.update", { circuitId: "circuit-2", changes: [ { field: "cableType", value: "NYM-J" }, { field: "cableCrossSection", value: "3x2,5" }, { field: "cableLength", value: 18 }, ], }, ], [ "circuit-protection.update", { circuitId: "circuit-2", expected: { circuitId: "circuit-2", type: "LS", ratedCurrentA: 10, fuseUtilizationCategory: null, tripCharacteristic: "B", rcdType: null, ratedResidualCurrentMa: null, }, target: { circuitId: "circuit-2", type: "LS", ratedCurrentA: 16, fuseUtilizationCategory: null, tripCharacteristic: "B", rcdType: null, ratedResidualCurrentMa: null, }, }, ], ] ); }); it("fills one value into every selected row and skips rows that cannot take it", () => { const selectedRowKeys = [ "circuitCompact:circuit-1", "circuitCompact:circuit-2", "circuitSummary:circuit-3", "device:row-3a", ]; const { writes } = resolvePasteWrites({ cells: resolveClipboardCells("0,5", null), anchor: { rowKey: "circuitCompact:circuit-1", cellKey: "powerPerUnit" }, selectedRowKeys, rows, columnKeys: ["equipmentIdentifier", "displayName", "powerPerUnit"], }); assert.equal(writes.length, 4); const plan = planCellWrites(writes); // The summary row of a multi-device circuit has no device power. assert.equal(plan.skippedCellCount, 1); assert.deepEqual( plan.updates.map((update) => [update.type, update.payload]), ["row-1", "row-2", "row-3a"].map((rowId) => [ "circuit-device-row.update", { rowId, changes: [{ field: "powerPerUnit", value: 0.5 }] }, ]) ); }); it("pastes an Excel block down the visible rows and across visible columns", () => { const cells = resolveClipboardCells("0,3\t0,8\r\n0,4\t0,9\r\n", null); const { writes, overflowCellCount } = resolvePasteWrites({ cells, anchor: { rowKey: "circuitCompact:circuit-1", cellKey: "powerPerUnit" }, selectedRowKeys: [], rows, columnKeys: ["displayName", "powerPerUnit", "simultaneityFactor"], }); assert.equal(overflowCellCount, 0); const plan = planCellWrites(writes); assert.deepEqual( plan.updates.map((update) => update.payload), [ { rowId: "row-1", changes: [ { field: "powerPerUnit", value: 0.3 }, { field: "simultaneityFactor", value: 0.8 }, ], }, { rowId: "row-2", changes: [ { field: "powerPerUnit", value: 0.4 }, { field: "simultaneityFactor", value: 0.9 }, ], }, ] ); }); it("reuses structured cells only while the clipboard text is unchanged", () => { const clipboard = buildCellClipboard( [rowByKey("circuitCompact:circuit-1")], ["protectionSummary"] ); assert.equal( resolveClipboardCells(clipboard.text, clipboard)[0][0].kind, "protection" ); assert.equal( resolveClipboardCells("LS 16 A", clipboard)[0][0].kind, "text" ); }); it("drops unchanged values, blocks shared identifiers and rejects invalid numbers", () => { const unchanged = planCellWrites([ { row: rowByKey("circuitCompact:circuit-1"), cellKey: "powerPerUnit", value: { kind: "text", text: "0,25" }, }, ]); assert.deepEqual(unchanged, { updates: [], changedCellCount: 0, skippedCellCount: 0, }); const identifiers = planCellWrites( [ { row: rowByKey("circuitCompact:circuit-2"), cellKey: "equipmentIdentifier", value: { kind: "text", text: "-2F9" }, }, ], { allowEquipmentIdentifier: false } ); assert.equal(identifiers.skippedCellCount, 1); assert.equal(identifiers.updates.length, 0); assert.throws( () => planCellWrites([ { row: rowByKey("circuitCompact:circuit-1"), cellKey: "powerPerUnit", value: { kind: "text", text: "1.5" }, }, ]), /Komma/ ); }); });