From 2cdef2393cfd1aff6debe83dd365ffe126c9ff73 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Sun, 25 Sep 2022 00:35:14 +0200 Subject: [PATCH 1/2] feat(foundation): add MAC-Address and APPID generator --- src/foundation/generators.ts | 98 ++++++++++++ test/unit/foundation/generators.test.ts | 191 ++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 src/foundation/generators.ts create mode 100644 test/unit/foundation/generators.test.ts diff --git a/src/foundation/generators.ts b/src/foundation/generators.ts new file mode 100644 index 000000000..ffe2baa88 --- /dev/null +++ b/src/foundation/generators.ts @@ -0,0 +1,98 @@ +const maxGseMacAddress = 0x010ccd0101ff; +const minGseMacAddress = 0x010ccd010000; + +const maxSmvMacAddress = 0x010ccd0401ff; +const minSmvMacAddress = 0x010ccd040000; + +function convertToMac(mac: number): string { + const str = 0 + mac.toString(16).toUpperCase(); + const arr = str.match(/.{1,2}/g)!; + return arr?.join('-'); +} + +const gseMacRange = Array(maxGseMacAddress - minGseMacAddress) + .fill(1) + .map((_, i) => convertToMac(minGseMacAddress + i)); + +const smvMacRange = Array(maxSmvMacAddress - minSmvMacAddress) + .fill(1) + .map((_, i) => convertToMac(minSmvMacAddress + i)); + +/** + * @param doc - project xml document + * @param serviceType - SampledValueControl (SMV) or GSEControl (GSE) + * @returns a function generating increasing unused `MAC-Address` within `doc` on subsequent invocations + */ +export function mACAddressGenerator( + doc: XMLDocument, + serviceType: 'SMV' | 'GSE' +): () => string { + const macs = new Set( + Array.from( + doc.querySelectorAll(`${serviceType} > Address > P[type="MAC-Address"]`) + ).map(macs => macs.textContent!) + ); + + const range = serviceType === 'SMV' ? smvMacRange : gseMacRange; + + return () => { + const uniqueMAC = range.find(mac => !macs.has(mac)); + if (uniqueMAC) macs.add(uniqueMAC); + return uniqueMAC ?? ''; + }; +} + +const maxGseAppId = 0x3fff; +const minGseAppId = 0x0000; + +// APPID range for Type1A(Trip) GOOSE acc. IEC 61850-8-1 +const maxGseTripAppId = 0xbfff; +const minGseTripAppId = 0x8000; + +const maxSmvAppId = 0x7fff; +const minSmvAppId = 0x4000; + +const gseAppIdRange = Array(maxGseAppId - minGseAppId) + .fill(1) + .map((_, i) => (minGseAppId + i).toString(16).toUpperCase().padStart(4, '0')); + +const gseTripAppIdRange = Array(maxGseTripAppId - minGseTripAppId) + .fill(1) + .map((_, i) => + (minGseTripAppId + i).toString(16).toUpperCase().padStart(4, '0') + ); + +const smvAppIdRange = Array(maxSmvAppId - minSmvAppId) + .fill(1) + .map((_, i) => (minSmvAppId + i).toString(16).toUpperCase().padStart(4, '0')); + +/** + * @param doc - project xml document + * @param serviceType - SampledValueControl (SMV) or GSEControl (GSE) + * @param type1A - whether the GOOSE is a Trip GOOSE resulting in different APPID range - default false + * @returns a function generating increasing unused `APPID` within `doc` on subsequent invocations + */ +export function appIdGenerator( + doc: XMLDocument, + serviceType: 'SMV' | 'GSE', + type1A = false +): () => string { + const appIds = new Set( + Array.from( + doc.querySelectorAll(`${serviceType} > Address > P[type="APPID"]`) + ).map(appId => appId.textContent!) + ); + + const range = + serviceType === 'SMV' + ? smvAppIdRange + : type1A + ? gseTripAppIdRange + : gseAppIdRange; + + return () => { + const uniqueAppId = range.find(appId => !appIds.has(appId)); + if (uniqueAppId) appIds.add(uniqueAppId); + return uniqueAppId ?? ''; + }; +} diff --git a/test/unit/foundation/generators.test.ts b/test/unit/foundation/generators.test.ts new file mode 100644 index 000000000..55673a81c --- /dev/null +++ b/test/unit/foundation/generators.test.ts @@ -0,0 +1,191 @@ +import { expect } from '@open-wc/testing'; +import { + appIdGenerator, + mACAddressGenerator, +} from '../../../src/foundation/generators.js'; + +describe('MAC-Address generator function', () => { + let macGenerator: () => string; + let doc: XMLDocument; + + describe('for GSE elements', () => { + beforeEach(() => { + doc = new DOMParser().parseFromString( + ` +

01-0C-CD-01-00-00

+

01-0C-CD-01-00-01

+

01-0C-CD-01-00-02

+

01-0C-CD-01-00-04

+

01-0C-CD-01-00-06

+

01-0C-CD-01-00-07

+

01-0C-CD-01-00-08

+

01-0C-CD-01-00-09

+

01-0C-CD-01-00-10

+

01-0C-CD-01-00-12

+

01-0C-CD-01-00-13

+

01-0C-CD-01-00-14

+

01-0C-CD-01-00-15

+

01-0C-CD-01-00-0F

+
`, + 'application/xml' + ); + + macGenerator = mACAddressGenerator(doc, 'GSE'); + }); + + it('returns unique MAC-Address', () => + expect(macGenerator()).to.equal('01-0C-CD-01-00-03')); + + it('always returns unique Mac-Address', () => { + expect(macGenerator()).to.equal('01-0C-CD-01-00-03'); + expect(macGenerator()).to.equal('01-0C-CD-01-00-05'); + expect(macGenerator()).to.equal('01-0C-CD-01-00-0A'); + expect(macGenerator()).to.equal('01-0C-CD-01-00-0B'); + }); + }); + + describe('for SMV elements', () => { + beforeEach(() => { + doc = new DOMParser().parseFromString( + ` +

01-0C-CD-04-00-00

+

01-0C-CD-04-00-01

+

01-0C-CD-04-00-02

+

01-0C-CD-04-00-03

+

01-0C-CD-04-00-06

+

01-0C-CD-04-00-07

+

01-0C-CD-04-00-08

+

01-0C-CD-04-00-09

+

01-0C-CD-04-00-10

+

01-0C-CD-04-00-12

+

01-0C-CD-04-00-13

+

01-0C-CD-04-00-14

+

01-0C-CD-04-00-15

+

01-0C-CD-04-00-0B

+
`, + 'application/xml' + ); + + macGenerator = mACAddressGenerator(doc, 'SMV'); + }); + + it('returns unique MAC-Address', () => + expect(macGenerator()).to.equal('01-0C-CD-04-00-04')); + + it('always returns unique MAC-Address', () => { + expect(macGenerator()).to.equal('01-0C-CD-04-00-04'); + expect(macGenerator()).to.equal('01-0C-CD-04-00-05'); + expect(macGenerator()).to.equal('01-0C-CD-04-00-0A'); + expect(macGenerator()).to.equal('01-0C-CD-04-00-0C'); + }); + }); +}); + +describe('APPID generator function', () => { + let appidGenerator: () => string; + let doc: XMLDocument; + + describe('for GSE elements Type1B (default)', () => { + beforeEach(() => { + doc = new DOMParser().parseFromString( + ` +

0001

+

0002

+

0004

+

0005

+

0006

+

0007

+

0008

+

0009

+

000A

+

000C

+

000E

+

000F

+

0010

+
`, + 'application/xml' + ); + + appidGenerator = appIdGenerator(doc, 'GSE'); + }); + + it('returns unique APPID', () => expect(appidGenerator()).to.equal('0000')); + + it('always returns unique APPID', () => { + expect(appidGenerator()).to.equal('0000'); + expect(appidGenerator()).to.equal('0003'); + expect(appidGenerator()).to.equal('000B'); + expect(appidGenerator()).to.equal('000D'); + expect(appidGenerator()).to.equal('0011'); + }); + }); + + describe('for GSE elements Type1A (Trip)', () => { + beforeEach(() => { + doc = new DOMParser().parseFromString( + ` +

8001

+

8002

+

8004

+

8005

+

8006

+

8007

+

8008

+

8009

+

800A

+

800C

+

800E

+

800F

+

8010

+
`, + 'application/xml' + ); + + appidGenerator = appIdGenerator(doc, 'GSE', true); + }); + + it('returns unique APPID', () => expect(appidGenerator()).to.equal('8000')); + + it('always returns unique APPID', () => { + expect(appidGenerator()).to.equal('8000'); + expect(appidGenerator()).to.equal('8003'); + expect(appidGenerator()).to.equal('800B'); + expect(appidGenerator()).to.equal('800D'); + expect(appidGenerator()).to.equal('8011'); + }); + }); + + describe('for SMV elements', () => { + beforeEach(() => { + doc = new DOMParser().parseFromString( + ` +

4000

+

4001

+

4002

+

4004

+

4005

+

4007

+

4009

+

400A

+

400B

+

400D

+

400E

+

4011

+

4009

+
`, + 'application/xml' + ); + + appidGenerator = appIdGenerator(doc, 'SMV'); + }); + + it('returns unique APPID', () => expect(appidGenerator()).to.equal('4003')); + + it('always returns unique APPID', () => { + expect(appidGenerator()).to.equal('4003'); + expect(appidGenerator()).to.equal('4006'); + expect(appidGenerator()).to.equal('4008'); + expect(appidGenerator()).to.equal('400C'); + }); + }); +}); From faa555403e19ecabdd92a8096723536a5b371bf6 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Sun, 25 Sep 2022 22:24:32 +0200 Subject: [PATCH 2/2] feat(wizards/connectedap): create GSE and SMV --- src/wizards/connectedap.ts | 262 +++++- test/testfiles/wizards/communication.scd | 750 ++++++++++++++++++ .../__snapshots__/connectedap-c.test.snap.js | 224 ++++++ .../__snapshots__/connectedap.test.snap.js | 82 -- test/unit/wizards/connectedap-c.test.ts | 218 +++++ test/unit/wizards/connectedap.test.ts | 86 +- 6 files changed, 1436 insertions(+), 186 deletions(-) create mode 100644 test/testfiles/wizards/communication.scd create mode 100644 test/unit/wizards/__snapshots__/connectedap-c.test.snap.js delete mode 100644 test/unit/wizards/__snapshots__/connectedap.test.snap.js create mode 100644 test/unit/wizards/connectedap-c.test.ts diff --git a/src/wizards/connectedap.ts b/src/wizards/connectedap.ts index e1df9cb16..c2ac8033c 100644 --- a/src/wizards/connectedap.ts +++ b/src/wizards/connectedap.ts @@ -24,6 +24,7 @@ import { ComplexAction, isPublic, identity, + SimpleAction, } from '../foundation.js'; import { getTypes, @@ -31,6 +32,10 @@ import { typeNullable, typePattern, } from './foundation/p-types.js'; +import { + mACAddressGenerator, + appIdGenerator, +} from '../foundation/generators.js'; interface AccessPointDescription { element: Element; @@ -46,28 +51,243 @@ function compareAccessPointConnection( return 0; } +function initSMVElements( + doc: XMLDocument, + connectedAp: Element, + options: { + macGeneratorSmv: () => string; + appidGeneratorSmv: () => string; + unconnectedSampledValueControl: Set; + } +): SimpleAction[] { + const actions: SimpleAction[] = []; + + const ied = doc.querySelector( + `IED[name="${connectedAp.getAttribute('iedName')}"]` + ); + + Array.from(ied?.querySelectorAll('SampledValueControl') ?? []) + .filter(sampledValueControl => { + const id = identity(sampledValueControl) as string; + + if (options.unconnectedSampledValueControl.has(id)) { + options.unconnectedSampledValueControl.delete(id); + return true; + } + + return false; + }) + .forEach(sampledValueControl => { + const cbName = sampledValueControl.getAttribute('name'); + const ldInst = + sampledValueControl.closest('LDevice')?.getAttribute('inst') ?? null; + + const sMV = createElement(connectedAp.ownerDocument, 'SMV', { + cbName, + ldInst, + }); + actions.push({ new: { parent: connectedAp, element: sMV } }); + + const address = createElement(connectedAp.ownerDocument, 'Address', {}); + actions.push({ new: { parent: sMV, element: address } }); + + const pMac = createElement(connectedAp.ownerDocument, 'P', { + type: 'MAC-Address', + }); + pMac.textContent = options.macGeneratorSmv(); + actions.push({ new: { parent: address, element: pMac } }); + + const pAppId = createElement(connectedAp.ownerDocument, 'P', { + type: 'APPID', + }); + pAppId.textContent = options.appidGeneratorSmv(); + actions.push({ new: { parent: address, element: pAppId } }); + + const pVlanId = createElement(connectedAp.ownerDocument, 'P', { + type: 'VLANID', + }); + pVlanId.textContent = '000'; + actions.push({ new: { parent: address, element: pVlanId } }); + + const pVlanPrio = createElement(connectedAp.ownerDocument, 'P', { + type: 'VLAN-Priority', + }); + pVlanPrio.textContent = '4'; + actions.push({ new: { parent: address, element: pVlanPrio } }); + }); + + return actions; +} + +function initGSEElements( + doc: XMLDocument, + connectedAp: Element, + options: { + macGeneratorGse: () => string; + appidGeneratorGse: () => string; + unconnectedGseControl: Set; + } +): SimpleAction[] { + const actions: SimpleAction[] = []; + + const ied = doc.querySelector( + `IED[name="${connectedAp.getAttribute('iedName')}"]` + ); + + Array.from(ied?.querySelectorAll('GSEControl') ?? []) + .filter(gseControl => { + const id = identity(gseControl) as string; + + if (options.unconnectedGseControl.has(id)) { + options.unconnectedGseControl.delete(id); + return true; + } + + return false; + }) + .forEach(gseControl => { + const cbName = gseControl.getAttribute('name'); + const ldInst = + gseControl.closest('LDevice')?.getAttribute('inst') ?? null; + + const gSE = createElement(connectedAp.ownerDocument, 'GSE', { + cbName, + ldInst, + }); + actions.push({ new: { parent: connectedAp, element: gSE } }); + + const address = createElement(connectedAp.ownerDocument, 'Address', {}); + actions.push({ new: { parent: gSE, element: address } }); + + const pMac = createElement(connectedAp.ownerDocument, 'P', { + type: 'MAC-Address', + }); + pMac.textContent = options.macGeneratorGse(); + actions.push({ new: { parent: address, element: pMac } }); + + const pAppId = createElement(connectedAp.ownerDocument, 'P', { + type: 'APPID', + }); + pAppId.textContent = options.appidGeneratorGse(); + actions.push({ new: { parent: address, element: pAppId } }); + + const pVlanId = createElement(connectedAp.ownerDocument, 'P', { + type: 'VLANID', + }); + pVlanId.textContent = '000'; + actions.push({ new: { parent: address, element: pVlanId } }); + + const pVlanPrio = createElement(connectedAp.ownerDocument, 'P', { + type: 'VLAN-Priority', + }); + pVlanPrio.textContent = '4'; + actions.push({ new: { parent: address, element: pVlanPrio } }); + + const minTime = createElement(connectedAp.ownerDocument, 'MinTime', { + unit: 's', + multiplier: 'm', + }); + minTime.textContent = '10'; + actions.push({ new: { parent: gSE, element: minTime } }); + + const maxTime = createElement(connectedAp.ownerDocument, 'MaxTime', { + unit: 's', + multiplier: 'm', + }); + maxTime.textContent = '10000'; + actions.push({ new: { parent: gSE, element: maxTime } }); + }); + + return actions; +} + +function unconnectedGseControls(doc: XMLDocument): Set { + const allGseControl = Array.from(doc.querySelectorAll('GSEControl')); + + const unconnectedGseControl = allGseControl + .filter(gseControl => { + const iedName = gseControl.closest('IED')?.getAttribute('name'); + const ldInst = gseControl.closest('LDevice')?.getAttribute('inst'); + const cbName = gseControl.getAttribute('name'); + + return !doc.querySelector( + `ConnectedAP[iedName="${iedName}"] ` + + `> GSE[ldInst="${ldInst}"][cbName="${cbName}"]` + ); + }) + .map(gseControl => identity(gseControl) as string); + + const mySet = new Set(unconnectedGseControl); + return mySet; +} + +function unconnectedSampledValueControls(doc: XMLDocument): Set { + const allSmvControl = Array.from(doc.querySelectorAll('SampledValueControl')); + + const unconnectedSmvControl = allSmvControl + .filter(gseControl => { + const iedName = gseControl.closest('IED')?.getAttribute('name'); + const ldInst = gseControl.closest('LDevice')?.getAttribute('inst'); + const cbName = gseControl.getAttribute('name'); + + return !doc.querySelector( + `ConnectedAP[iedName="${iedName}"] ` + + `> SMV[ldInst="${ldInst}"][cbName="${cbName}"]` + ); + }) + .map(gseControl => identity(gseControl) as string); + + const mySet = new Set(unconnectedSmvControl); + return mySet; +} + function createConnectedApAction(parent: Element): WizardActor { return ( _: WizardInputElement[], __: Element, list?: List | null ): EditorAction[] => { + const doc = parent.ownerDocument; + + // generators ensure unique MAC-Address and APPID across the project + const macGeneratorSmv = mACAddressGenerator(doc, 'SMV'); + const appidGeneratorSmv = appIdGenerator(doc, 'SMV'); + const macGeneratorGse = mACAddressGenerator(doc, 'GSE'); + const appidGeneratorGse = appIdGenerator(doc, 'GSE'); + + // track GSE and SMV for multiselect access points connection + const unconnectedGseControl = unconnectedGseControls(doc); + const unconnectedSampledValueControl = unconnectedSampledValueControls(doc); + if (!list) return []; const identities = (list.selected).map(item => item.value); const actions = identities.map(identity => { const [iedName, apName] = identity.split('>'); + const actions: SimpleAction[] = []; - return { - new: { - parent, - element: createElement(parent.ownerDocument, 'ConnectedAP', { - iedName, - apName, - }), - }, - }; + const connectedAp = createElement(parent.ownerDocument, 'ConnectedAP', { + iedName, + apName, + }); + actions.push({ new: { parent, element: connectedAp } }); + actions.push( + ...initSMVElements(doc, connectedAp, { + macGeneratorSmv, + appidGeneratorSmv, + unconnectedSampledValueControl, + }) + ); + actions.push( + ...initGSEElements(doc, connectedAp, { + macGeneratorGse, + appidGeneratorGse, + unconnectedGseControl, + }) + ); + + return { title: 'Added ConnectedAP', actions }; }); return actions; @@ -90,27 +310,31 @@ function existConnectedAp(accesspoint: Element): boolean { * @param element - The ConnectedAP of the wizard. * @returns The checkbox within a formfield. */ -export function createTypeRestrictionCheckbox(element: Element): TemplateResult { +export function createTypeRestrictionCheckbox( + element: Element +): TemplateResult { return html` - ` + `; } -export function createPTextField(element: Element, pType: string): TemplateResult { +export function createPTextField( + element: Element, + pType: string +): TemplateResult { return html` P[type="${pType}"]` - )?.innerHTML ?? null} + .maybeValue=${element.querySelector(`Address > P[type="${pType}"]`) + ?.innerHTML ?? null} maxLength="${ifDefined(typeMaxLength[pType])}" - >` + >`; } /** @returns single page [[`Wizard`]] for creating SCL element ConnectedAP. */ @@ -251,9 +475,9 @@ export function editConnectedApWizard(element: Element): Wizard { }, content: [ html`${createTypeRestrictionCheckbox(element)} - ${getTypes(element).map( - pType => html`${createPTextField(element, pType)}` - )}`, + ${getTypes(element).map( + pType => html`${createPTextField(element, pType)}` + )}`, ], }, ]; diff --git a/test/testfiles/wizards/communication.scd b/test/testfiles/wizards/communication.scd new file mode 100644 index 000000000..04f69c21e --- /dev/null +++ b/test/testfiles/wizards/communication.scd @@ -0,0 +1,750 @@ + + +
+ + + 110 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

192.168.210.111

+
+ +
+

01-0C-CD-01-00-00

+

0000

+
+
+
+
+ + +
+

192.168.210.113

+
+ +
+

01-0C-CD-04-00-01

+

4002

+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + + status-only + + + + + + + + + + + + + + + sbo-with-enhanced-security + + + 30000 + + + 600 + + + + + + + + + + + + + + + + + + + + + + + + + IEC 61850-7-4:2007B4 + + + + + + + + + + + + + + + + + + + sbo-with-enhanced-security + + + 30000 + + + 600 + + + + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + A + + + + + 0.01 + + + 0 + + + + + Hz + + + + + + + + + A + + + + + 0.001 + + + 0 + + + + + + + + + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + y + z + a + f + p + n + µ + m + c + d + + da + h + k + M + G + T + P + E + Z + Y + + + + m + kg + s + A + K + mol + cd + deg + rad + sr + Gy + Bq + °C + Sv + F + C + S + H + V + ohm + J + N + Hz + lx + Lm + Wb + T + W + Pa + + + m/s + m/s² + m³/s + m/m³ + M + kg/m³ + m²/s + W/m K + J/K + ppm + 1/s + rad/s + W/m² + J/m² + S/m + K/s + Pa/s + J/kg K + VA + Watts + VAr + phi + cos(phi) + Vs + + As + + A²t + VAh + Wh + VArh + V/Hz + Hz/s + char + char/s + kgm² + dB + J/Wh + W/s + l/s + dBm + h + min + Ohm/m + percent/s + + + Load Break + Disconnector + Earthing Switch + High Speed Earthing Switch + + + status-only + + + pulse + persistent + persistent-feedback + + + Ok + Warning + Alarm + + + status-only + direct-with-normal-security + sbo-with-normal-security + direct-with-enhanced-security + sbo-with-enhanced-security + + + on + blocked + test + test/blocked + off + + + not-supported + bay-control + station-control + remote-control + automatic-bay + automatic-station + automatic-remote + maintenance + process + + + diff --git a/test/unit/wizards/__snapshots__/connectedap-c.test.snap.js b/test/unit/wizards/__snapshots__/connectedap-c.test.snap.js new file mode 100644 index 000000000..3a884af34 --- /dev/null +++ b/test/unit/wizards/__snapshots__/connectedap-c.test.snap.js @@ -0,0 +1,224 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["create wizard for ConnectedAP element looks like the latest snapshot"] = +` +
+ + + + GOOSE_Publisher>AP2 + + + + + GOOSE_Publisher>AP3 + + + + + GOOSE_Publisher>AP4 + + + + + GOOSE_Publisher2>AP1 + + + + + GOOSE_Publisher2>AP2 + + + + + GOOSE_Publisher2>AP3 + + + + + GOOSE_Publisher2>AP4 + + + + + GOOSE_Subscriber>AP1 + + + + + GOOSE_Subscriber>AP2 + + + + + SMV_Publisher>AP1 + + + + + SMV_Publisher>AP3 + + + + + SMV_Publisher>AP4 + + + + + SMV_Publisher2>AP1 + + + + + SMV_Publisher2>AP2 + + + + + SMV_Publisher2>AP3 + + + + + GOOSE_Publisher>AP1 + + + + + SMV_Publisher>AP2 + + + +
+ + + + +
+`; +/* end snapshot create wizard for ConnectedAP element looks like the latest snapshot */ + diff --git a/test/unit/wizards/__snapshots__/connectedap.test.snap.js b/test/unit/wizards/__snapshots__/connectedap.test.snap.js deleted file mode 100644 index d6898ae7a..000000000 --- a/test/unit/wizards/__snapshots__/connectedap.test.snap.js +++ /dev/null @@ -1,82 +0,0 @@ -/* @web/test-runner snapshot v1 */ -export const snapshots = {}; - -snapshots["Wizards for SCL element ConnectedAP include a create wizard that looks like the latest snapshot"] = -` -
- - - - IED3>P2 - - - - - IED1>P1 - - - - - IED2>P1 - - - - - IED3>P1 - - - -
- - - - -
-`; -/* end snapshot Wizards for SCL element ConnectedAP include a create wizard that looks like the latest snapshot */ - diff --git a/test/unit/wizards/connectedap-c.test.ts b/test/unit/wizards/connectedap-c.test.ts new file mode 100644 index 000000000..7f06d842e --- /dev/null +++ b/test/unit/wizards/connectedap-c.test.ts @@ -0,0 +1,218 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import '../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../mock-wizard-editor.js'; + +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base.js'; + +import { createConnectedApWizard } from '../../../src/wizards/connectedap.js'; + +function isAllMacUnique(parent: Element, serviceType: 'GSE' | 'SMV'): boolean { + const allMacs = Array.from( + parent.ownerDocument.querySelectorAll( + `${serviceType} > Address > P[type="MAC-Address"]` + ) + ).map(pType => pType.textContent!); + + const set = new Set(allMacs); + + return allMacs.length === set.size; +} + +function isAllAppIdUnique( + parent: Element, + serviceType: 'GSE' | 'SMV' +): boolean { + const allMacs = Array.from( + parent.ownerDocument.querySelectorAll( + `${serviceType} > Address > P[type="APPID"]` + ) + ).map(pType => pType.textContent!); + + const set = new Set(allMacs); + + return allMacs.length === set.size; +} + +async function clickListItem( + element: MockWizardEditor, + values: string[] +): Promise { + Array.from(values).forEach(value => { + element.wizardUI + .dialog!.querySelector( + `mwc-check-list-item[value="${value}"]` + ) + ?.click(); + }); + + await element.updateComplete; + + (( + element.wizardUI.dialog?.querySelector('mwc-button[slot="primaryAction"]') + )).click(); + + await element.updateComplete; +} + +describe('create wizard for ConnectedAP element', () => { + let doc: XMLDocument; + let element: MockWizardEditor; + let parent: Element; + + beforeEach(async () => { + element = ( + await fixture(html``) + ); + + doc = await fetch('/test/testfiles/wizards/communication.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + parent = doc.querySelector('SubNetwork')!; + const wizard = createConnectedApWizard(parent); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + /* + inputs = Array.from(element.wizardUI.inputs); + */ + }); + + it('looks like the latest snapshot', async () => + await expect(element.wizardUI.dialog).dom.to.equalSnapshot()); + + it('it does not allow to add already connected access points', () => { + const disabledItems = Array.from( + element.wizardUI.dialog!.querySelectorAll( + 'mwc-check-list-item' + ) + ).filter(item => item.disabled); + + for (const item of disabledItems) { + const [iedName, apName] = item.value.split('>'); + expect( + doc.querySelector( + `ConnectedAP[iedName="${iedName}"][apName="${apName}"]` + ) + ).to.exist; + } + }); + + describe('on connecting one new access point', () => { + it('adds a new ConnectedAP element', async () => { + await clickListItem(element, ['GOOSE_Publisher>AP2']); + + expect( + parent.querySelector( + 'ConnectedAP[iedName="GOOSE_Publisher"][apName="AP2"]' + ) + ).to.exist; + }); + + describe('with publishing GSEControl or SampledValueControl', () => { + it('create unique GSE for each GSEControl', async () => { + await clickListItem(element, ['GOOSE_Publisher>AP2']); + expect( + parent.querySelectorAll( + 'ConnectedAP[iedName="GOOSE_Publisher"][apName="AP2"] ' + '> GSE' + ) + ).to.have.length(2); + }); + + it('adds uniques GSE MAC-Address and APPID', async () => { + const value = 'GOOSE_Publisher>AP2'; + await clickListItem(element, [value]); + + expect(isAllMacUnique(parent, 'GSE')).to.be.true; + expect(isAllAppIdUnique(parent, 'GSE')).to.be.true; + }); + + it('create unique SMV for each SampledValueControl', async () => { + await clickListItem(element, ['SMV_Publisher>AP1']); + + expect( + parent.querySelectorAll( + 'ConnectedAP[iedName="SMV_Publisher"][apName="AP1"] ' + '> SMV' + ) + ).to.have.length(2); + }); + + it('adds uniques SMV MAC-Address and APPID', async () => { + const value = 'SMV_Publisher>AP1'; + await clickListItem(element, [value]); + + expect(isAllMacUnique(parent, 'SMV')).to.be.true; + expect(isAllAppIdUnique(parent, 'SMV')).to.be.true; + }); + }); + }); + + describe('on connecting multiple new access point', () => { + it('adds new ConnectedAP element for each selected acc p', async () => { + await clickListItem(element, [ + 'GOOSE_Publisher>AP2', + 'GOOSE_Publisher>AP3', + ]); + + expect( + parent.querySelector( + 'ConnectedAP[iedName="GOOSE_Publisher"][apName="AP2"]' + ) + ).to.exist; + + expect( + parent.querySelector( + 'ConnectedAP[iedName="GOOSE_Publisher"][apName="AP3"]' + ) + ).to.exist; + }); + + describe('with publishing GSEControl or SampledValueControl', () => { + it('create unique GSE for each GSEControl', async () => { + await clickListItem(element, [ + 'GOOSE_Publisher>AP2', + 'GOOSE_Publisher>AP3', + ]); + expect( + parent.ownerDocument.querySelectorAll( + 'ConnectedAP[iedName="GOOSE_Publisher"]' + '> GSE' + ) + ).to.have.length(3); + }); + + it('adds uniques GSE MAC-Address and APPID', async () => { + await clickListItem(element, [ + 'GOOSE_Publisher>AP2', + 'GOOSE_Publisher2>AP1', + ]); + + expect(isAllMacUnique(parent, 'GSE')).to.be.true; + expect(isAllMacUnique(parent, 'GSE')).to.be.true; + }); + + it('create unique SMV for each SampledValueControl', async () => { + await clickListItem(element, [ + 'SMV_Publisher>AP1', + 'SMV_Publisher>AP4', + ]); + + expect( + parent.ownerDocument.querySelectorAll( + 'ConnectedAP[iedName="SMV_Publisher"] ' + '> SMV' + ) + ).to.have.length(3); + }); + + it('adds uniques MAC-Address and APPID', async () => { + await clickListItem(element, [ + 'SMV_Publisher>AP1', + 'SMV_Publisher2>AP1', + ]); + + expect(isAllMacUnique(parent, 'SMV')).to.be.true; + expect(isAllAppIdUnique(parent, 'SMV')).to.be.true; + }); + }); + }); +}); diff --git a/test/unit/wizards/connectedap.test.ts b/test/unit/wizards/connectedap.test.ts index 8ed9ccd53..833c5f063 100644 --- a/test/unit/wizards/connectedap.test.ts +++ b/test/unit/wizards/connectedap.test.ts @@ -16,11 +16,7 @@ import { Create, WizardInputElement, } from '../../../src/foundation.js'; -import { - createConnectedApWizard, - editConnectedApWizard, -} from '../../../src/wizards/connectedap.js'; -import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; +import { editConnectedApWizard } from '../../../src/wizards/connectedap.js'; describe('Wizards for SCL element ConnectedAP', () => { let doc: XMLDocument; @@ -166,84 +162,4 @@ describe('Wizards for SCL element ConnectedAP', () => { ).to.exist; }); }); - - describe('include a create wizard that', () => { - beforeEach(async () => { - doc = await fetch('/test/testfiles/valid2007B4.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - const wizard = createConnectedApWizard(doc.querySelector('ConnectedAP')!); - element.workflow.push(() => wizard); - await element.requestUpdate(); - - inputs = Array.from(element.wizardUI.inputs); - - primaryAction = ( - element.wizardUI.dialog?.querySelector( - 'mwc-button[slot="primaryAction"]' - ) - ); - }); - - it('looks like the latest snapshot', async () => { - await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); - }); - - it('does not allow to add connected AccessPoints', () => { - const disabledItems = Array.from( - element.wizardUI.dialog!.querySelectorAll( - 'mwc-check-list-item' - ) - ).filter(item => item.disabled); - - for (const item of disabledItems) { - const [iedName, apName] = item.value.split('>'); - expect( - doc.querySelector( - `ConnectedAP[iedName="${iedName}"][apName="${apName}"]` - ) - ).to.exist; - } - }); - - it('allows to add unconnected AccessPoints', () => { - const enabledItems = Array.from( - element.wizardUI.dialog!.querySelectorAll( - 'mwc-check-list-item' - ) - ).filter(item => !item.disabled); - - for (const item of enabledItems) { - const [iedName, apName] = item.value.split('>'); - expect( - doc.querySelector( - `ConnectedAP[iedName="${iedName}"][apName="${apName}"]` - ) - ).to.not.exist; - } - }); - - it('shows all AccessPoint in the project', async () => - expect( - element.wizardUI.dialog?.querySelectorAll('mwc-check-list-item').length - ).to.equal(doc.querySelectorAll(':root > IED > AccessPoint').length)); - - it('triggers a create editor action on primary action', async () => { - Array.from( - element.wizardUI.dialog!.querySelectorAll( - 'mwc-check-list-item' - ) - ) - .filter(item => !item.disabled)[0] - .click(); - await element.requestUpdate(); - - primaryAction.click(); - await element.requestUpdate(); - - expect(actionEvent).to.be.calledOnce; - expect(actionEvent.args[0][0].detail.action).to.satisfy(isCreate); - }); - }); });