diff --git a/index.ts b/index.ts index 70d2699..187e5a8 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,4 @@ -export { - TreeSelection, -} from "./foundation/utils.js"; +export { TreeSelection } from "./foundation/utils.js"; export { updateBay } from "./tBay/updateBay.js"; export { updateVoltageLevel } from "./tVoltageLevel/updateVoltageLevel.js"; @@ -69,8 +67,6 @@ export { export { sourceControlBlock } from "./tExtRef/sourceControlBlock.js"; export { isSubscribed } from "./tExtRef/isSubscribed.js"; - - export { LNodeDescription, NameSpaceDescription, @@ -79,7 +75,10 @@ export { export { insertSelectedLNodeType } from "./tDataTypeTemplates/insertSelectedLNodeType.js"; -export { removeDataType, RemoveDataTypeOptions } from "./tDataTypeTemplates/removeDataType.js" +export { + removeDataType, + RemoveDataTypeOptions, +} from "./tDataTypeTemplates/removeDataType.js"; export { importLNodeType } from "./tDataTypeTemplates/importLNodeType.js"; export { updateLNodeType } from "./tDataTypeTemplates/updateLNodeType.js"; export { lNodeTypeToSelection } from "./tDataTypeTemplates/lNodeTypeToSelection.js"; diff --git a/tAddress/changeGseOrSmvAddress.ts b/tAddress/changeGseOrSmvAddress.ts index 78d4443..50985e5 100644 --- a/tAddress/changeGseOrSmvAddress.ts +++ b/tAddress/changeGseOrSmvAddress.ts @@ -1,4 +1,4 @@ -import { Insert, Remove } from "@openscd/oscd-api" +import { Insert, Remove } from "@openscd/oscd-api"; import { createElement } from "../foundation/utils.js"; import { getReference } from "../tBaseElement/getReference.js"; diff --git a/tBay/updateBay.spec.ts b/tBay/updateBay.spec.ts index c92e6b6..69a1295 100644 --- a/tBay/updateBay.spec.ts +++ b/tBay/updateBay.spec.ts @@ -119,7 +119,8 @@ describe("update Bay element", () => { .filter(isSetAttributes) .filter( ({ attributes }) => - attributes?.connectivityNode === "AA1/E1/Q03/L1" && attributes?.bayName === "Q03", + attributes?.connectivityNode === "AA1/E1/Q03/L1" && + attributes?.bayName === "Q03", ), ).to.have.lengthOf(2); expect( @@ -127,7 +128,8 @@ describe("update Bay element", () => { .filter(isSetAttributes) .find( ({ attributes }) => - attributes?.connectivityNode === "AA1/E1/Q03/L2" && attributes?.bayName === "Q03", + attributes?.connectivityNode === "AA1/E1/Q03/L2" && + attributes?.bayName === "Q03", ), ).to.exist; }); diff --git a/tControl/removeControlBlock.ts b/tControl/removeControlBlock.ts index e0cf68b..d6e4e76 100644 --- a/tControl/removeControlBlock.ts +++ b/tControl/removeControlBlock.ts @@ -29,7 +29,9 @@ export function removeControlBlock(remove: Remove): (Remove | SetAttributes)[] { const controlBlock = remove.node as Element; - const ctrlBlockRemoveAction: (Remove | SetAttributes)[] = [{ node: controlBlock }]; + const ctrlBlockRemoveAction: (Remove | SetAttributes)[] = [ + { node: controlBlock }, + ]; const dataSet = controlBlock.parentElement?.querySelector( `DataSet[name="${controlBlock.getAttribute("datSet")}"]`, diff --git a/tControl/updateDatSet.ts b/tControl/updateDatSet.ts index df8d919..d188c7f 100644 --- a/tControl/updateDatSet.ts +++ b/tControl/updateDatSet.ts @@ -2,11 +2,13 @@ import { SetAttributes } from "@openscd/oscd-api"; const controlBlockTags = ["ReportControl", "GSEControl", "SampledValueControl"]; -/** +/** * On update the `datSet` attribute of a given control block this function will change * `DataSet.name` attribute as well if the `DataSet` is only used by this control block. */ -export function updateDatSet(setAttributes: SetAttributes): SetAttributes | null { +export function updateDatSet( + setAttributes: SetAttributes, +): SetAttributes | null { const newDatSet = setAttributes?.attributes?.datSet; const controlBlock = setAttributes.element; const oldDatSet = controlBlock.getAttribute("datSet"); diff --git a/tDataSet/removeDataSet.ts b/tDataSet/removeDataSet.ts index de7f12e..9b3d954 100644 --- a/tDataSet/removeDataSet.ts +++ b/tDataSet/removeDataSet.ts @@ -26,12 +26,12 @@ export function removeDataSet(remove: Remove): (Remove | SetAttributes)[] { const extRefEdits: (Remove | SetAttributes)[] = []; extRefEdits.push(...unsubscribe(extRefs)); - const ctrlBlockUpdates: (Remove | SetAttributes)[] = controlBlocks(dataSet).map( - (ctrlBlock) => ({ - element: ctrlBlock, - attributes: { datSet: null, confRev: updatedConfRev(ctrlBlock) }, - }), - ); + const ctrlBlockUpdates: (Remove | SetAttributes)[] = controlBlocks( + dataSet, + ).map((ctrlBlock) => ({ + element: ctrlBlock, + attributes: { datSet: null, confRev: updatedConfRev(ctrlBlock) }, + })); return dataSetRemove.concat(extRefEdits, ctrlBlockUpdates); } diff --git a/tDataTypeTemplates/foundation.ts b/tDataTypeTemplates/foundation.ts index aff274c..5e5a263 100644 --- a/tDataTypeTemplates/foundation.ts +++ b/tDataTypeTemplates/foundation.ts @@ -1,142 +1,143 @@ - - function describeEnumType(element: Element): { vals: Record } { - const vals: Record = {}; + const vals: Record = {}; - const sortedEnumVals = Array.from(element.children) - .filter((child) => child.tagName === "EnumVal") - .sort( - (v1, v2) => - parseInt(v1.getAttribute("ord")!, 10) - - parseInt(v2.getAttribute("ord")!, 10), - ); - for (const val of sortedEnumVals) - vals[val.getAttribute("ord")!] = val.textContent ?? ""; + const sortedEnumVals = Array.from(element.children) + .filter((child) => child.tagName === "EnumVal") + .sort( + (v1, v2) => + parseInt(v1.getAttribute("ord")!, 10) - + parseInt(v2.getAttribute("ord")!, 10), + ); + for (const val of sortedEnumVals) + vals[val.getAttribute("ord")!] = val.textContent ?? ""; - return { vals }; + return { vals }; } function describeDAType(element: Element): { - bdas: Record>; + bdas: Record>; } { - const bdas: Record> = {}; - for (const bda of Array.from(element.children) - .filter((child) => child.tagName === "BDA") - .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { - const [bType, type, dchg, dupd, qchg] = [ - "bType", - "type", - "dchg", - "dupd", - "qchg", - ].map((attr) => bda.getAttribute(attr)); - bdas[bda.getAttribute("name")!] = { bType, type, dchg, dupd, qchg }; - } - return { bdas }; + const bdas: Record> = {}; + for (const bda of Array.from(element.children) + .filter((child) => child.tagName === "BDA") + .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { + const [bType, type, dchg, dupd, qchg] = [ + "bType", + "type", + "dchg", + "dupd", + "qchg", + ].map((attr) => bda.getAttribute(attr)); + bdas[bda.getAttribute("name")!] = { bType, type, dchg, dupd, qchg }; + } + return { bdas }; } function describeDOType(element: Element) { - const sdos: Record> = {}; - for (const sdo of Array.from(element.children) - .filter((child) => child.tagName === "SDO") - .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { - const [name, type, transient] = ["name", "type", "transient"].map((attr) => - sdo.getAttribute(attr), - ); - sdos[name!] = { type, transient }; - } - const das: Record> = {}; - for (const da of Array.from(element.children) - .filter((child) => child.tagName === "DA") - .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { - const [name, fc, bType, type, dchg, dupd, qchg] = [ - "name", - "fc", - "bType", - "type", - "dchg", - "dupd", - "qchg", - ].map((attr) => da.getAttribute(attr)); - das[name!] = { - fc, - bType, - type, - dchg, - dupd, - qchg, - }; - } - return { - sdos, - das, - cdc: element.getAttribute("cdc"), + const sdos: Record> = {}; + for (const sdo of Array.from(element.children) + .filter((child) => child.tagName === "SDO") + .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { + const [name, type, transient] = ["name", "type", "transient"].map((attr) => + sdo.getAttribute(attr), + ); + sdos[name!] = { type, transient }; + } + const das: Record> = {}; + for (const da of Array.from(element.children) + .filter((child) => child.tagName === "DA") + .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { + const [name, fc, bType, type, dchg, dupd, qchg] = [ + "name", + "fc", + "bType", + "type", + "dchg", + "dupd", + "qchg", + ].map((attr) => da.getAttribute(attr)); + das[name!] = { + fc, + bType, + type, + dchg, + dupd, + qchg, }; + } + return { + sdos, + das, + cdc: element.getAttribute("cdc"), + }; } function describeLNodeType(element: Element) { - const dos: Record> = {}; - for (const doElement of Array.from(element.children) - .filter((child) => child.tagName === "DO") - .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { - const [name, type, transient] = ["name", "type", "transient"].map((attr) => - doElement.getAttribute(attr), - ); - dos[name!] = { type, transient }; - } - return { - dos, - lnClass: element.getAttribute("lnClass"), - }; + const dos: Record> = {}; + for (const doElement of Array.from(element.children) + .filter((child) => child.tagName === "DO") + .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { + const [name, type, transient] = ["name", "type", "transient"].map((attr) => + doElement.getAttribute(attr), + ); + dos[name!] = { type, transient }; + } + return { + dos, + lnClass: element.getAttribute("lnClass"), + }; } const typeDescriptions = { - EnumType: describeEnumType, - DAType: describeDAType, - DOType: describeDOType, - LNodeType: describeLNodeType, + EnumType: describeEnumType, + DAType: describeDAType, + DOType: describeDOType, + LNodeType: describeLNodeType, } as Partial object>>; function describeElement(element: Element): object { - const describe = typeDescriptions[element.tagName]!; + const describe = typeDescriptions[element.tagName]!; - return describe(element); + return describe(element); } export function hashElement(element: Element): string { - /** A direct copy from www.github.com/openscd/open-scd-core/foundation/cyrb64.ts */ + /** A direct copy from www.github.com/openscd/open-scd-core/foundation/cyrb64.ts */ - /** - * Hashes `str` using the cyrb64 variant of - * https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js - * @returns digest - a rather insecure hash, very quickly - */ - function cyrb64(str: string): string { - /* eslint-disable no-bitwise */ - let h1 = 0xdeadbeef; - let h2 = 0x41c6ce57; - /* eslint-disable-next-line no-plusplus */ - for (let i = 0, ch; i < str.length; i++) { - ch = str.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = - Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ - Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = - Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ - Math.imul(h1 ^ (h1 >>> 13), 3266489909); - return ( - (h2 >>> 0).toString(16).padStart(8, "0") + - (h1 >>> 0).toString(16).padStart(8, "0") - ); - /* eslint-enable no-bitwise */ + /** + * Hashes `str` using the cyrb64 variant of + * https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js + * @returns digest - a rather insecure hash, very quickly + */ + function cyrb64(str: string): string { + /* eslint-disable no-bitwise */ + let h1 = 0xdeadbeef; + let h2 = 0x41c6ce57; + /* eslint-disable-next-line no-plusplus */ + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); } + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return ( + (h2 >>> 0).toString(16).padStart(8, "0") + + (h1 >>> 0).toString(16).padStart(8, "0") + ); + /* eslint-enable no-bitwise */ + } - return cyrb64(JSON.stringify(describeElement(element))); + return cyrb64(JSON.stringify(describeElement(element))); } export function isEqualNode(ours: Element, theirs: Element): boolean { - return JSON.stringify(describeElement(ours)) === JSON.stringify(describeElement(theirs)); -} \ No newline at end of file + return ( + JSON.stringify(describeElement(ours)) === + JSON.stringify(describeElement(theirs)) + ); +} diff --git a/tDataTypeTemplates/importLNodeType.spec.ts b/tDataTypeTemplates/importLNodeType.spec.ts index 7c34304..2d5aead 100644 --- a/tDataTypeTemplates/importLNodeType.spec.ts +++ b/tDataTypeTemplates/importLNodeType.spec.ts @@ -19,15 +19,15 @@ const completeTemplate = findElement(competeBayTemplate) as XMLDocument; const invalidTemplate = findElement(invalidBayTemplate) as XMLDocument; const tctrLNodeType = findElement( baseDataTypes, - 'LNodeType[id="Dummy.TCTR"]' + 'LNodeType[id="Dummy.TCTR"]', ) as Element; const mmxuLNodeType = findElement( baseDataTypes, - 'LNodeType[id="Dummy.MMXU"]' + 'LNodeType[id="Dummy.MMXU"]', ) as Element; const tctrHardUpdate = findElement( hardUpdate, - 'LNodeType[id="Dummy.TCTR"]' + 'LNodeType[id="Dummy.TCTR"]', ) as Element; describe("Function to import LNodeType with its sub data", () => { @@ -76,14 +76,18 @@ describe("Function to import LNodeType with its sub data", () => { }); it("allows to overwrite existing LNodeType", () => { - const edits1 = importLNodeType(tctrHardUpdate, completeTemplate, { overwrite: true }) as Insert[]; + const edits1 = importLNodeType(tctrHardUpdate, completeTemplate, { + overwrite: true, + }) as Insert[]; expect(edits1.length).to.equal(2); expect(edits1[0]).to.satisfies(isInsert); expect(edits1[1]).to.satisfies(isRemove); - const edits2 = importLNodeType(tctrHardUpdate, completeTemplate, { overwrite: false }) as Insert[]; + const edits2 = importLNodeType(tctrHardUpdate, completeTemplate, { + overwrite: false, + }) as Insert[]; expect(edits2.length).to.equal(1); diff --git a/tDataTypeTemplates/importLNodeType.ts b/tDataTypeTemplates/importLNodeType.ts index e2aa951..03dec4a 100644 --- a/tDataTypeTemplates/importLNodeType.ts +++ b/tDataTypeTemplates/importLNodeType.ts @@ -7,7 +7,7 @@ import { isEqualNode } from "./foundation.js"; type ImportLNodeTypeOptions = { overwrite?: boolean; -} +}; function removeDuplicates(inserts: Insert[]): Insert[] { const uniqueInserts: Insert[] = []; @@ -27,10 +27,10 @@ function removeDuplicates(inserts: Insert[]): Insert[] { function insertDataType( dataType: Element, - targetDataTypeTemplate: Element + targetDataTypeTemplate: Element, ): Insert | undefined { const existingDataType = targetDataTypeTemplate.querySelector( - `${dataType.tagName}[id="${dataType.getAttribute("id")}"] ` + `${dataType.tagName}[id="${dataType.getAttribute("id")}"] `, ); if (existingDataType && isEqualNode(dataType, existingDataType)) return; @@ -48,7 +48,7 @@ function insertDataTypes(dataTypes: Element[], targetScl: Element): Insert[] { const dataTypeEdit: Insert[] = []; const targetDataTypeTemplates = targetScl.querySelector( - ":root > DataTypeTemplates" + ":root > DataTypeTemplates", ) ? targetScl.querySelector(":root > DataTypeTemplates")! : createElement(targetScl.ownerDocument, "DataTypeTemplates", {}); @@ -64,7 +64,7 @@ function insertDataTypes(dataTypes: Element[], targetScl: Element): Insert[] { dataTypeEdit.push( ...(dataTypes .map((dataType) => insertDataType(dataType, targetDataTypeTemplates)) - .filter((insert) => !!insert) as Insert[]) + .filter((insert) => !!insert) as Insert[]), ); return removeDuplicates(dataTypeEdit); @@ -74,14 +74,14 @@ function getDaTypes(parent: Element): Element[] { const doc = parent.ownerDocument; const daTypes = Array.from( - parent.querySelectorAll(":scope > DA, :scope > BDA") + parent.querySelectorAll(":scope > DA, :scope > BDA"), ) .map((dAorBda) => doc.querySelector( `:root > DataTypeTemplates > DAType[id="${dAorBda.getAttribute( - "type" - )}"]` - ) + "type", + )}"]`, + ), ) .filter((daType) => !!daType) as Element[]; @@ -94,14 +94,14 @@ function getDoTypes(parent: Element): Element[] { const doc = parent.ownerDocument; const doTypes = Array.from( - parent.querySelectorAll(":scope > DO, :scope > SDO") + parent.querySelectorAll(":scope > DO, :scope > SDO"), ) .map((dOorSdo) => doc.querySelector( `:root > DataTypeTemplates > DOType[id="${dOorSdo.getAttribute( - "type" - )}"]` - ) + "type", + )}"]`, + ), ) .filter((doType) => !!doType) as Element[]; @@ -120,7 +120,7 @@ function getDoTypes(parent: Element): Element[] { export function importLNodeType( lNodeType: Element, targetDoc: XMLDocument, - option: ImportLNodeTypeOptions = {} + option: ImportLNodeTypeOptions = {}, ): EditV2[] { const doc = lNodeType.ownerDocument; const targetScl = targetDoc.querySelector("SCL"); @@ -134,24 +134,27 @@ export function importLNodeType( const enumTypes = [...doTypes, ...daTypes].flatMap( (doOrDaType) => Array.from( - doOrDaType.querySelectorAll('BDA[bType="Enum"], DA[bType="Enum"]') + doOrDaType.querySelectorAll('BDA[bType="Enum"], DA[bType="Enum"]'), ) .map((dAorBda) => - doc.querySelector(`EnumType[id="${dAorBda.getAttribute("type")}"]`) + doc.querySelector(`EnumType[id="${dAorBda.getAttribute("type")}"]`), ) - .filter((enumType) => !!enumType) as Element[] + .filter((enumType) => !!enumType) as Element[], ); const inserts = insertDataTypes( [lNodeType, ...doTypes, ...daTypes, ...enumTypes], - targetScl + targetScl, ); - if (option.overwrite === undefined || option.overwrite === false) return inserts; + if (option.overwrite === undefined || option.overwrite === false) + return inserts; const duplicatedLNodeType = targetScl.querySelector( - `:root > DataTypeTemplates > LNodeType[id="${lNodeType.getAttribute("id")}"]` + `:root > DataTypeTemplates > LNodeType[id="${lNodeType.getAttribute( + "id", + )}"]`, ); if (!duplicatedLNodeType) return inserts; - return [...inserts, { node: duplicatedLNodeType }] + return [...inserts, { node: duplicatedLNodeType }]; } diff --git a/tDataTypeTemplates/insertSelecetdLNodeType.spec.ts b/tDataTypeTemplates/insertSelecetdLNodeType.spec.ts index 3a5aa6b..4f0ab54 100644 --- a/tDataTypeTemplates/insertSelecetdLNodeType.spec.ts +++ b/tDataTypeTemplates/insertSelecetdLNodeType.spec.ts @@ -10,7 +10,7 @@ import { mmxuSelection, ptocSelection, lln0Selection, - ptrcSelection + ptrcSelection, } from "./insertSelectedLNodeType.testdata.js"; import { @@ -21,8 +21,12 @@ import { } from "./insertSelectedDataType.testfiles.js"; import { insertSelectedLNodeType } from "./insertSelectedLNodeType.js"; -import { CdcChildren, DaDescription, LNodeDescription, nsdToJson } from "./nsdToJson.js"; - +import { + CdcChildren, + DaDescription, + LNodeDescription, + nsdToJson, +} from "./nsdToJson.js"; function cyrb64(str: string): string { /* eslint-disable no-bitwise */ @@ -47,24 +51,24 @@ function cyrb64(str: string): string { /* eslint-enable no-bitwise */ } - const incompleteMmxu = findElement(missingMmxuTypes) as XMLDocument; const imcompleteLtrk = findElement(incompleteLtrkTypes) as XMLDocument; const incompleteAtcc = findElement(incompleteAtccTypes) as XMLDocument; const missingDataTypes = findElement(emptySSD) as XMLDocument; describe("insertLNodeTypeSelection", () => { - it('is insensitive for invalid EnumTypes', () => { + it("is insensitive for invalid EnumTypes", () => { const data = nsdToJson("LLN0") as LNodeDescription; - insertSelectedLNodeType(incompleteMmxu, invalidSelection, { class: "LLN0", data }); - }) + insertSelectedLNodeType(incompleteMmxu, invalidSelection, { + class: "LLN0", + data, + }); + }); it("insert MMXU LNodeType including missing sub data", () => { - const edits = insertSelectedLNodeType( - incompleteMmxu, - mmxuSelection, - { class: "MMXU" }, - ); + const edits = insertSelectedLNodeType(incompleteMmxu, mmxuSelection, { + class: "MMXU", + }); expect(edits.length).to.equal(6); @@ -108,11 +112,10 @@ describe("insertLNodeTypeSelection", () => { it("insert LTRK LNodeType including missing sub data", () => { const data = nsdToJson("LTRK") as LNodeDescription; - const edits = insertSelectedLNodeType( - imcompleteLtrk, - ltrkSelection, - { class: "LTRK", data }, - ); + const edits = insertSelectedLNodeType(imcompleteLtrk, ltrkSelection, { + class: "LTRK", + data, + }); expect(edits.length).to.equal(7); @@ -164,11 +167,10 @@ describe("insertLNodeTypeSelection", () => { it("insert ATCC LNodeType including missing sub data", () => { const data = nsdToJson("ATCC") as LNodeDescription; - const edits = insertSelectedLNodeType( - incompleteAtcc, - atccSelection, - { class: "ATCC", data }, - ); + const edits = insertSelectedLNodeType(incompleteAtcc, atccSelection, { + class: "ATCC", + data, + }); expect(edits.length).to.equal(5); @@ -207,11 +209,11 @@ describe("insertLNodeTypeSelection", () => { it("insert DataTypeTemplates when missing", () => { const data = nsdToJson("ATCC") as LNodeDescription; - const edits = insertSelectedLNodeType( - missingDataTypes, - atccSelection, - { class: "ATCC", desc: 'SomeDesc', data }, - ); + const edits = insertSelectedLNodeType(missingDataTypes, atccSelection, { + class: "ATCC", + desc: "SomeDesc", + data, + }); expect(edits.length).to.equal(19); @@ -221,59 +223,76 @@ describe("insertLNodeTypeSelection", () => { it("add count attribute to data attribute in case is an array", () => { const data = nsdToJson("PTOC") as LNodeDescription; - const edits = insertSelectedLNodeType( - missingDataTypes, - ptocSelection, - { class: "PTOC", data }, - ); + const edits = insertSelectedLNodeType(missingDataTypes, ptocSelection, { + class: "PTOC", + data, + }); const doTypeEdit = edits[5].node as Element; - expect(doTypeEdit.querySelector('DA[name="crvPts"]')?.getAttribute('count')).to.equal("maxPts"); + expect( + doTypeEdit.querySelector('DA[name="crvPts"]')?.getAttribute("count"), + ).to.equal("maxPts"); }); it("add count attribute to sub data object in case is an array", () => { const data = nsdToJson("MHAI") as LNodeDescription; - const edits = insertSelectedLNodeType( - missingDataTypes, - mhaiSelection, - { class: "MHAI", data }, - ); + const edits = insertSelectedLNodeType(missingDataTypes, mhaiSelection, { + class: "MHAI", + data, + }); const doTypeEdit = edits[4].node as Element; - expect(doTypeEdit.querySelector('SDO[name="phsAHar"]')?.getAttribute('count')).to.equal("maxPts"); + expect( + doTypeEdit.querySelector('SDO[name="phsAHar"]')?.getAttribute("count"), + ).to.equal("maxPts"); }); - it('add user defined data object', () => { + it("add user defined data object", () => { const lnData = nsdToJson("LLN0") as LNodeDescription; const userData = nsdToJson("SPS") as CdcChildren; (userData["dataNs"] as DaDescription).mandatory = true; (userData["dataNs"] as DaDescription).val = "TestNameSpace-1-d-1234567890"; const cdcDescription = { - tagName: 'DataObject', - type: 'SPS', - descID: '', - presCond: 'O', + tagName: "DataObject", + type: "SPS", + descID: "", + presCond: "O", children: userData, }; Object.assign(lnData, { - ['TestDo']: cdcDescription, + ["TestDo"]: cdcDescription, }); - const edits = insertSelectedLNodeType(missingDataTypes, lln0Selection, { class: "LLN0", data: lnData }); + const edits = insertSelectedLNodeType(missingDataTypes, lln0Selection, { + class: "LLN0", + data: lnData, + }); expect(edits.length).to.equal(5); - expect((edits[3].node as Element).querySelector('DA[name="dataNs"] > Val')?.textContent).to.equal("TestNameSpace-1-d-1234567890"); + expect( + (edits[3].node as Element).querySelector('DA[name="dataNs"] > Val') + ?.textContent, + ).to.equal("TestNameSpace-1-d-1234567890"); }); - it('set user defined LNodeType.id', () => { + it("set user defined LNodeType.id", () => { const id = cyrb64("TestFile"); - const edits = insertSelectedLNodeType(missingDataTypes, ptrcSelection, { class: "PTRC", id }); + const edits = insertSelectedLNodeType(missingDataTypes, ptrcSelection, { + class: "PTRC", + id, + }); expect(edits.length).to.equal(4); - expect((edits[1].node as Element).getAttribute("id")).to.equal("96ba6481aba181d8"); - expect((edits[2].node as Element).getAttribute("id")).to.equal("Beh$oscd$_c6ed035c8137b35a"); - expect((edits[3].node as Element).getAttribute("id")).to.equal("stVal$oscd$_48ba16345b8e7f5b"); - }) + expect((edits[1].node as Element).getAttribute("id")).to.equal( + "96ba6481aba181d8", + ); + expect((edits[2].node as Element).getAttribute("id")).to.equal( + "Beh$oscd$_c6ed035c8137b35a", + ); + expect((edits[3].node as Element).getAttribute("id")).to.equal( + "stVal$oscd$_48ba16345b8e7f5b", + ); + }); }); diff --git a/tDataTypeTemplates/insertSelectedLNodeType.testdata.ts b/tDataTypeTemplates/insertSelectedLNodeType.testdata.ts index 558c3ec..dda0a2e 100644 --- a/tDataTypeTemplates/insertSelectedLNodeType.testdata.ts +++ b/tDataTypeTemplates/insertSelectedLNodeType.testdata.ts @@ -354,18 +354,17 @@ export const atccSelection = { }, }; - export const invalidSelection = { - "Mod": { - "Oper": { - "ctlVal": { - "on": {}, - "blocked": {}, - "test": {}, - "test/blocked": {} + Mod: { + Oper: { + ctlVal: { + on: {}, + blocked: {}, + test: {}, + "test/blocked": {}, }, - "origin": { - "orCat": { + origin: { + orCat: { "not-supported": {}, "bay-control": {}, "station-control": {}, @@ -373,177 +372,177 @@ export const invalidSelection = { "automatic-bay": {}, "automatic-station": {}, "automatic-remote": {}, - "maintenance": {}, - "process": {} + maintenance: {}, + process: {}, }, - "orIdent": {} + orIdent: {}, }, - "ctlNum": {}, - "T": {}, - "Test": {}, - "Check": {} - }, - "stVal": { - "on": {}, - "blocked": {}, - "test": {}, - "test/blocked": {} - }, - "q": {}, - "t": {}, - "ctlModel": { + ctlNum: {}, + T: {}, + Test: {}, + Check: {}, + }, + stVal: { + on: {}, + blocked: {}, + test: {}, + "test/blocked": {}, + }, + q: {}, + t: {}, + ctlModel: { "status-only": {}, - "direct-with-normal-security": {} - } + "direct-with-normal-security": {}, + }, }, - "Beh": { - "stVal": { - "on": {}, - "blocked": {}, - "test": {}, + Beh: { + stVal: { + on: {}, + blocked: {}, + test: {}, "test/blocked": {}, - "off": {} + off: {}, }, - "q": {}, - "t": {} + q: {}, + t: {}, }, - "Health": { - "stVal": { - "Waiting": {}, - "Test": {}, - "Ok": {}, - "Warning": {}, - "Alarm": {} - }, - "q": {}, - "t": {} + Health: { + stVal: { + Waiting: {}, + Test: {}, + Ok: {}, + Warning: {}, + Alarm: {}, + }, + q: {}, + t: {}, + }, + NamPlt: { + vendor: {}, + swRev: {}, + d: {}, + configRev: {}, + paramRev: {}, + valRev: {}, + ldNs: {}, }, - "NamPlt": { - "vendor": {}, - "swRev": {}, - "d": {}, - "configRev": {}, - "paramRev": {}, - "valRev": {}, - "ldNs": {} - } }; export const ptocSelection = { - "Beh": { - "q": {}, - "stVal": { - "blocked": {}, - "off": {}, - "on": {}, - "test": {}, - "test/blocked": {} - }, - "t": {} + Beh: { + q: {}, + stVal: { + blocked: {}, + off: {}, + on: {}, + test: {}, + "test/blocked": {}, + }, + t: {}, }, - "Op": { - "general": {}, - "q": {}, - "t": {} + Op: { + general: {}, + q: {}, + t: {}, }, - "Str": { - "dirGeneral": { - "backward": {}, - "both": {}, - "forward": {}, - "unknown": {} - }, - "general": {}, - "q": {}, - "t": {} + Str: { + dirGeneral: { + backward: {}, + both: {}, + forward: {}, + unknown: {}, + }, + general: {}, + q: {}, + t: {}, }, - "TmAChr": { - "crvPts": { - "xVal": {}, - "yVal": {} - }, - "maxPts": {}, - "numPts": {}, - "xD": {}, - "yD": {}, - "yUnits": { - "SIUnit": { + TmAChr: { + crvPts: { + xVal: {}, + yVal: {}, + }, + maxPts: {}, + numPts: {}, + xD: {}, + yD: {}, + yUnits: { + SIUnit: { "°C": {}, - "1/s": {} - } + "1/s": {}, + }, }, - "xUnits": { - "SIUnit": { + xUnits: { + SIUnit: { "°C": {}, - "1/s": {} - } - } - } -} + "1/s": {}, + }, + }, + }, +}; export const mhaiSelection = { - "Beh": { - "q": {}, - "stVal": { - "blocked": {}, - "off": {}, - "on": {}, - "test": {}, - "test/blocked": {} - }, - "t": {} + Beh: { + q: {}, + stVal: { + blocked: {}, + off: {}, + on: {}, + test: {}, + "test/blocked": {}, + }, + t: {}, }, - "HA": { - "evalTm": {}, - "frequency": {}, - "maxPts": {}, - "numCyc": {}, - "numHar": {}, - "phsAHar": { - "cVal": { - "mag": { - "f": {} + HA: { + evalTm: {}, + frequency: {}, + maxPts: {}, + numCyc: {}, + numHar: {}, + phsAHar: { + cVal: { + mag: { + f: {}, + }, + ang: { + f: {}, }, - "ang": { - "f": {} - } }, - "q": {}, - "t": {} - } - } -} + q: {}, + t: {}, + }, + }, +}; export const lln0Selection = { - "Beh": { - "q": {}, - "stVal": { - "blocked": {}, - "off": {}, - "on": {}, - "test": {}, - "test/blocked": {} - }, - "t": {} + Beh: { + q: {}, + stVal: { + blocked: {}, + off: {}, + on: {}, + test: {}, + "test/blocked": {}, + }, + t: {}, }, - "TestDo": { - "stVal": {}, - "q": {}, - "t": {}, - "dataNs": {} + TestDo: { + stVal: {}, + q: {}, + t: {}, + dataNs: {}, }, -} +}; export const ptrcSelection = { - "Beh": { - "q": {}, - "stVal": { - "blocked": {}, - "off": {}, - "on": {}, - "test": {}, - "test/blocked": {} - }, - "t": {} + Beh: { + q: {}, + stVal: { + blocked: {}, + off: {}, + on: {}, + test: {}, + "test/blocked": {}, + }, + t: {}, }, -} +}; diff --git a/tDataTypeTemplates/insertSelectedLNodeType.ts b/tDataTypeTemplates/insertSelectedLNodeType.ts index a62e27d..00edcc2 100644 --- a/tDataTypeTemplates/insertSelectedLNodeType.ts +++ b/tDataTypeTemplates/insertSelectedLNodeType.ts @@ -34,7 +34,12 @@ function data(lnData: any, path: string[]): any { export function insertSelectedLNodeType( doc: XMLDocument, selection: TreeSelection, - logicalnode: { class: string, desc?: string, id?: string, data?: LNodeDescription }, + logicalnode: { + class: string; + desc?: string; + id?: string; + data?: LNodeDescription; + }, ): Insert[] { const types = new Set(); const elements: Templates = { @@ -46,7 +51,7 @@ export function insertSelectedLNodeType( const lnData = logicalnode.data ?? nsdToJson(logicalnode.class); const lnClass = logicalnode.class; - const desc = logicalnode.desc ?? null + const desc = logicalnode.desc ?? null; function isUnknownId(id: string): boolean { const alreadyCreate = types.has(id); @@ -226,14 +231,12 @@ export function insertSelectedLNodeType( if (dep.typeKind === "BASIC" || !dep.typeKind) { da.setAttribute("bType", dep.type!); - // One can include a value for any data attribute if (dep.val) { const value = createElement(doc, "Val", {}); value.textContent = dep.val; (da as Node).insertBefore(value, null); } - } if (dep.typeKind === "ENUMERATED") { diff --git a/tDataTypeTemplates/nsdToJson.ts b/tDataTypeTemplates/nsdToJson.ts index 4e0072a..f82165e 100644 --- a/tDataTypeTemplates/nsdToJson.ts +++ b/tDataTypeTemplates/nsdToJson.ts @@ -75,7 +75,7 @@ export type DaDescription = { defaultValue?: string; presCondArgs?: string; children?: DaChildren; - val?: string + val?: string; }; export type CdcChildren = Record< @@ -101,7 +101,7 @@ type CdcDescription = { isArray?: string; sizeAttribute?: string; children: CdcChildren; - typeKindParameterized?: string + typeKindParameterized?: string; }; export type LNodeDescription = Record; @@ -180,7 +180,6 @@ export function nsdToJson( lnClassOrCdc: string, nsds?: NameSpaceDescription, ): LNodeDescription | CdcChildren | undefined { - const doc74 = nsds && nsds["74"] ? nsds["74"] : defaultDoc74; const doc7420 = nsds && nsds["7420"] ? nsds["7420"] : defaultDoc7420; @@ -235,7 +234,7 @@ export function nsdToJson( function getSubDataAttributes(dataAttribute: Element): Element[] { const type = dataAttribute.getAttribute("type"); - return getSubDataAttributesType(type) + return getSubDataAttributesType(type); } function getDataAttributesType(type: string | null): Element[] { @@ -277,7 +276,7 @@ export function nsdToJson( function getSubDataObjects(dataObject: Element): Element[] { const type = dataObject.getAttribute("type"); - return getSubDataObjectsType(type) + return getSubDataObjectsType(type); } function getDataObjects(lnClass: Element): Element[] { @@ -538,8 +537,12 @@ export function nsdToJson( } function dataObjectName(dataObject: Element): string { - const multiInstances = ['Omulti', 'Mmulti'] - const instance = multiInstances.includes(dataObject.getAttribute("presCond")!) ? '1' : ''; + const multiInstances = ["Omulti", "Mmulti"]; + const instance = multiInstances.includes( + dataObject.getAttribute("presCond")!, + ) + ? "1" + : ""; const name = dataObject.getAttribute("name")!; return `${name}${instance}`; } @@ -553,7 +556,7 @@ export function nsdToJson( "transient", ].map((attr) => dataObject.getAttribute(attr)!); - const name = dataObjectName(dataObject); // adopt when presCond is Omulti + const name = dataObjectName(dataObject); // adopt when presCond is Omulti const tagName = dataObject.tagName; @@ -625,13 +628,13 @@ export function nsdToJson( if (deprecated) data["deprecated"] = deprecated; if (isArray) data["isArray"] = isArray; if (sizeAttribute) data["sizeAttribute"] = sizeAttribute; - if (typeKindParameterized) data["typeKindParameterized"] = typeKindParameterized; + if (typeKindParameterized) + data["typeKindParameterized"] = typeKindParameterized; return data; } function CdcChildren(type: string): CdcChildren { - const children: CdcChildren = {}; getSubDataObjectsType(type).forEach((dataObject) => { const name = dataObject.getAttribute("name")!; @@ -642,11 +645,7 @@ export function nsdToJson( getDataAttributesType(type).forEach((dataAttribute) => { const name = dataAttribute.getAttribute("name")!; - children[name] = nsdDataAttribute( - dataAttribute, - undefined, - undefined, - ); + children[name] = nsdDataAttribute(dataAttribute, undefined, undefined); }); getServiceDataAttributesType(type).forEach((serviceDataAttribute) => { @@ -659,11 +658,12 @@ export function nsdToJson( } if (lnClassOrCdc === undefined) return; - else if (isSupportedCdc(lnClassOrCdc)) - return CdcChildren(lnClassOrCdc); + else if (isSupportedCdc(lnClassOrCdc)) return CdcChildren(lnClassOrCdc); else { const nsdLnClass74 = doc74.querySelector(`LNClass[name="${lnClassOrCdc}"]`); - const nsdLnClass7420 = doc7420.querySelector(`LNClass[name="${lnClassOrCdc}"]`); + const nsdLnClass7420 = doc7420.querySelector( + `LNClass[name="${lnClassOrCdc}"]`, + ); const nsdLnClass = nsdLnClass74 || nsdLnClass7420; if (!nsdLnClass) return undefined; @@ -675,6 +675,6 @@ export function nsdToJson( lnClassJson[name] = nsdDataObject(dataObject); }); - return lnClassJson + return lnClassJson; } } diff --git a/tDataTypeTemplates/nsdToJson/testJson/APC.ts b/tDataTypeTemplates/nsdToJson/testJson/APC.ts index e5be999..3f714c9 100644 --- a/tDataTypeTemplates/nsdToJson/testJson/APC.ts +++ b/tDataTypeTemplates/nsdToJson/testJson/APC.ts @@ -1098,7 +1098,7 @@ export const apcChildren = { dchg: "true", descID: "IEC61850_7_3.CDCControl::APC.dbRef.desc", presCond: "MO", - presCondArgs: "db" + presCondArgs: "db", }, d: { tagName: "DataAttribute", diff --git a/tDataTypeTemplates/nsdToJson/testJson/BAC.ts b/tDataTypeTemplates/nsdToJson/testJson/BAC.ts index 5b89105..b06af1a 100644 --- a/tDataTypeTemplates/nsdToJson/testJson/BAC.ts +++ b/tDataTypeTemplates/nsdToJson/testJson/BAC.ts @@ -1108,7 +1108,7 @@ export const bacChildren = { dchg: "true", descID: "IEC61850_7_3.CDCControl::BAC.dbRef.desc", presCond: "MO", - presCondArgs: "db" + presCondArgs: "db", }, d: { tagName: "DataAttribute", diff --git a/tDataTypeTemplates/nsdToJson/testJson/BCR.ts b/tDataTypeTemplates/nsdToJson/testJson/BCR.ts index ab00a39..dbd0a8b 100644 --- a/tDataTypeTemplates/nsdToJson/testJson/BCR.ts +++ b/tDataTypeTemplates/nsdToJson/testJson/BCR.ts @@ -711,7 +711,7 @@ export const bcrChildren = { dchg: "true", descID: "IEC61850_7_3.CDCStatusInfo::BCR.strTm.desc", presCond: "OF", - presCondArgs: "frVal" + presCondArgs: "frVal", }, frPd: { tagName: "DataAttribute", diff --git a/tDataTypeTemplates/nsdToJson/testJson/RFLO.ts b/tDataTypeTemplates/nsdToJson/testJson/RFLO.ts index c71b1c9..562ec34 100644 --- a/tDataTypeTemplates/nsdToJson/testJson/RFLO.ts +++ b/tDataTypeTemplates/nsdToJson/testJson/RFLO.ts @@ -1937,7 +1937,8 @@ export const rflo = { descID: "IEC61850_7_4.LNGroupR::RFLO.FltZ.desc", presCond: "MOcond", presCondArgs: "2", - presCondArgsID: "IEC61850_7_4.LNGroupR::RFLO__(_MOcond(2)_=__((_FltZ,_FltX,_FltR_))__)_.cond.LNGroupR::RFLO.FltZ", + presCondArgsID: + "IEC61850_7_4.LNGroupR::RFLO__(_MOcond(2)_=__((_FltZ,_FltX,_FltR_))__)_.cond.LNGroupR::RFLO.FltZ", dsPresCond: "O", children: cmvChildren, }, diff --git a/tDataTypeTemplates/nsdToJson/testJson/TCTR.ts b/tDataTypeTemplates/nsdToJson/testJson/TCTR.ts index 776ac02..f47e337 100644 --- a/tDataTypeTemplates/nsdToJson/testJson/TCTR.ts +++ b/tDataTypeTemplates/nsdToJson/testJson/TCTR.ts @@ -4330,8 +4330,8 @@ export const tctr = { TPM: { literalVal: "11", name: "TPM", - tagName: "Literal" - } + tagName: "Literal", + }, }, }, d: { diff --git a/tDataTypeTemplates/nsdToJson/testNsdJson.ts b/tDataTypeTemplates/nsdToJson/testNsdJson.ts index 13b88a4..9bfe576 100644 --- a/tDataTypeTemplates/nsdToJson/testNsdJson.ts +++ b/tDataTypeTemplates/nsdToJson/testNsdJson.ts @@ -551,37 +551,37 @@ export const lnClassData: Record = { // eslint-disable-next-line @typescript-eslint/no-explicit-any export const cdcData: Record = { - "ACD": acdChildren, - "ACT": actChildren, - "APC": apcChildren, - "ASG": asgChildren, - "BAC": bacChildren, - "BCR": bcrChildren, - "BSC": bscChildren, - "CMV": cmvChildren, - "DEL": delChildren, - "DPC": dpcChildren, - "DPL": dplChildren, - "HDEL": hdelChildren, - "HMV": hmvChildren, - "HST": hstChildren, - "HWYE": hwyeChildren, - "INC": incChildren, - "INS": insChildren, - "ISC": iscChildren, - "LPL": lplChildren, - "MV": mvChildren, - "ORG": orgChildren, - "ORS": orsChildren, - "SAV": savChildren, - "SEC": secChildren, - "SEQ": seqChildren, - "SPC": spcChildren, - "SPG": spgChildren, - "SPS": spsChildren, - "TCS": tcsChildren, - "TSG": tsgChildren, - "VSG": vsgChildren, - "VSS": vssChildren, - "WYE": wyeChildren + ACD: acdChildren, + ACT: actChildren, + APC: apcChildren, + ASG: asgChildren, + BAC: bacChildren, + BCR: bcrChildren, + BSC: bscChildren, + CMV: cmvChildren, + DEL: delChildren, + DPC: dpcChildren, + DPL: dplChildren, + HDEL: hdelChildren, + HMV: hmvChildren, + HST: hstChildren, + HWYE: hwyeChildren, + INC: incChildren, + INS: insChildren, + ISC: iscChildren, + LPL: lplChildren, + MV: mvChildren, + ORG: orgChildren, + ORS: orsChildren, + SAV: savChildren, + SEC: secChildren, + SEQ: seqChildren, + SPC: spcChildren, + SPG: spgChildren, + SPS: spsChildren, + TCS: tcsChildren, + TSG: tsgChildren, + VSG: vsgChildren, + VSS: vssChildren, + WYE: wyeChildren, }; diff --git a/tDataTypeTemplates/removeDataType.spec.ts b/tDataTypeTemplates/removeDataType.spec.ts index 076bcd8..5395a34 100644 --- a/tDataTypeTemplates/removeDataType.spec.ts +++ b/tDataTypeTemplates/removeDataType.spec.ts @@ -24,7 +24,9 @@ describe("Function to recursively remove data types ", () => { expect(removes.length).equals(6); expect(removes[0].node).equal(doType); - removes.forEach(remove => expect(remove.node.parentElement).to.not.be.null); + removes.forEach( + (remove) => expect(remove.node.parentElement).to.not.be.null, + ); }); it("does not remove linked data types with force option set", () => { @@ -33,6 +35,8 @@ describe("Function to recursively remove data types ", () => { expect(removes.length).equals(15); expect(removes[0].node).equal(lNodeType); - removes.forEach(remove => expect(remove.node.parentElement).to.not.be.null); + removes.forEach( + (remove) => expect(remove.node.parentElement).to.not.be.null, + ); }); }); diff --git a/tDataTypeTemplates/removeDataType.ts b/tDataTypeTemplates/removeDataType.ts index 24dd6ad..733ec13 100644 --- a/tDataTypeTemplates/removeDataType.ts +++ b/tDataTypeTemplates/removeDataType.ts @@ -26,7 +26,7 @@ function isLNodeTypeReferenced(lNodeType: Element): boolean { const id = lNodeType.getAttribute("id"); const linkedAnyLn = dtt?.ownerDocument.querySelector( - `:root > IED *[lnType="${id}"]` + `:root > IED *[lnType="${id}"]`, ); return !!linkedAnyLn; } @@ -36,9 +36,7 @@ function isDataTypeReferenced(dataType: Element): boolean { const id = dataType.getAttribute("id"); - const linkedData = dtt?.querySelector( - `:scope *[type="${id}"]` - ); + const linkedData = dtt?.querySelector(`:scope *[type="${id}"]`); return !!linkedData; } @@ -49,13 +47,11 @@ function getOrphans(ddt: Element, saveOrphans: Element[] = []): Element[] { } function clonedDataType(dtt: Element, dataType: Element): Element { - - return dtt.querySelector(`:scope > *[id="${dataType.getAttribute('id')}"]`)!; - + return dtt.querySelector(`:scope > *[id="${dataType.getAttribute("id")}"]`)!; } function isDataTypeLinked(dataType: Element): boolean { - if (dataType.tagName === 'LNodeType') return isLNodeTypeReferenced(dataType); + if (dataType.tagName === "LNodeType") return isLNodeTypeReferenced(dataType); return isDataTypeReferenced(dataType); } @@ -68,12 +64,14 @@ function isDataTypeLinked(dataType: Element): boolean { */ export function removeDataType( dtRemove: Remove, - options: RemoveDataTypeOptions = { force: false } + options: RemoveDataTypeOptions = { force: false }, ): Remove[] { const dataType = dtRemove.node as Element; const dtt = dataType.closest("DataTypeTemplates"); - const dttClone = dataType.closest("DataTypeTemplates")?.cloneNode(true) as Element; + const dttClone = dataType + .closest("DataTypeTemplates") + ?.cloneNode(true) as Element; if (!dttClone) return []; if (isDataTypeLinked(dataType) && !options.force) return []; diff --git a/tDataTypeTemplates/updateLNodeType.spec.ts b/tDataTypeTemplates/updateLNodeType.spec.ts index 50d63b2..5bca9de 100644 --- a/tDataTypeTemplates/updateLNodeType.spec.ts +++ b/tDataTypeTemplates/updateLNodeType.spec.ts @@ -1,65 +1,79 @@ import { expect } from "chai"; -import { Insert, Remove } from "@openscd/oscd-api" +import { Insert, Remove } from "@openscd/oscd-api"; -import { docWithComplexMmxuTarget, newMmxuLNodeTypeWithChanges } from "./updateLNodeType.testfiles.js"; +import { + docWithComplexMmxuTarget, + newMmxuLNodeTypeWithChanges, +} from "./updateLNodeType.testfiles.js"; import { updateLNodeType } from "./updateLNodeType.js"; describe("updateLnType", () => { - it("handles complex MMXU update scenario with multiple changes", () => { - const doc = new DOMParser().parseFromString(docWithComplexMmxuTarget, "application/xml"); + it("handles complex MMXU update scenario with multiple changes", () => { + const doc = new DOMParser().parseFromString( + docWithComplexMmxuTarget, + "application/xml", + ); - // Create the new LNodeType with changes (external source) - const newDoc = new DOMParser().parseFromString(newMmxuLNodeTypeWithChanges, - "application/xml" - ); - const newLNodeType = newDoc.querySelector('LNodeType[id="MMXU_1"]')!; + // Create the new LNodeType with changes (external source) + const newDoc = new DOMParser().parseFromString( + newMmxuLNodeTypeWithChanges, + "application/xml", + ); + const newLNodeType = newDoc.querySelector('LNodeType[id="MMXU_1"]')!; - const edits = updateLNodeType(newLNodeType, doc) as (Insert | Remove)[]; + const edits = updateLNodeType(newLNodeType, doc) as (Insert | Remove)[]; - // Expected removals: - // 1. DOS name="PhV" (entire DO removed from LNodeType) - // 2. SDS name="ang" in PhPh.phsAB.cVal (ang BDA removed from Vector type) - // 3. SDS name="ang" in PhPh.phsBC.cVal (ang BDA removed from Vector type) - // 4-9. 6 SourceRef elements that reference invalid paths - expect(edits).to.have.lengthOf(15); + // Expected removals: + // 1. DOS name="PhV" (entire DO removed from LNodeType) + // 2. SDS name="ang" in PhPh.phsAB.cVal (ang BDA removed from Vector type) + // 3. SDS name="ang" in PhPh.phsBC.cVal (ang BDA removed from Vector type) + // 4-9. 6 SourceRef elements that reference invalid paths + expect(edits).to.have.lengthOf(15); - // Check data structure removals - const removedDataElements = edits.filter(r => - (r.node as Element).tagName === "eTr_6-100:DOS" || - (r.node as Element).tagName === "eTr_6-100:SDS" || - (r.node as Element).tagName === "eTr_6-100:DAS" - ); - expect(removedDataElements).to.have.lengthOf(3); + // Check data structure removals + const removedDataElements = edits.filter( + (r) => + (r.node as Element).tagName === "eTr_6-100:DOS" || + (r.node as Element).tagName === "eTr_6-100:SDS" || + (r.node as Element).tagName === "eTr_6-100:DAS", + ); + expect(removedDataElements).to.have.lengthOf(3); - const removedDataNames = removedDataElements.map(r => (r.node as Element).getAttribute("name")); - expect(removedDataNames).to.include("PhV"); // DOS removed - expect(removedDataNames.filter(name => name === "ang")).to.have.lengthOf(2); // 2 SDS ang removed + const removedDataNames = removedDataElements.map((r) => + (r.node as Element).getAttribute("name"), + ); + expect(removedDataNames).to.include("PhV"); // DOS removed + expect(removedDataNames.filter((name) => name === "ang")).to.have.lengthOf( + 2, + ); // 2 SDS ang removed - // Check SourceRef removals - const removedSourceRefs = edits.filter(r => (r.node as Element).localName === "SourceRef"); - expect(removedSourceRefs).to.have.lengthOf(6); + // Check SourceRef removals + const removedSourceRefs = edits.filter( + (r) => (r.node as Element).localName === "SourceRef", + ); + expect(removedSourceRefs).to.have.lengthOf(6); - const removedResourceNames = removedSourceRefs.map(r => - (r.node as Element).getAttribute("resourceName") - ); + const removedResourceNames = removedSourceRefs.map((r) => + (r.node as Element).getAttribute("resourceName"), + ); - // SourceRefs that should be removed due to PhV removal - expect(removedResourceNames).to.include("InvalidPhVRef"); - expect(removedResourceNames).to.include("InvalidPhVRef2"); - expect(removedResourceNames).to.include("CrossInvalidPhVRef"); + // SourceRefs that should be removed due to PhV removal + expect(removedResourceNames).to.include("InvalidPhVRef"); + expect(removedResourceNames).to.include("InvalidPhVRef2"); + expect(removedResourceNames).to.include("CrossInvalidPhVRef"); - // SourceRefs that should be removed due to ang removal in PhPh - expect(removedResourceNames).to.include("InvalidPhPhAngRef"); - expect(removedResourceNames).to.include("InvalidPhPhAngRef2"); - expect(removedResourceNames).to.include("CrossInvalidPhPhAngRef"); + // SourceRefs that should be removed due to ang removal in PhPh + expect(removedResourceNames).to.include("InvalidPhPhAngRef"); + expect(removedResourceNames).to.include("InvalidPhPhAngRef2"); + expect(removedResourceNames).to.include("CrossInvalidPhPhAngRef"); - // SourceRefs that should NOT be removed (valid paths) - expect(removedResourceNames).to.not.include("ValidAPhsAMag"); - expect(removedResourceNames).to.not.include("ValidAPhsAAng"); - expect(removedResourceNames).to.not.include("ValidARes"); // A.res.cVal.mag.f is valid with new LNodeType - expect(removedResourceNames).to.not.include("ValidPhPhMag"); - expect(removedResourceNames).to.not.include("CrossValidARef"); - }); -}) \ No newline at end of file + // SourceRefs that should NOT be removed (valid paths) + expect(removedResourceNames).to.not.include("ValidAPhsAMag"); + expect(removedResourceNames).to.not.include("ValidAPhsAAng"); + expect(removedResourceNames).to.not.include("ValidARes"); // A.res.cVal.mag.f is valid with new LNodeType + expect(removedResourceNames).to.not.include("ValidPhPhMag"); + expect(removedResourceNames).to.not.include("CrossValidARef"); + }); +}); diff --git a/tDataTypeTemplates/updateLNodeType.testfiles.ts b/tDataTypeTemplates/updateLNodeType.testfiles.ts index fe78e99..1f40af4 100644 --- a/tDataTypeTemplates/updateLNodeType.testfiles.ts +++ b/tDataTypeTemplates/updateLNodeType.testfiles.ts @@ -1,4 +1,3 @@ - export const docWithComplexMmxuTarget = ` DataTypeTemplates > LNodeType[id="${lNodeType.getAttribute("id")}"]` - ); +function findExistingLNodeType( + lNodeType: Element, + targetDoc: XMLDocument, +): Element | null { + const targetScl = targetDoc.querySelector("SCL"); + if (!targetScl) return null; + return targetScl.querySelector( + `:root > DataTypeTemplates > LNodeType[id="${lNodeType.getAttribute( + "id", + )}"]`, + ); } -export function updateLNodeType(lNodeType: Element, targetDoc: XMLDocument): EditV2[] { - - // Find existing LNodeType in targetDoc - const existingLNodeType = findExistingLNodeType(lNodeType, targetDoc); - if (!existingLNodeType) return []; +export function updateLNodeType( + lNodeType: Element, + targetDoc: XMLDocument, +): EditV2[] { + // Find existing LNodeType in targetDoc + const existingLNodeType = findExistingLNodeType(lNodeType, targetDoc); + if (!existingLNodeType) return []; - // Import the new one including its children - const inserts = importLNodeType(lNodeType, targetDoc); + // Import the new one including its children + const inserts = importLNodeType(lNodeType, targetDoc); - // Remove the existing LNodeType - const removeEdit: EditV2 = { node: existingLNodeType }; + // Remove the existing LNodeType + const removeEdit: EditV2 = { node: existingLNodeType }; - // Update the substation section - const removes = updateLnType(lNodeType, targetDoc); + // Update the substation section + const removes = updateLnType(lNodeType, targetDoc); - return [...inserts, removeEdit, ...removes]; -} \ No newline at end of file + return [...inserts, removeEdit, ...removes]; +} diff --git a/tExtRef/extRefTypeRestrictions.spec.ts b/tExtRef/extRefTypeRestrictions.spec.ts index 88d6930..b79c72c 100644 --- a/tExtRef/extRefTypeRestrictions.spec.ts +++ b/tExtRef/extRefTypeRestrictions.spec.ts @@ -38,12 +38,16 @@ const pDAInValid = ` { it("return undefined with missing pDO", () => - expect(extRefTypeRestrictions(findElement(unrestrictedExtRef, "ExtRef")! as Element)) - .to.be.undefined); + expect( + extRefTypeRestrictions( + findElement(unrestrictedExtRef, "ExtRef")! as Element, + ), + ).to.be.undefined); it("return undefined with invalid pDO", () => - expect(extRefTypeRestrictions(findElement(pDOInValid1, "ExtRef")! as Element)).to.be - .undefined); + expect( + extRefTypeRestrictions(findElement(pDOInValid1, "ExtRef")! as Element), + ).to.be.undefined); describe("with pDO only given ExtRefs", () => { it("return correct CDC with a valid pDO", () => @@ -57,23 +61,29 @@ describe("A function to determine the CDC and bType though pXXX attributes", () ).to.deep.equal({ cdc: "WYE" })); it("returns undefined an invalid pDO", () => - expect(extRefTypeRestrictions(findElement(pDOInValid, "ExtRef")! as Element)).to - .undefined); + expect( + extRefTypeRestrictions(findElement(pDOInValid, "ExtRef")! as Element), + ).to.undefined); }); describe("with pDO and pDA given ExtRefs", () => { it("return correct CDC and bType with a valid pDO and pDA", () => expect( - extRefTypeRestrictions(findElement(pDOandpDAValid1, "ExtRef")! as Element), + extRefTypeRestrictions( + findElement(pDOandpDAValid1, "ExtRef")! as Element, + ), ).to.deep.equal({ cdc: "SAV", bType: "INT32" })); it("return correct CDC and bType with another valid pDO and pDA", () => expect( - extRefTypeRestrictions(findElement(pDOandpDAValid2, "ExtRef")! as Element), + extRefTypeRestrictions( + findElement(pDOandpDAValid2, "ExtRef")! as Element, + ), ).to.deep.equal({ cdc: "CMV", bType: "FLOAT32" })); it("returns undefined with invalid pDA", () => - expect(extRefTypeRestrictions(findElement(pDAInValid, "ExtRef")! as Element)).to - .undefined); + expect( + extRefTypeRestrictions(findElement(pDAInValid, "ExtRef")! as Element), + ).to.undefined); }); }); diff --git a/tExtRef/subscribe.ts b/tExtRef/subscribe.ts index 6851870..2a02114 100644 --- a/tExtRef/subscribe.ts +++ b/tExtRef/subscribe.ts @@ -157,7 +157,9 @@ function createSubscribeEdit( return { parent, node: extRef, reference }; } -function createSubscribeEdits(connections: Connection[]): (Insert | SetAttributes)[] { +function createSubscribeEdits( + connections: Connection[], +): (Insert | SetAttributes)[] { const inputEdits: Insert[] = []; const extRefEdits = connections @@ -272,10 +274,10 @@ export function subscribe( const validConnections = options.force ? connections : (connections.filter((conn) => - validSubscribeConditions(conn, { - checkOnlyBType: options.checkOnlyBType, - }), - ) as Connection[]); + validSubscribeConditions(conn, { + checkOnlyBType: options.checkOnlyBType, + }), + ) as Connection[]); const extRefEdits = createSubscribeEdits(validConnections); diff --git a/tGSE/createGSE.spec.ts b/tGSE/createGSE.spec.ts index d03e5fe..8dea980 100644 --- a/tGSE/createGSE.spec.ts +++ b/tGSE/createGSE.spec.ts @@ -5,7 +5,6 @@ import { isInsert } from "@openscd/oscd-api/utils.js"; import { createGSE } from "./createGSE.js"; - const parent = new DOMParser() .parseFromString( ` diff --git a/tGSEControl/updateGSEControl.spec.ts b/tGSEControl/updateGSEControl.spec.ts index efb82ee..aa0604b 100644 --- a/tGSEControl/updateGSEControl.spec.ts +++ b/tGSEControl/updateGSEControl.spec.ts @@ -1,7 +1,11 @@ import { expect } from "chai"; import { Insert, Remove, SetAttributes } from "@openscd/oscd-api"; -import { isInsert, isRemove, isSetAttributes } from "@openscd/oscd-api/utils.js"; +import { + isInsert, + isRemove, + isSetAttributes, +} from "@openscd/oscd-api/utils.js"; import { gseControlDoc } from "./gsecontrol.testfiles.js"; diff --git a/tGSEControl/updateGSEControl.ts b/tGSEControl/updateGSEControl.ts index ce7205e..b1e261e 100644 --- a/tGSEControl/updateGSEControl.ts +++ b/tGSEControl/updateGSEControl.ts @@ -19,7 +19,9 @@ import { updateDatSet } from "../tControl/updateDatSet.js"; * @param attributes - * @returns action array to update all `GSEControl` attributes */ -export function updateGSEControl(setAttributes: SetAttributes): (SetAttributes | Remove | Insert)[] { +export function updateGSEControl( + setAttributes: SetAttributes, +): (SetAttributes | Remove | Insert)[] { if (setAttributes.element.tagName !== "GSEControl") return []; const updates: (SetAttributes | Remove | Insert)[] = []; @@ -32,17 +34,22 @@ export function updateGSEControl(setAttributes: SetAttributes): (SetAttributes | })); const supervisionUpdates: (Remove | Insert)[] = Array.from( - setAttributes.element.ownerDocument.querySelectorAll('*[lnClass="LGOS"] Val'), + setAttributes.element.ownerDocument.querySelectorAll( + '*[lnClass="LGOS"] Val', + ), ) - .filter((val) => val.textContent === controlBlockObjRef(setAttributes.element)) + .filter( + (val) => val.textContent === controlBlockObjRef(setAttributes.element), + ) .flatMap((val) => { const [path] = controlBlockObjRef(setAttributes.element)!.split("."); const oldValContent = Array.from(val.childNodes).find( (node) => node.nodeType === Node.TEXT_NODE, )!; - const newValContent = setAttributes.element.ownerDocument.createTextNode( - `${path}.${setAttributes.attributes!.name}`, - ) as Text; + const newValContent = + setAttributes.element.ownerDocument.createTextNode( + `${path}.${setAttributes.attributes!.name}`, + ) as Text; return [ { node: oldValContent }, diff --git a/tIED/insertIED.spec.ts b/tIED/insertIED.spec.ts index 6d58d2b..354feab 100644 --- a/tIED/insertIED.spec.ts +++ b/tIED/insertIED.spec.ts @@ -126,7 +126,7 @@ describe("Function to an importIED and its referenced elements", () => { ).querySelector("SCL")!; // try to import the same IED twice - for (let count = 2; count--;) { + for (let count = 2; count--; ) { const multipleIEDs = ( await fetch("tIED/insertIED/multipleieds.scd") .then((response) => response.text()) diff --git a/tIED/removeIED.ts b/tIED/removeIED.ts index 0de9165..cf6c1d9 100644 --- a/tIED/removeIED.ts +++ b/tIED/removeIED.ts @@ -49,7 +49,9 @@ function removeIedSubscriptionsAndSupervisions( } const lNodeKey = (ln: Element): string => - ["lnClass", "lnInst", "ldInst", "prefix"].map((a) => ln.getAttribute(a) ?? "").join("|"); + ["lnClass", "lnInst", "ldInst", "prefix"] + .map((a) => ln.getAttribute(a) ?? "") + .join("|"); const getLNodeScopeElement = (ln: Element): Element => { return ln.closest("Bay, VoltageLevel, Substation")!; @@ -73,7 +75,7 @@ const getLNodesByIedName = (doc: XMLDocument, name: string): Element[] => { * Default handling for LNodes - find any (public) matching LNodes and create a Remove edit for them. */ function removeBoundLNodes(ied: Element, name: string): Remove[] { - return (getLNodesByIedName(ied.ownerDocument, name)).map(createRemoveEdit); + return getLNodesByIedName(ied.ownerDocument, name).map(createRemoveEdit); } /** diff --git a/tIED/updateIED.spec.ts b/tIED/updateIED.spec.ts index 6e5765b..92c340e 100644 --- a/tIED/updateIED.spec.ts +++ b/tIED/updateIED.spec.ts @@ -10,8 +10,9 @@ import { scl } from "./updateIED.testfile.js"; import { updateIED } from "./updateIED.js"; function numberUpdates(edits: EditV2[], tag: string): number { - return edits.filter((edit) => isSetAttributes(edit) && edit.element.tagName === tag) - .length; + return edits.filter( + (edit) => isSetAttributes(edit) && edit.element.tagName === tag, + ).length; } const pub = new DOMParser() diff --git a/tIED/updateIED.ts b/tIED/updateIED.ts index 00e5ac9..fc3826c 100644 --- a/tIED/updateIED.ts +++ b/tIED/updateIED.ts @@ -82,8 +82,8 @@ function validSubscriptionSupervision( return !!otherIED.querySelector( `:scope > AccessPoint > Server > LDevice ExtRef[iedName="${oldIedName}"][srcLDInst="${srcLDInst}"][srcCBName="${srcCB.getAttribute( - "name", - )}"]`, + "name", + )}"]`, ); }) .map((srcCB) => { @@ -263,7 +263,10 @@ function updateObjectReferences( * (other than supervision node GoCBRef values). * @returns - Set of addition edits updating all references SCL elements */ -export function updateIED(setAttributes: SetAttributes, checkPermission = false): EditV2[] { +export function updateIED( + setAttributes: SetAttributes, + checkPermission = false, +): EditV2[] { if (setAttributes.element.tagName !== "IED") return []; if (!setAttributes.attributes?.name) return [setAttributes]; diff --git a/tLN/supervision/insertSubscriptionSupervisions.ts b/tLN/supervision/insertSubscriptionSupervisions.ts index be6db00..28da6b9 100644 --- a/tLN/supervision/insertSubscriptionSupervisions.ts +++ b/tLN/supervision/insertSubscriptionSupervisions.ts @@ -45,7 +45,8 @@ function uniqueSupervisions( let source: ExtRefSource; let sinkIED: Element | null = null; - const isExtRefUpdate = isSetAttributes(edit) && edit.element.tagName === "ExtRef"; + const isExtRefUpdate = + isSetAttributes(edit) && edit.element.tagName === "ExtRef"; const newExtRefInsert = isInsert(edit) && (edit.node as Element).tagName === "ExtRef"; diff --git a/tReportControl/canAddReportControl.spec.ts b/tReportControl/canAddReportControl.spec.ts index fe49e3d..b0e38e0 100644 --- a/tReportControl/canAddReportControl.spec.ts +++ b/tReportControl/canAddReportControl.spec.ts @@ -84,62 +84,62 @@ export const scl = ` `; const ap1ln01 = findElement( - scl, - `IED[name="ied1"]>AccessPoint[name="ap1"] LN0`, + scl, + `IED[name="ied1"]>AccessPoint[name="ap1"] LN0`, )! as Element; const ap2ln02 = findElement( - scl, - `IED[name="ied1"]>AccessPoint[name="ap2"] LN0`, + scl, + `IED[name="ied1"]>AccessPoint[name="ap2"] LN0`, )! as Element; const ln02 = findElement(scl, `IED[name="ied2"] LN0`)! as Element; const ln03 = findElement(scl, `IED[name="ied3"] LN0`)! as Element; const ln04 = findElement(scl, `IED[name="ied4"] LN0`)! as Element; describe("Function determining whether ReportControl can be created", () => { - describe("with existing maxBuf attribute", () => { - it("returns true with one new buf instance below limit of maxBuf", () => - expect(canAddReportControl(ap1ln01, { buffered: true })).to.true); + describe("with existing maxBuf attribute", () => { + it("returns true with one new buf instance below limit of maxBuf", () => + expect(canAddReportControl(ap1ln01, { buffered: true })).to.true); - it("returns true with three new buf instance below limit of maxBuf", () => - expect(canAddReportControl(ap1ln01, { buffered: true, newInstances: 3 })) - .to.true); + it("returns true with three new buf instance below limit of maxBuf", () => + expect(canAddReportControl(ap1ln01, { buffered: true, newInstances: 3 })) + .to.true); - it("returns false new instances exceed limit of maxBuf", () => - expect(canAddReportControl(ap1ln01, { buffered: true, newInstances: 10 })) - .to.false); + it("returns false new instances exceed limit of maxBuf", () => + expect(canAddReportControl(ap1ln01, { buffered: true, newInstances: 10 })) + .to.false); - it("returns true with new unbuf instances within limit of max - maxBuf", () => - expect(canAddReportControl(ln03, { buffered: false, newInstances: 4 })).to - .true); + it("returns true with new unbuf instances within limit of max - maxBuf", () => + expect(canAddReportControl(ln03, { buffered: false, newInstances: 4 })).to + .true); - it("returns true with new unbuf instances exceed limit of max - maxBuf", () => - expect(canAddReportControl(ln03, { buffered: false, newInstances: 5 })).to - .false); - }); + it("returns true with new unbuf instances exceed limit of max - maxBuf", () => + expect(canAddReportControl(ln03, { buffered: false, newInstances: 5 })).to + .false); + }); - describe("with missing maxBuf attribute", () => { - it("returns true with one new buf instance below limit of max", () => - expect(canAddReportControl(ap2ln02)).to.true); + describe("with missing maxBuf attribute", () => { + it("returns true with one new buf instance below limit of max", () => + expect(canAddReportControl(ap2ln02)).to.true); - it("returns false new instances exceed limit of max", () => - expect(canAddReportControl(ap2ln02, { buffered: true, newInstances: 2 })) - .to.false); + it("returns false new instances exceed limit of max", () => + expect(canAddReportControl(ap2ln02, { buffered: true, newInstances: 2 })) + .to.false); - it("returns true with new unbuf within limit of max", () => - expect(canAddReportControl(ln02, { buffered: false })).to.true); + it("returns true with new unbuf within limit of max", () => + expect(canAddReportControl(ln02, { buffered: false })).to.true); - it("returns true with five unbuf instances within limit of max", () => - expect(canAddReportControl(ln02, { buffered: false, newInstances: 5 })).to - .true); + it("returns true with five unbuf instances within limit of max", () => + expect(canAddReportControl(ln02, { buffered: false, newInstances: 5 })).to + .true); - it("returns true with new unbuf instances exceed limit of max", () => - expect(canAddReportControl(ln02, { buffered: false, newInstances: 6 })).to - .false); - }); + it("returns true with new unbuf instances exceed limit of max", () => + expect(canAddReportControl(ln02, { buffered: false, newInstances: 6 })).to + .false); + }); - it("returns true with missing max for buf report", () => - expect(canAddReportControl(ln04)).to.false); + it("returns true with missing max for buf report", () => + expect(canAddReportControl(ln04)).to.false); - it("returns true with missing max for unbuf report", () => - expect(canAddReportControl(ln04, { buffered: false })).to.false); + it("returns true with missing max for unbuf report", () => + expect(canAddReportControl(ln04, { buffered: false })).to.false); }); diff --git a/tReportControl/updateReportControl.spec.ts b/tReportControl/updateReportControl.spec.ts index 21b0d4a..ae27e53 100644 --- a/tReportControl/updateReportControl.spec.ts +++ b/tReportControl/updateReportControl.spec.ts @@ -29,7 +29,10 @@ describe("update ReportControl element", () => { describe("when no name attribute is changed", () => { it("updates ReportControl attributes and confRev", () => { - const reportControl = findElement(subscribedReport, "ReportControl")! as Element; + const reportControl = findElement( + subscribedReport, + "ReportControl", + )! as Element; const edits = updateReportControl({ element: reportControl, attributes, @@ -45,7 +48,10 @@ describe("update ReportControl element", () => { }); it("updates confRev of DataSet change ReportControl", () => { - const reportControl = findElement(subscribedReport, "ReportControl")! as Element; + const reportControl = findElement( + subscribedReport, + "ReportControl", + )! as Element; const edits = updateReportControl({ element: reportControl, attributes: { datSet: "someDataSet" }, @@ -63,7 +69,10 @@ describe("update ReportControl element", () => { describe("when name attribute is changed", () => { it("also updates subscribed ExtRefs", () => { - const reportControl = findElement(subscribedReport, "ReportControl")! as Element; + const reportControl = findElement( + subscribedReport, + "ReportControl", + )! as Element; const edits = updateReportControl({ element: reportControl, attributes: { diff --git a/tReportControl/updateReportControl.ts b/tReportControl/updateReportControl.ts index 6e38a6f..06877c5 100644 --- a/tReportControl/updateReportControl.ts +++ b/tReportControl/updateReportControl.ts @@ -6,7 +6,9 @@ import { updatedConfRev } from "../tControl/updateConfRev.js"; /** Updates `ReportControl` attributes and cross-referenced elements * @param setAttributes - setAttributes edit on `ReportControl` attributes * @returns Completed update edit array */ -export function updateReportControl(setAttributes: SetAttributes): SetAttributes[] { +export function updateReportControl( + setAttributes: SetAttributes, +): SetAttributes[] { if (setAttributes.element.tagName !== "ReportControl") return []; const reportControl = setAttributes.element; diff --git a/tSampledValueControl/updateSampledValueControl.spec.ts b/tSampledValueControl/updateSampledValueControl.spec.ts index 08cb7c3..824dcd4 100644 --- a/tSampledValueControl/updateSampledValueControl.spec.ts +++ b/tSampledValueControl/updateSampledValueControl.spec.ts @@ -1,7 +1,11 @@ import { expect } from "chai"; import { Insert, Remove, SetAttributes } from "@openscd/oscd-api"; -import { isInsert, isRemove, isSetAttributes } from "@openscd/oscd-api/utils.js"; +import { + isInsert, + isRemove, + isSetAttributes, +} from "@openscd/oscd-api/utils.js"; import { smvControlDoc } from "./smvcontrol.testfiles.js"; diff --git a/tSampledValueControl/updateSampledValueControl.ts b/tSampledValueControl/updateSampledValueControl.ts index 456d190..0affb4e 100644 --- a/tSampledValueControl/updateSampledValueControl.ts +++ b/tSampledValueControl/updateSampledValueControl.ts @@ -37,15 +37,18 @@ export function updateSampledValueControl( ':root > IED > AccessPoint > Server > LDevice > LN[lnClass="LSVS"] > DOI[name="SvCBRef"] > DAI[name="setSrcRef"] > Val', ), ) - .filter((val) => val.textContent === controlBlockObjRef(setAttributes.element)) + .filter( + (val) => val.textContent === controlBlockObjRef(setAttributes.element), + ) .flatMap((val) => { const [path] = controlBlockObjRef(setAttributes.element)!.split("."); const oldValContent = Array.from(val.childNodes).find( (node) => node.nodeType === Node.TEXT_NODE, )!; - const newValContent = setAttributes.element.ownerDocument.createTextNode( - `${path}.${setAttributes.attributes!.name}`, - ) as Text; + const newValContent = + setAttributes.element.ownerDocument.createTextNode( + `${path}.${setAttributes.attributes!.name}`, + ) as Text; return [ { node: oldValContent }, diff --git a/tServices/tConfReportControl/maxReportControl.spec.ts b/tServices/tConfReportControl/maxReportControl.spec.ts index 0163280..68da304 100644 --- a/tServices/tConfReportControl/maxReportControl.spec.ts +++ b/tServices/tConfReportControl/maxReportControl.spec.ts @@ -49,15 +49,15 @@ const ln03 = findElement(scl, `IED[name="ied3"] LN0`)! as Element; const ied = findElement(scl, `SCL`)! as Element; describe("Function to get maximum definable ReportControl", () => { - it("returns max buffered ReportControl per AccessPoint", () => - expect(maxReportControl(ln01)).to.deep.equal({ max: 6, maxBuf: 3 })); + it("returns max buffered ReportControl per AccessPoint", () => + expect(maxReportControl(ln01)).to.deep.equal({ max: 6, maxBuf: 3 })); - it("returns max buffered ReportControl per IED", () => - expect(maxReportControl(ln02)).to.deep.equal({ max: 7, maxBuf: -1 })); + it("returns max buffered ReportControl per IED", () => + expect(maxReportControl(ln02)).to.deep.equal({ max: 7, maxBuf: -1 })); - it("returns -1 with missing max attributes", () => - expect(maxReportControl(ln03)).to.deep.equal({ max: -1, maxBuf: -1 })); + it("returns -1 with missing max attributes", () => + expect(maxReportControl(ln03)).to.deep.equal({ max: -1, maxBuf: -1 })); - it("returns -1 with invalid parent input", () => - expect(maxReportControl(ied)).to.deep.equal({ max: -1, maxBuf: -1 })); + it("returns -1 with invalid parent input", () => + expect(maxReportControl(ied)).to.deep.equal({ max: -1, maxBuf: -1 })); }); diff --git a/tSubstation/updateLnType.spec.ts b/tSubstation/updateLnType.spec.ts index 8587e89..d1c3e42 100644 --- a/tSubstation/updateLnType.spec.ts +++ b/tSubstation/updateLnType.spec.ts @@ -2,364 +2,395 @@ import { expect } from "chai"; import { updateLnType } from "./updateLnType.js"; import { - docWithLNodesAndDataTypes, - docWithMissingDOs, - docWithMissingSDOs, - docWithMissingDAs, - docWithMissingBDAs, - docWithNoLNodes, - docWithDifferentLnType, - docWithoutDataTypeTemplates, - docWithSourceRefs, - docWithMalformedSourceRefs, - docWithComplexNestedStructures, - docWithMixedLNodes, + docWithLNodesAndDataTypes, + docWithMissingDOs, + docWithMissingSDOs, + docWithMissingDAs, + docWithMissingBDAs, + docWithNoLNodes, + docWithDifferentLnType, + docWithoutDataTypeTemplates, + docWithSourceRefs, + docWithMalformedSourceRefs, + docWithComplexNestedStructures, + docWithMixedLNodes, } from "./updateLnType.testfiles.js"; describe("updateLnType", () => { - describe("with valid LNodeType and matching LNode instances", () => { - it("returns empty array when all DOS, SDS, and DAS elements are valid", () => { - const doc = new DOMParser().parseFromString( - docWithLNodesAndDataTypes, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(0); - }); - - it("removes DOS elements when corresponding DO is missing from LNodeType", () => { - const doc = new DOMParser().parseFromString( - docWithMissingDOs, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(1); - expect((removes[0].node as Element).tagName).to.equal("eTr_6-100:DOS"); - expect((removes[0].node as Element).getAttribute("name")).to.equal("PhV"); - }); - - it("removes SDS elements when corresponding SDO is missing from DOType", () => { - const doc = new DOMParser().parseFromString( - docWithMissingSDOs, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(2); - removes.forEach(remove => { - expect((remove.node as Element).tagName).to.equal("eTr_6-100:SDS"); - expect(["phsB", "phsC"]).to.include((remove.node as Element).getAttribute("name")); - }); - }); - - it("removes DAS elements when corresponding DA is missing from DOType", () => { - const doc = new DOMParser().parseFromString( - docWithMissingDAs, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(4); - removes.forEach(remove => { - expect((remove.node as Element).tagName).to.equal("eTr_6-100:DAS"); - expect(["q", "t"]).to.include((remove.node as Element).getAttribute("name")); - }); - }); - - it("removes SDS elements when corresponding BDA is missing from DAType", () => { - const doc = new DOMParser().parseFromString( - docWithMissingBDAs, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(1); - expect((removes[0].node as Element).tagName).to.equal("eTr_6-100:SDS"); - expect((removes[0].node as Element).getAttribute("name")).to.equal("ang"); - }); - - it("handles multiple LNode instances with the same lnType", () => { - const doc = new DOMParser().parseFromString( - docWithMissingDOs, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - // Add another LNode with the same lnType and missing DOS - const substation = doc.querySelector("Substation")!; - const newLNode = doc.createElement("LNode"); - newLNode.setAttribute("lnClass", "MMXU"); - newLNode.setAttribute("lnType", "MMXU_1"); - newLNode.setAttribute("lnInst", "3"); - - const privateEl = doc.createElement("Private"); - privateEl.setAttribute("type", "eIEC61850-6-100"); - - const dos = doc.createElementNS("http://www.iec.ch/61850/2019/SCL/6-100", "eTr_6-100:DOS"); - dos.setAttribute("name", "PhV"); - const sds = doc.createElementNS("http://www.iec.ch/61850/2019/SCL/6-100", "eTr_6-100:SDS"); - sds.setAttribute("name", "phsA"); - sds.setAttribute("name", "phsA"); - dos.appendChild(sds); - privateEl.appendChild(dos); - newLNode.appendChild(privateEl); - substation.appendChild(newLNode); - - const removes = updateLnType(lNodeType, doc); - - // Should remove PhV DOS from both LNode instances - expect(removes).to.have.lengthOf(2); - removes.forEach(remove => { - expect((remove.node as Element).tagName).to.equal("eTr_6-100:DOS"); - expect((remove.node as Element).getAttribute("name")).to.equal("PhV"); - }); - }); + describe("with valid LNodeType and matching LNode instances", () => { + it("returns empty array when all DOS, SDS, and DAS elements are valid", () => { + const doc = new DOMParser().parseFromString( + docWithLNodesAndDataTypes, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(0); + }); + + it("removes DOS elements when corresponding DO is missing from LNodeType", () => { + const doc = new DOMParser().parseFromString( + docWithMissingDOs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(1); + expect((removes[0].node as Element).tagName).to.equal("eTr_6-100:DOS"); + expect((removes[0].node as Element).getAttribute("name")).to.equal("PhV"); + }); + + it("removes SDS elements when corresponding SDO is missing from DOType", () => { + const doc = new DOMParser().parseFromString( + docWithMissingSDOs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(2); + removes.forEach((remove) => { + expect((remove.node as Element).tagName).to.equal("eTr_6-100:SDS"); + expect(["phsB", "phsC"]).to.include( + (remove.node as Element).getAttribute("name"), + ); + }); + }); + + it("removes DAS elements when corresponding DA is missing from DOType", () => { + const doc = new DOMParser().parseFromString( + docWithMissingDAs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(4); + removes.forEach((remove) => { + expect((remove.node as Element).tagName).to.equal("eTr_6-100:DAS"); + expect(["q", "t"]).to.include( + (remove.node as Element).getAttribute("name"), + ); + }); + }); + + it("removes SDS elements when corresponding BDA is missing from DAType", () => { + const doc = new DOMParser().parseFromString( + docWithMissingBDAs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(1); + expect((removes[0].node as Element).tagName).to.equal("eTr_6-100:SDS"); + expect((removes[0].node as Element).getAttribute("name")).to.equal("ang"); + }); + + it("handles multiple LNode instances with the same lnType", () => { + const doc = new DOMParser().parseFromString( + docWithMissingDOs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + // Add another LNode with the same lnType and missing DOS + const substation = doc.querySelector("Substation")!; + const newLNode = doc.createElement("LNode"); + newLNode.setAttribute("lnClass", "MMXU"); + newLNode.setAttribute("lnType", "MMXU_1"); + newLNode.setAttribute("lnInst", "3"); + + const privateEl = doc.createElement("Private"); + privateEl.setAttribute("type", "eIEC61850-6-100"); + + const dos = doc.createElementNS( + "http://www.iec.ch/61850/2019/SCL/6-100", + "eTr_6-100:DOS", + ); + dos.setAttribute("name", "PhV"); + const sds = doc.createElementNS( + "http://www.iec.ch/61850/2019/SCL/6-100", + "eTr_6-100:SDS", + ); + sds.setAttribute("name", "phsA"); + sds.setAttribute("name", "phsA"); + dos.appendChild(sds); + privateEl.appendChild(dos); + newLNode.appendChild(privateEl); + substation.appendChild(newLNode); + + const removes = updateLnType(lNodeType, doc); + + // Should remove PhV DOS from both LNode instances + expect(removes).to.have.lengthOf(2); + removes.forEach((remove) => { + expect((remove.node as Element).tagName).to.equal("eTr_6-100:DOS"); + expect((remove.node as Element).getAttribute("name")).to.equal("PhV"); + }); + }); + }); + + describe("with edge cases", () => { + it("returns empty array when no LNode instances match the lnType", () => { + const doc = new DOMParser().parseFromString( + docWithDifferentLnType, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(0); + }); + + it("returns empty array when no LNode instances exist", () => { + const doc = new DOMParser().parseFromString( + docWithNoLNodes, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(0); + }); + + it("handles LNodeType without id attribute", () => { + const doc = new DOMParser().parseFromString( + docWithLNodesAndDataTypes, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + lNodeType.removeAttribute("id"); + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(0); + }); + + it("handles LNodeType from external document (without DataTypeTemplates in target)", () => { + const doc = new DOMParser().parseFromString( + docWithoutDataTypeTemplates, + "application/xml", + ); + + // Create an external LNodeType (e.g., from template library) + const externalLNodeType = doc.createElement("LNodeType"); + externalLNodeType.setAttribute("id", "MMXU_1"); + externalLNodeType.setAttribute("lnClass", "MMXU"); + + // Add DO that matches the LNode instance + const behDO = doc.createElement("DO"); + behDO.setAttribute("name", "Beh"); + behDO.setAttribute("type", "BehaviourDO"); + externalLNodeType.appendChild(behDO); + + const removes = updateLnType(externalLNodeType, doc); + + // Should work with external LNodeType and not remove valid DOS + expect(removes).to.have.lengthOf(0); + }); + + it("removes elements when using external LNodeType with different structure", () => { + const doc = new DOMParser().parseFromString( + docWithoutDataTypeTemplates, + "application/xml", + ); + + // Create an external LNodeType that doesn't include the "Beh" DO + const externalLNodeType = doc.createElement("LNodeType"); + externalLNodeType.setAttribute("id", "MMXU_1"); + externalLNodeType.setAttribute("lnClass", "MMXU"); + + // Add a different DO that doesn't match the LNode instance + const otherDO = doc.createElement("DO"); + otherDO.setAttribute("name", "A"); + otherDO.setAttribute("type", "WYE"); + externalLNodeType.appendChild(otherDO); + + const removes = updateLnType(externalLNodeType, doc); + + // Should remove the "Beh" DOS since it's not in the external LNodeType + expect(removes).to.have.lengthOf(1); + expect((removes[0].node as Element).tagName).to.equal("eTr_6-100:DOS"); + expect((removes[0].node as Element).getAttribute("name")).to.equal("Beh"); + }); + + it("handles LNode without Private element", () => { + const doc = new DOMParser().parseFromString( + docWithLNodesAndDataTypes, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + // Remove Private element from LNode + const lNode = doc.querySelector('LNode[lnType="MMXU_1"]')!; + const privateEl = lNode.querySelector("Private"); + if (privateEl) { + lNode.removeChild(privateEl); + } + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(0); }); - describe("with edge cases", () => { - it("returns empty array when no LNode instances match the lnType", () => { - const doc = new DOMParser().parseFromString( - docWithDifferentLnType, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(0); - }); - - it("returns empty array when no LNode instances exist", () => { - const doc = new DOMParser().parseFromString( - docWithNoLNodes, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(0); - }); - - it("handles LNodeType without id attribute", () => { - const doc = new DOMParser().parseFromString( - docWithLNodesAndDataTypes, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - lNodeType.removeAttribute("id"); - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(0); - }); - - it("handles LNodeType from external document (without DataTypeTemplates in target)", () => { - const doc = new DOMParser().parseFromString( - docWithoutDataTypeTemplates, - "application/xml" - ); - - // Create an external LNodeType (e.g., from template library) - const externalLNodeType = doc.createElement("LNodeType"); - externalLNodeType.setAttribute("id", "MMXU_1"); - externalLNodeType.setAttribute("lnClass", "MMXU"); - - // Add DO that matches the LNode instance - const behDO = doc.createElement("DO"); - behDO.setAttribute("name", "Beh"); - behDO.setAttribute("type", "BehaviourDO"); - externalLNodeType.appendChild(behDO); - - const removes = updateLnType(externalLNodeType, doc); - - // Should work with external LNodeType and not remove valid DOS - expect(removes).to.have.lengthOf(0); - }); - - it("removes elements when using external LNodeType with different structure", () => { - const doc = new DOMParser().parseFromString( - docWithoutDataTypeTemplates, - "application/xml" - ); - - // Create an external LNodeType that doesn't include the "Beh" DO - const externalLNodeType = doc.createElement("LNodeType"); - externalLNodeType.setAttribute("id", "MMXU_1"); - externalLNodeType.setAttribute("lnClass", "MMXU"); - - // Add a different DO that doesn't match the LNode instance - const otherDO = doc.createElement("DO"); - otherDO.setAttribute("name", "A"); - otherDO.setAttribute("type", "WYE"); - externalLNodeType.appendChild(otherDO); - - const removes = updateLnType(externalLNodeType, doc); - - // Should remove the "Beh" DOS since it's not in the external LNodeType - expect(removes).to.have.lengthOf(1); - expect((removes[0].node as Element).tagName).to.equal("eTr_6-100:DOS"); - expect((removes[0].node as Element).getAttribute("name")).to.equal("Beh"); - }); - - it("handles LNode without Private element", () => { - const doc = new DOMParser().parseFromString( - docWithLNodesAndDataTypes, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - // Remove Private element from LNode - const lNode = doc.querySelector('LNode[lnType="MMXU_1"]')!; - const privateEl = lNode.querySelector("Private"); - if (privateEl) { - lNode.removeChild(privateEl); - } - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(0); - }); - - it("handles LNode with Private element but no DOS children", () => { - const doc = new DOMParser().parseFromString( - docWithLNodesAndDataTypes, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - // Remove all DOS elements from Private - const lNode = doc.querySelector('LNode[lnType="MMXU_1"]')!; - const privateEl = lNode.querySelector("Private")!; - const dosElements = Array.from(privateEl.querySelectorAll("DOS")); - dosElements.forEach(dos => privateEl.removeChild(dos)); - - const removes = updateLnType(lNodeType, doc); - - expect(removes).to.have.lengthOf(0); - }); - - it("handles nested data structures with missing elements at different levels", () => { - const doc = new DOMParser().parseFromString(docWithComplexNestedStructures, "application/xml"); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - // Should remove: PhV DOS, phsB SDS, q DAS, ang SDS - expect(removes).to.have.lengthOf(4); - - const removedNames = removes.map(r => (r.node as Element).getAttribute("name")); - expect(removedNames).to.include("PhV"); - expect(removedNames).to.include("phsB"); - expect(removedNames).to.include("q"); - expect(removedNames).to.include("ang"); - }); + it("handles LNode with Private element but no DOS children", () => { + const doc = new DOMParser().parseFromString( + docWithLNodesAndDataTypes, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + // Remove all DOS elements from Private + const lNode = doc.querySelector('LNode[lnType="MMXU_1"]')!; + const privateEl = lNode.querySelector("Private")!; + const dosElements = Array.from(privateEl.querySelectorAll("DOS")); + dosElements.forEach((dos) => privateEl.removeChild(dos)); + + const removes = updateLnType(lNodeType, doc); + + expect(removes).to.have.lengthOf(0); }); - describe("SourceRef removal", () => { - it("removes SourceRef elements when referenced data structures are missing", () => { - const doc = new DOMParser().parseFromString( - docWithSourceRefs, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - // Should remove: 1 DOS (PhV), 1 SDS (phsC), 1 SDS (ang), 1 DAS (t), and 5 SourceRefs - expect(removes).to.have.lengthOf(9); - - const removedSourceRefs = removes.filter(r => (r.node as Element).localName === "SourceRef"); - expect(removedSourceRefs).to.have.lengthOf(5); - - const removedResourceNames = removedSourceRefs.map(r => - (r.node as Element).getAttribute("resourceName") - ); - - expect(removedResourceNames).to.include("InvalidPhVReference"); - expect(removedResourceNames).to.include("InvalidPhsCReference"); - expect(removedResourceNames).to.include("InvalidAngReference"); - expect(removedResourceNames).to.include("InvalidTReference"); - expect(removedResourceNames).to.include("CrossLNodeReference"); - - // Valid SourceRefs should not be removed - expect(removedResourceNames).to.not.include("ValidReference"); - }); - - it("removes SourceRef elements from different LNodes when they reference invalid data", () => { - const doc = new DOMParser().parseFromString( - docWithSourceRefs, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - const removedSourceRefs = removes.filter(r => (r.node as Element).localName === "SourceRef"); - const crossLNodeRef = removedSourceRefs.find(r => - (r.node as Element).getAttribute("resourceName") === "CrossLNodeReference" - ); - - expect(crossLNodeRef).to.exist; - - // Valid cross-LNode reference should not be removed - const validCrossRef = removedSourceRefs.find(r => - (r.node as Element).getAttribute("resourceName") === "ValidCrossReference" - ); - expect(validCrossRef).to.be.undefined; - }); - - it("handles documents without SourceRef elements", () => { - const doc = new DOMParser().parseFromString( - docWithMissingDOs, - "application/xml" - ); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - // Should only remove the PhV DOS, no SourceRefs to remove - expect(removes).to.have.lengthOf(1); - expect((removes[0].node as Element).tagName).to.equal("eTr_6-100:DOS"); - expect((removes[0].node as Element).getAttribute("name")).to.equal("PhV"); - }); - - it("handles SourceRef elements with malformed source attributes", () => { - const doc = new DOMParser().parseFromString(docWithMalformedSourceRefs, "application/xml"); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - - const removes = updateLnType(lNodeType, doc); - - // Should not crash and should not remove any elements (all data is valid) - expect(removes).to.have.lengthOf(0); - }); + it("handles nested data structures with missing elements at different levels", () => { + const doc = new DOMParser().parseFromString( + docWithComplexNestedStructures, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + // Should remove: PhV DOS, phsB SDS, q DAS, ang SDS + expect(removes).to.have.lengthOf(4); + + const removedNames = removes.map((r) => + (r.node as Element).getAttribute("name"), + ); + expect(removedNames).to.include("PhV"); + expect(removedNames).to.include("phsB"); + expect(removedNames).to.include("q"); + expect(removedNames).to.include("ang"); + }); + }); + + describe("SourceRef removal", () => { + it("removes SourceRef elements when referenced data structures are missing", () => { + const doc = new DOMParser().parseFromString( + docWithSourceRefs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + // Should remove: 1 DOS (PhV), 1 SDS (phsC), 1 SDS (ang), 1 DAS (t), and 5 SourceRefs + expect(removes).to.have.lengthOf(9); + + const removedSourceRefs = removes.filter( + (r) => (r.node as Element).localName === "SourceRef", + ); + expect(removedSourceRefs).to.have.lengthOf(5); + + const removedResourceNames = removedSourceRefs.map((r) => + (r.node as Element).getAttribute("resourceName"), + ); + + expect(removedResourceNames).to.include("InvalidPhVReference"); + expect(removedResourceNames).to.include("InvalidPhsCReference"); + expect(removedResourceNames).to.include("InvalidAngReference"); + expect(removedResourceNames).to.include("InvalidTReference"); + expect(removedResourceNames).to.include("CrossLNodeReference"); + + // Valid SourceRefs should not be removed + expect(removedResourceNames).to.not.include("ValidReference"); }); - describe("selector specificity", () => { - it("finds LNode instances both inside and outside Bay elements", () => { - const doc = new DOMParser().parseFromString(docWithMixedLNodes, "application/xml"); - const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + it("removes SourceRef elements from different LNodes when they reference invalid data", () => { + const doc = new DOMParser().parseFromString( + docWithSourceRefs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + const removedSourceRefs = removes.filter( + (r) => (r.node as Element).localName === "SourceRef", + ); + const crossLNodeRef = removedSourceRefs.find( + (r) => + (r.node as Element).getAttribute("resourceName") === + "CrossLNodeReference", + ); + + expect(crossLNodeRef).to.exist; + + // Valid cross-LNode reference should not be removed + const validCrossRef = removedSourceRefs.find( + (r) => + (r.node as Element).getAttribute("resourceName") === + "ValidCrossReference", + ); + expect(validCrossRef).to.be.undefined; + }); - const removes = updateLnType(lNodeType, doc); + it("handles documents without SourceRef elements", () => { + const doc = new DOMParser().parseFromString( + docWithMissingDOs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; - // Should find and process both LNode instances - expect(removes).to.have.lengthOf(2); - removes.forEach(remove => { - expect((remove.node as Element).tagName).to.equal("eTr_6-100:DOS"); - expect((remove.node as Element).getAttribute("name")).to.equal("MissingDO"); - }); - }); + const removes = updateLnType(lNodeType, doc); + + // Should only remove the PhV DOS, no SourceRefs to remove + expect(removes).to.have.lengthOf(1); + expect((removes[0].node as Element).tagName).to.equal("eTr_6-100:DOS"); + expect((removes[0].node as Element).getAttribute("name")).to.equal("PhV"); + }); + + it("handles SourceRef elements with malformed source attributes", () => { + const doc = new DOMParser().parseFromString( + docWithMalformedSourceRefs, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + // Should not crash and should not remove any elements (all data is valid) + expect(removes).to.have.lengthOf(0); + }); + }); + + describe("selector specificity", () => { + it("finds LNode instances both inside and outside Bay elements", () => { + const doc = new DOMParser().parseFromString( + docWithMixedLNodes, + "application/xml", + ); + const lNodeType = doc.querySelector('LNodeType[id="MMXU_1"]')!; + + const removes = updateLnType(lNodeType, doc); + + // Should find and process both LNode instances + expect(removes).to.have.lengthOf(2); + removes.forEach((remove) => { + expect((remove.node as Element).tagName).to.equal("eTr_6-100:DOS"); + expect((remove.node as Element).getAttribute("name")).to.equal( + "MissingDO", + ); + }); }); + }); }); diff --git a/tSubstation/updateLnType.ts b/tSubstation/updateLnType.ts index 296bee3..7a22343 100644 --- a/tSubstation/updateLnType.ts +++ b/tSubstation/updateLnType.ts @@ -1,103 +1,113 @@ import { Remove } from "@openscd/oscd-api"; function getDataType(data: Element): Element | null { - const dataTypeTemplates = data.closest("DataTypeTemplates"); - if (!dataTypeTemplates) return null; + const dataTypeTemplates = data.closest("DataTypeTemplates"); + if (!dataTypeTemplates) return null; - const type = data.getAttribute("type"); + const type = data.getAttribute("type"); - return dataTypeTemplates.querySelector(`DAType[id="${type}"], DOType[id="${type}"]`); + return dataTypeTemplates.querySelector( + `DAType[id="${type}"], DOType[id="${type}"]`, + ); } function removeMissingSdsOrDas(dataType: Element, dosOrSds: Element): Remove[] { - const removes: Remove[] = []; - - dosOrSds.querySelectorAll(":scope > SDS, :scope > DAS").forEach((sdsOrDas) => { - const name = sdsOrDas.getAttribute("name"); - const childData = dataType.querySelector( - `:scope > DA[name="${name}"], :scope > BDA[name="${name}"], :scope > SDO[name="${name}"]` - ); - if (!childData) removes.push({ node: sdsOrDas }); - else { - const childDataType = getDataType(childData); - if (childDataType) - removes.push(...removeMissingSdsOrDas(childDataType, sdsOrDas)); - } + const removes: Remove[] = []; + + dosOrSds + .querySelectorAll(":scope > SDS, :scope > DAS") + .forEach((sdsOrDas) => { + const name = sdsOrDas.getAttribute("name"); + const childData = dataType.querySelector( + `:scope > DA[name="${name}"], :scope > BDA[name="${name}"], :scope > SDO[name="${name}"]`, + ); + if (!childData) removes.push({ node: sdsOrDas }); + else { + const childDataType = getDataType(childData); + if (childDataType) + removes.push(...removeMissingSdsOrDas(childDataType, sdsOrDas)); + } }); - return removes; + return removes; } function removeMissingDos(lNodeType: Element, lNode: Element): Remove[] { - const removes: Remove[] = []; - - lNode.querySelectorAll(":scope > Private > DOS").forEach((dos) => { - const doElement = lNodeType.querySelector( - `:scope > DO[name="${dos.getAttribute("name")}"]` - ); - if (!doElement) removes.push({ node: dos }); - else { - const dataType = getDataType(doElement); - if (dataType) - removes.push(...removeMissingSdsOrDas(dataType, dos)); - } - }); + const removes: Remove[] = []; + + lNode.querySelectorAll(":scope > Private > DOS").forEach((dos) => { + const doElement = lNodeType.querySelector( + `:scope > DO[name="${dos.getAttribute("name")}"]`, + ); + if (!doElement) removes.push({ node: dos }); + else { + const dataType = getDataType(doElement); + if (dataType) removes.push(...removeMissingSdsOrDas(dataType, dos)); + } + }); - return removes; + return removes; } function lNodeReference(lNode: Element): string { - let reference = ""; - const lNodeTag = `${lNode.getAttribute("prefix") || ""}${lNode.getAttribute("lnClass") || ""}${lNode.getAttribute("lnInst") || ""}`; + let reference = ""; + const lNodeTag = `${lNode.getAttribute("prefix") || ""}${ + lNode.getAttribute("lnClass") || "" + }${lNode.getAttribute("lnInst") || ""}`; - reference = lNodeTag; + reference = lNodeTag; - let child = lNode; - while (child.parentElement !== null) { - if (child.parentElement.tagName === "SCL") break; - const name = child.parentElement.getAttribute("name"); + let child = lNode; + while (child.parentElement !== null) { + if (child.parentElement.tagName === "SCL") break; + const name = child.parentElement.getAttribute("name"); - // refix the name to the existing prefix seperated by / - if (name) reference = `${name}/${reference}`; - child = child.parentElement; - } + // refix the name to the existing prefix seperated by / + if (name) reference = `${name}/${reference}`; + child = child.parentElement; + } - return reference + return reference; } function invalidPath(path: string[], dataType: Element, index = 0): boolean { - const dataName = path[index]; + const dataName = path[index]; - const doElement = dataType.querySelector( - `:scope > DO[name="${dataName}"], :scope > SDO[name="${dataName}"], :scope > DA[name="${dataName}"], :scope > BDA[name="${dataName}"]` - ); - if (!doElement) return true; + const doElement = dataType.querySelector( + `:scope > DO[name="${dataName}"], :scope > SDO[name="${dataName}"], :scope > DA[name="${dataName}"], :scope > BDA[name="${dataName}"]`, + ); + if (!doElement) return true; - const childDataType = getDataType(doElement); - if (!childDataType) return false; + const childDataType = getDataType(doElement); + if (!childDataType) return false; - if (index + 1 < path.length) return invalidPath(path, childDataType, index + 1); + if (index + 1 < path.length) + return invalidPath(path, childDataType, index + 1); - return false + return false; } function removeInvalidSourceRef(lNodeType: Element, lNode: Element): Remove[] { - const removes: Remove[] = []; + const removes: Remove[] = []; - const lNodeRef = lNodeReference(lNode); + const lNodeRef = lNodeReference(lNode); - const srcRefs = Array.from(lNode.ownerDocument.querySelectorAll(":root Private SourceRef")).filter(srcRef => srcRef.getAttribute("source")?.startsWith(lNodeRef)); + const srcRefs = Array.from( + lNode.ownerDocument.querySelectorAll(":root Private SourceRef"), + ).filter((srcRef) => srcRef.getAttribute("source")?.startsWith(lNodeRef)); - srcRefs.forEach((srcRef) => { - // source without the starting lNodeRef - const srcRefPath = srcRef.getAttribute("source")!.substring(lNodeRef.length + 1); + srcRefs.forEach((srcRef) => { + // source without the starting lNodeRef + const srcRefPath = srcRef + .getAttribute("source")! + .substring(lNodeRef.length + 1); - const path = srcRefPath.split('.'); + const path = srcRefPath.split("."); - if (invalidPath(path, lNodeType)) removes.push({ node: srcRef }); - }); + if (invalidPath(path, lNodeType)) removes.push({ node: srcRef }); + }); - return removes; + return removes; } /** @@ -106,20 +116,28 @@ function removeInvalidSourceRef(lNodeType: Element, lNode: Element): Remove[] { * @param targetDoc the target XML document where the LNode instance is located * @returns an array of Remove edits for the missing elements */ -export function updateLnType(lNodeType: Element, targetDoc: XMLDocument): Remove[] { - - // find all LNode instances in targetDoc with the same lnType - const lnType = lNodeType.getAttribute("id"); - const lNodes = Array.from(targetDoc.querySelectorAll( - `:root > Substation LNode[lnType="${lnType}"], :root > Substation > LNode[lnType="${lnType}"]` - )); - if (lNodes.length === 0) return []; - - // for each LNode instance remove missing DOS, SDS or DAS - const removeInstanceData: Remove[] = lNodes.flatMap((lNode) => removeMissingDos(lNodeType, lNode)); - - // remove SourceRef when reference is invalid - const removeSrcRef = lNodes.flatMap(lNode => removeInvalidSourceRef(lNodeType, lNode)); - - return [...removeInstanceData, ...removeSrcRef]; -} \ No newline at end of file +export function updateLnType( + lNodeType: Element, + targetDoc: XMLDocument, +): Remove[] { + // find all LNode instances in targetDoc with the same lnType + const lnType = lNodeType.getAttribute("id"); + const lNodes = Array.from( + targetDoc.querySelectorAll( + `:root > Substation LNode[lnType="${lnType}"], :root > Substation > LNode[lnType="${lnType}"]`, + ), + ); + if (lNodes.length === 0) return []; + + // for each LNode instance remove missing DOS, SDS or DAS + const removeInstanceData: Remove[] = lNodes.flatMap((lNode) => + removeMissingDos(lNodeType, lNode), + ); + + // remove SourceRef when reference is invalid + const removeSrcRef = lNodes.flatMap((lNode) => + removeInvalidSourceRef(lNodeType, lNode), + ); + + return [...removeInstanceData, ...removeSrcRef]; +} diff --git a/tSubstation/updateSubstation.spec.ts b/tSubstation/updateSubstation.spec.ts index afd2708..481c4fb 100644 --- a/tSubstation/updateSubstation.spec.ts +++ b/tSubstation/updateSubstation.spec.ts @@ -130,10 +130,7 @@ describe("update Substation element", () => { expect( updates.find( - ({ - element: { tagName }, - attributes, - }) => + ({ element: { tagName }, attributes }) => tagName === "Terminal" && attributes?.connectivityNode === "AA2/E1/BB1/L1" && attributes?.substationName === "AA2", @@ -142,10 +139,7 @@ describe("update Substation element", () => { expect( updates.find( - ({ - element: { tagName }, - attributes, - }) => + ({ element: { tagName }, attributes }) => tagName === "NeutralPoint" && attributes?.connectivityNode === "AA2/E1/BB1/L1" && attributes?.substationName === "AA2", diff --git a/tVoltageLevel/updateVoltageLevel.spec.ts b/tVoltageLevel/updateVoltageLevel.spec.ts index 55e5c4c..b1dded2 100644 --- a/tVoltageLevel/updateVoltageLevel.spec.ts +++ b/tVoltageLevel/updateVoltageLevel.spec.ts @@ -150,10 +150,7 @@ describe("update VoltageLevel element", () => { expect( updates.find( - ({ - element: { tagName }, - attributes, - }) => + ({ element: { tagName }, attributes }) => tagName === "NeutralPoint" && attributes?.["voltageLevelName"] === "E2" && attributes?.["connectivityNode"] === "AA1/E2/BB1/L1", diff --git a/tVoltageLevel/updateVoltageLevel.ts b/tVoltageLevel/updateVoltageLevel.ts index 6259442..2755767 100644 --- a/tVoltageLevel/updateVoltageLevel.ts +++ b/tVoltageLevel/updateVoltageLevel.ts @@ -112,7 +112,9 @@ function updateTerminals( /** Updates `VoltageLevel` attributes and cross-referenced elements * @param setAttributes - update edit on `VoltageLevel` attributes * @returns Completed update edit array */ -export function updateVoltageLevel(setAttributes: SetAttributes): SetAttributes[] { +export function updateVoltageLevel( + setAttributes: SetAttributes, +): SetAttributes[] { if (setAttributes.element.tagName !== "VoltageLevel") return [setAttributes]; const voltageLevel = setAttributes.element; @@ -126,7 +128,8 @@ export function updateVoltageLevel(setAttributes: SetAttributes): SetAttributes[ ?.getAttribute("name"); const newName = attributes.name; - if (!substationName || !oldName || oldName === newName) return [setAttributes]; + if (!substationName || !oldName || oldName === newName) + return [setAttributes]; return [setAttributes].concat( ...updateConnectivityNodes(voltageLevel, {