diff --git a/src/editors/communication/connectedap-editor.ts b/src/editors/communication/connectedap-editor.ts index 454f1b088a..10cc184ee6 100644 --- a/src/editors/communication/connectedap-editor.ts +++ b/src/editors/communication/connectedap-editor.ts @@ -5,273 +5,12 @@ import { html, property, } from 'lit-element'; -import { ifDefined } from 'lit-html/directives/if-defined'; -import { translate, get } from 'lit-translate'; -import '@material/mwc-checkbox'; import '@material/mwc-fab'; -import '@material/mwc-formfield'; -import '@material/mwc-list/mwc-list-item'; -import '@material/mwc-list/mwc-check-list-item'; -import '@material/mwc-icon'; -import { Checkbox } from '@material/mwc-checkbox'; -import { List } from '@material/mwc-list'; -import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../../action-icon.js'; -import '../../wizard-textfield.js'; -import '../../filtered-list.js'; -import { - EditorAction, - newWizardEvent, - Wizard, - WizardActor, - WizardInput, - newActionEvent, - compareNames, - getValue, - createElement, - ComplexAction, -} from '../../foundation.js'; -import { selectors } from './foundation.js'; -import { - getTypes, - typePattern, - typeNullable, - typeMaxLength, -} from './p-types.js'; - -/** Data needed to uniquely identify an `AccessPoint` */ -interface apAttributes { - iedName: string; - apName: string; -} - -/** Description of a `ListItem` representing an `IED` and `AccessPoint` */ -interface ItemDescription { - value: apAttributes; - connected?: boolean; -} - -/** Sorts disabled `ListItem`s to the bottom. */ -function compareListItemConnection( - a: ItemDescription, - b: ItemDescription -): number { - if (a.connected !== b.connected) return b.connected ? -1 : 1; - return 0; -} - -function isEqualAddress(oldAddr: Element, newAdddr: Element): boolean { - return ( - Array.from(oldAddr.querySelectorAll(selectors.Address + ' > P')).filter( - pType => - !newAdddr - .querySelector(`Address > P[type="${pType.getAttribute('type')}"]`) - ?.isEqualNode(pType) - ).length === 0 - ); -} - -function createAddressElement( - inputs: WizardInput[], - parent: Element, - instType: boolean -): Element { - const element = createElement(parent.ownerDocument, 'Address', {}); - - inputs - .filter(input => getValue(input) !== null) - .forEach(validInput => { - const type = validInput.label; - const child = createElement(parent.ownerDocument, 'P', { type }); - if (instType) - child.setAttributeNS( - 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:type', - 'tP_' + type - ); - child.textContent = getValue(validInput); - element.appendChild(child); - }); - - return element; -} - -function createConnectedApAction(parent: Element): WizardActor { - return ( - inputs: WizardInput[], - wizard: Element, - list?: List | null - ): EditorAction[] => { - if (!list) return []; - - const apValue = (list.selected).map( - item => JSON.parse(item.value) - ); - - const actions = apValue.map( - value => - { - new: { - parent, - element: createElement(parent.ownerDocument, 'ConnectedAP', { - iedName: value.iedName, - apName: value.apName, - }), - }, - } - ); - - return actions; - }; -} - -function renderWizardPage(element: Element): TemplateResult { - const doc = element.ownerDocument; - - const accPoints = Array.from(doc.querySelectorAll(':root > IED')) - .sort(compareNames) - .flatMap(ied => - Array.from(ied.querySelectorAll(':root > IED > AccessPoint')) - ) - .map(accP => { - return { - iedName: accP.parentElement!.getAttribute('name')!, - apName: accP.getAttribute('name')!, - }; - }); - - const accPointDescription = accPoints - .map(value => { - return { - value, - connected: - doc?.querySelector( - `:root > Communication > SubNetwork > ConnectedAP[iedName="${value.iedName}"][apName="${value.apName}"]` - ) !== null, - }; - }) - .sort(compareListItemConnection); - - if (accPointDescription.length) - return html` ${accPointDescription.map( - item => html`${item.value.apName}${item.value.iedName}` - )} - `; - - return html` - ${translate('lnode.wizard.placeholder')} - info - `; -} - -/** @returns a Wizard for creating `element` `ConnectedAP`. */ -export function createConnectedApWizard(element: Element): Wizard { - return [ - { - title: get('connectedap.wizard.title.connect'), - primary: { - icon: 'save', - label: get('save'), - action: createConnectedApAction(element), - }, - content: [renderWizardPage(element)], - }, - ]; -} - -export function editConnectedApAction(parent: Element): WizardActor { - return (inputs: WizardInput[], wizard: Element): EditorAction[] => { - const instType: boolean = - (wizard.shadowRoot?.querySelector('#instType'))?.checked ?? - false; - - const newAddress = createAddressElement(inputs, parent, instType); - - const complexAction: ComplexAction = { - actions: [], - title: get('connectedap.action.addaddress', { - iedName: parent.getAttribute('iedName') ?? '', - apName: parent.getAttribute('apName') ?? '', - }), - }; - - const oldAddress = parent.querySelector(selectors.Address); - - if (oldAddress !== null && !isEqualAddress(oldAddress, newAddress)) { - // We cannot use updateAction on address as both address child elements P are changed - complexAction.actions.push({ - old: { - parent, - element: oldAddress, - reference: oldAddress.nextSibling, - }, - }); - complexAction.actions.push({ - new: { - parent, - element: newAddress, - reference: oldAddress.nextSibling, - }, - }); - } else if (oldAddress === null) - complexAction.actions.push({ - new: { - parent: parent, - element: newAddress, - }, - }); - - return [complexAction]; - }; -} - -function editConnectedApWizard(element: Element): Wizard { - return [ - { - title: get('connectedap.wizard.title.edit'), - element, - primary: { - icon: 'save', - label: get('save'), - action: editConnectedApAction(element), - }, - content: [ - html` - ${getTypes(element).map( - ptype => - html` Communication > SubNetwork > ConnectedAP > Address > P[type="${ptype}"]` - )?.innerHTML ?? null} - maxLength="${ifDefined(typeMaxLength[ptype])}" - >` - )}`, - ], - }, - ]; -} +import { newWizardEvent, newActionEvent } from '../../foundation.js'; +import { editConnectedApWizard } from '../../wizards/connectedap.js'; /** [[`Communication`]] subeditor for a `ConnectedAP` element. */ @customElement('connectedap-editor') @@ -279,7 +18,7 @@ export class ConnectedAPEditor extends LitElement { /** SCL element ConnectedAP */ @property({ attribute: false }) element!: Element; - /** ConductingEquipment apName attribute */ + /** ConnectedAP attribute apName */ @property({ type: String }) get apName(): string { return this.element.getAttribute('apName') ?? 'UNDEFINED'; diff --git a/src/editors/communication/p-types.ts b/src/editors/communication/p-types.ts deleted file mode 100644 index 223ebc5db0..0000000000 --- a/src/editors/communication/p-types.ts +++ /dev/null @@ -1,156 +0,0 @@ -// patterns are evolving over the years and thus with higher versions of the standards. -// However, their limits are essentially not defined in other standards. This meaning OpenSCD -// is not always using the pattern defined in the standard but the one using the user the best. -// e.g the definition of the pattern for IP adresses does allow 300.300.300.300 which is clearly not -// a valid IP adress -export function getTypes(element: Element): string[] { - if (!element.ownerDocument) return []; - - const scl: Element = element.ownerDocument.querySelector(':root')!; - - const type = - (scl.getAttribute('version') ?? '2003') + - (scl.getAttribute('revision') ?? '') + - (scl.getAttribute('release') ?? ''); - - if (type === '2003') return pTypes2003; - - if (type === '2007B') return pTypes2007B; - - return pTypes2007B3; -} - -const pTypes2003: string[] = [ - 'IP', - 'IP-SUBNET', - 'IP-GATEWAY', - 'OSI-TSEL', - 'OSI-SSEL', - 'OSI-PSEL', - 'OSI-AP-Title', - 'OSI-AP-Invoke', - 'OSI-AE-Qualifier', - 'OSI-AE-Invoke', - 'OSI-NSAP', - 'VLAN-ID', - 'VLAN-PRIORITY', -]; - -const pTypes2007B: string[] = [ - ...pTypes2003, - 'SNTP-Port', - 'MMS-Port', - 'DNSName', - 'UDP-Port', - 'TCP-Port', - 'C37-118-IP-Port', -]; - -const pTypes2007B3: string[] = [ - ...pTypes2007B, - 'IPv6', - 'IPv6-SUBNET', - 'IPv6-GATEWAY', - 'IPv6FlowLabel', - 'IPv6ClassOfTraffic', - 'IPv6-IGMPv3Src', - 'IP-IGMPv3Sr', - 'IP-ClassOfTraffic', -]; - -export const pTypesGSESMV: string[] = [ - 'MAC-Address', - 'APPID', - 'VLAN-ID', - 'VLAN-PRIORITY', -]; - -export const typeNullable: Partial> = { - IP: false, - 'IP-SUBNET': false, - 'IP-GATEWAY': false, - 'OSI-TSEL': false, - 'OSI-SSEL': false, - 'OSI-PSEL': false, - 'OSI-AP-Title': true, - 'OSI-AP-Invoke': true, - 'OSI-AE-Qualifier': true, - 'OSI-AE-Invoke': true, - 'OSI-NSAP': true, - 'MAC-Address': false, - APPID: false, - 'VLAN-ID': true, - 'VLAN-PRIORITY': true, - 'SNTP-Port': true, - 'MMS-Port': true, - DNSName: true, - 'UDP-Port': true, - 'TCP-Port': true, - 'C37-118-IP-Port': true, - IPv6: true, - 'IPv6-SUBNET': true, - 'IPv6-GATEWAY': true, - IPv6FlowLabel: true, - IPv6ClassOfTraffic: true, - 'IPv6-IGMPv3Src': true, - 'IP-IGMPv3Sr': true, - 'IP-ClassOfTraffic': true, -}; - -const typeBase = { - IP: '([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])', - OSI: '[0-9A-F]+', - OSId: '[0-9]+', - OSIAPi: '[0-9\u002C]+', - MAC: '([0-9A-F]{2}-){5}[0-9A-F]{2}', - APPID: '[0-9A-F]{4}', - VLANp: '[0-7]', - VLANid: '[0-9A-F]{3}', - port: '0|([1-9][0-9]{0,3})|([1-5][0-9]{4,4})|(6[0-4][0-9]{3,3})|(65[0-4][0-9]{2,2})|(655[0-2][0-9])|(6553[0-5])', - IPv6: '([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}', - IPv6sub: '/[1-9]|/[1-9][0-9]|/1[0-1][0-9]|/12[0-7]', -}; - -export const typePattern: Partial> = { - IP: typeBase.IP, - 'IP-SUBNET': typeBase.IP, - 'IP-GATEWAY': typeBase.IP, - 'OSI-TSEL': typeBase.OSI, - 'OSI-SSEL': typeBase.OSI, - 'OSI-PSEL': typeBase.OSI, - 'OSI-AP-Title': typeBase.OSIAPi, - 'OSI-AP-Invoke': typeBase.OSId, - 'OSI-AE-Qualifier': typeBase.OSId, - 'OSI-AE-Invoke': typeBase.OSId, - 'MAC-Address': typeBase.MAC, - APPID: typeBase.APPID, - 'VLAN-ID': typeBase.VLANid, - 'VLAN-PRIORITY': typeBase.VLANp, - 'OSI-NSAP': typeBase.OSI, - 'SNTP-Port': typeBase.port, - 'MMS-Port': typeBase.port, - DNSName: '[^ ]*', - 'UDP-Port': typeBase.port, - 'TCP-Port': typeBase.port, - 'C37-118-IP-Port': - '102[5-9]|10[3-9][0-9]|1[1-9][0-9][0-9]|[2-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]', - IPv6: typeBase.IPv6, - 'IPv6-SUBNET': typeBase.IPv6sub, - 'IPv6-GATEWAY': typeBase.IPv6, - IPv6FlowLabel: '[0-9a-fA-F]{1,5}', - IPv6ClassOfTraffic: '[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]', - 'IPv6-IGMPv3Src': typeBase.IPv6, - 'IP-IGMPv3Sr': typeBase.IP, - 'IP-ClassOfTraffic': typeBase.OSI, -}; - -export const typeMaxLength: Partial> = { - 'OSI-TSEL': 8, - 'OSI-SSEL': 16, - 'OSI-PSEL': 16, - 'OSI-AP-Invoke': 5, - 'OSI-AE-Qualifier': 5, - 'OSI-AE-Invoke': 5, - 'OSI-NSAP': 40, - 'IP-ClassOfTraffic': 2, -}; diff --git a/src/editors/communication/subnetwork-editor.ts b/src/editors/communication/subnetwork-editor.ts index 3d07e7afd4..3cb5f133aa 100644 --- a/src/editors/communication/subnetwork-editor.ts +++ b/src/editors/communication/subnetwork-editor.ts @@ -27,7 +27,7 @@ import { cloneElement, } from '../../foundation.js'; import { styles, WizardOptions, isCreateOptions } from './foundation.js'; -import { createConnectedApWizard } from './connectedap-editor.js'; +import { createConnectedApWizard } from '../../wizards/connectedap.js'; /** Initial attribute values suggested for `SubNetwork` creation */ const initial = { diff --git a/src/translations/de.ts b/src/translations/de.ts index ef1dfc2302..9c3759716d 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -371,10 +371,6 @@ export const de: Translations = { name: 'Schnittstelle', wizard: { addschemainsttype: 'XMLSchema-instance type hinzufügen', - title: { - connect: 'Schnittstelle verbinden', - edit: 'Schnittstelle bearbeiten', - }, }, action: { addaddress: 'Adressfeld bearbeitet ({{iedName}} - {{apName}})', diff --git a/src/translations/en.ts b/src/translations/en.ts index 1d6531e7ea..4e61734d9b 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -368,10 +368,6 @@ export const en = { name: 'Connected access point', wizard: { addschemainsttype: 'Add XMLSchema-instance type', - title: { - connect: 'Connect access point', - edit: 'Edit access point', - }, }, action: { addaddress: 'Edit Address ({{iedName}} - {{apName}})', diff --git a/src/wizards/connectedap.ts b/src/wizards/connectedap.ts new file mode 100644 index 0000000000..e8448b5e0f --- /dev/null +++ b/src/wizards/connectedap.ts @@ -0,0 +1,247 @@ +import { html } from 'lit-element'; +import { ifDefined } from 'lit-html/directives/if-defined'; +import { translate, get } from 'lit-translate'; + +import '@material/mwc-checkbox'; +import '@material/mwc-formfield'; +import '@material/mwc-list/mwc-list-item'; +import '@material/mwc-list/mwc-check-list-item'; +import '@material/mwc-icon'; +import { Checkbox } from '@material/mwc-checkbox'; +import { List } from '@material/mwc-list'; +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; + +import '../wizard-textfield.js'; +import '../filtered-list.js'; +import { + EditorAction, + Wizard, + WizardActor, + WizardInput, + compareNames, + getValue, + createElement, + ComplexAction, + isPublic, + identity, +} from '../foundation.js'; +import { + getTypes, + typeMaxLength, + typeNullable, + typePattern, +} from './foundation/p-types.js'; + +interface AccessPointDescription { + element: Element; + connected?: boolean; +} + +/** Sorts connected `AccessPoint`s to the bottom. */ +function compareAccessPointConnection( + a: AccessPointDescription, + b: AccessPointDescription +): number { + if (a.connected !== b.connected) return b.connected ? -1 : 1; + return 0; +} + +function createConnectedApAction(parent: Element): WizardActor { + return ( + _: WizardInput[], + __: Element, + list?: List | null + ): EditorAction[] => { + if (!list) return []; + + const identities = (list.selected).map(item => item.value); + + const actions = identities.map(identity => { + const [iedName, apName] = identity.split('>'); + + return { + new: { + parent, + element: createElement(parent.ownerDocument, 'ConnectedAP', { + iedName, + apName, + }), + }, + }; + }); + + return actions; + }; +} + +function existConnectedAp(accesspoint: Element): boolean { + const iedName = accesspoint.closest('IED')?.getAttribute('name'); + const apName = accesspoint.getAttribute('name'); + + const connAp = accesspoint.ownerDocument.querySelector( + `ConnectedAP[iedName="${iedName}"][apName="${apName}"]` + ); + + return (connAp && isPublic(connAp)) ?? false; +} + +/** @returns single page [[`Wizard`]] for creating SCL element ConnectedAP. */ +export function createConnectedApWizard(element: Element): Wizard { + const doc = element.ownerDocument; + + const accessPoints = Array.from(doc.querySelectorAll(':root > IED')) + .sort(compareNames) + .flatMap(ied => + Array.from(ied.querySelectorAll(':root > IED > AccessPoint')) + ) + .map(accesspoint => { + return { + element: accesspoint, + connected: existConnectedAp(accesspoint), + }; + }) + .sort(compareAccessPointConnection); + + return [ + { + title: get('wizard.title.add', { tagName: 'ConnectedAP' }), + primary: { + icon: 'save', + label: get('save'), + action: createConnectedApAction(element), + }, + content: [ + html` ${accessPoints.map(accesspoint => { + const id = identity(accesspoint.element); + + return html`${id}`; + })} + `, + ], + }, + ]; +} + +function isEqualAddress(oldAddress: Element, newAddress: Element): boolean { + return Array.from(oldAddress.querySelectorAll('Address > P')).every(pType => + newAddress + .querySelector(`Address > P[type="${pType.getAttribute('type')}"]`) + ?.isEqualNode(pType) + ); +} + +function createAddressElement( + inputs: WizardInput[], + parent: Element, + typeRestriction: boolean +): Element { + const element = createElement(parent.ownerDocument, 'Address', {}); + + inputs + .filter(input => getValue(input) !== null) + .forEach(validInput => { + const type = validInput.label; + const child = createElement(parent.ownerDocument, 'P', { type }); + + if (typeRestriction) + child.setAttributeNS( + 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:type', + 'tP_' + type + ); + + child.textContent = getValue(validInput); + element.appendChild(child); + }); + + return element; +} + +function updateConnectedApAction(parent: Element): WizardActor { + return (inputs: WizardInput[], wizard: Element): EditorAction[] => { + const typeRestriction: boolean = + (wizard.shadowRoot?.querySelector('#typeRestriction')) + ?.checked ?? false; + + const newAddress = createAddressElement(inputs, parent, typeRestriction); + const oldAddress = parent.querySelector('Address'); + + const complexAction: ComplexAction = { + actions: [], + title: get('connectedap.action.addaddress', { + iedName: parent.getAttribute('iedName') ?? '', + apName: parent.getAttribute('apName') ?? '', + }), + }; + if (oldAddress !== null && !isEqualAddress(oldAddress, newAddress)) { + // We cannot use updateAction on address as both address child elements P are changed + complexAction.actions.push({ + old: { + parent, + element: oldAddress, + }, + }); + complexAction.actions.push({ + new: { + parent, + element: newAddress, + }, + }); + } else if (oldAddress === null) + complexAction.actions.push({ + new: { + parent: parent, + element: newAddress, + }, + }); + + return complexAction.actions.length ? [complexAction] : []; + }; +} + +function hasTypeRestriction(element: Element): boolean { + return Array.from(element.querySelectorAll('Address > P')) + .filter(p => isPublic(p)) + .some(pType => pType.getAttribute('xsi:type')); +} + +/** @returns single page [[`Wizard`]] to edit SCL element ConnectedAP. */ +export function editConnectedApWizard(element: Element): Wizard { + return [ + { + title: get('wizard.title.edit', { tagName: element.tagName }), + element, + primary: { + icon: 'save', + label: get('save'), + action: updateConnectedApAction(element), + }, + content: [ + html`${getTypes(element).map( + ptype => + html` P[type="${ptype}"]` + )?.innerHTML ?? null} + maxLength="${ifDefined(typeMaxLength[ptype])}" + >` + )}`, + ], + }, + ]; +} diff --git a/src/wizards/foundation/p-types.ts b/src/wizards/foundation/p-types.ts index 0a6d8ad1a1..0deacdc79a 100644 --- a/src/wizards/foundation/p-types.ts +++ b/src/wizards/foundation/p-types.ts @@ -1,3 +1,61 @@ +/** Selects edition depending array of `P` element types + * Supported Editions are 1 (2003), 2 (2007B) and 2.1 (2007B4) + */ +export function getTypes(element: Element): string[] { + if (!element.ownerDocument) return []; + + const scl: Element = element.ownerDocument.querySelector(':root')!; + + const type = + (scl.getAttribute('version') ?? '2003') + + (scl.getAttribute('revision') ?? '') + + (scl.getAttribute('release') ?? ''); + + if (type === '2003') return pTypes2003; + + if (type === '2007B') return pTypes2007B; + + return pTypes2007B4; +} + +const pTypes2003: string[] = [ + 'IP', + 'IP-SUBNET', + 'IP-GATEWAY', + 'OSI-TSEL', + 'OSI-SSEL', + 'OSI-PSEL', + 'OSI-AP-Title', + 'OSI-AP-Invoke', + 'OSI-AE-Qualifier', + 'OSI-AE-Invoke', + 'OSI-NSAP', + 'VLAN-ID', + 'VLAN-PRIORITY', +]; + +const pTypes2007B: string[] = [ + ...pTypes2003, + 'SNTP-Port', + 'MMS-Port', + 'DNSName', + 'UDP-Port', + 'TCP-Port', + 'C37-118-IP-Port', +]; + +const pTypes2007B4: string[] = [ + ...pTypes2007B, + 'IPv6', + 'IPv6-SUBNET', + 'IPv6-GATEWAY', + 'IPv6FlowLabel', + 'IPv6ClassOfTraffic', + 'IPv6-IGMPv3Src', + 'IP-IGMPv3Sr', + 'IP-ClassOfTraffic', +]; + export const pTypesGSESMV: string[] = [ 'MAC-Address', 'APPID', @@ -5,16 +63,95 @@ export const pTypesGSESMV: string[] = [ 'VLAN-PRIORITY', ]; +const typeBase = { + IP: '([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])', + OSI: '[0-9A-F]+', + OSId: '[0-9]+', + OSIAPi: '[0-9\u002C]+', + MAC: '([0-9A-F]{2}-){5}[0-9A-F]{2}', + APPID: '[0-9A-F]{4}', + VLANp: '[0-7]', + VLANid: '[0-9A-F]{3}', + port: '0|([1-9][0-9]{0,3})|([1-5][0-9]{4,4})|(6[0-4][0-9]{3,3})|(65[0-4][0-9]{2,2})|(655[0-2][0-9])|(6553[0-5])', + IPv6: '([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}', + IPv6sub: '/[1-9]|/[1-9][0-9]|/1[0-1][0-9]|/12[0-7]', +}; + +/** Patterns from IEC 61850-6 for all `P` elements */ +export const typePattern: Partial> = { + IP: typeBase.IP, + 'IP-SUBNET': typeBase.IP, + 'IP-GATEWAY': typeBase.IP, + 'OSI-TSEL': typeBase.OSI, + 'OSI-SSEL': typeBase.OSI, + 'OSI-PSEL': typeBase.OSI, + 'OSI-AP-Title': typeBase.OSIAPi, + 'OSI-AP-Invoke': typeBase.OSId, + 'OSI-AE-Qualifier': typeBase.OSId, + 'OSI-AE-Invoke': typeBase.OSId, + 'MAC-Address': typeBase.MAC, + APPID: typeBase.APPID, + 'VLAN-ID': typeBase.VLANid, + 'VLAN-PRIORITY': typeBase.VLANp, + 'OSI-NSAP': typeBase.OSI, + 'SNTP-Port': typeBase.port, + 'MMS-Port': typeBase.port, + DNSName: '[^ ]*', + 'UDP-Port': typeBase.port, + 'TCP-Port': typeBase.port, + 'C37-118-IP-Port': + '102[5-9]|10[3-9][0-9]|1[1-9][0-9][0-9]|[2-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]', + IPv6: typeBase.IPv6, + 'IPv6-SUBNET': typeBase.IPv6sub, + 'IPv6-GATEWAY': typeBase.IPv6, + IPv6FlowLabel: '[0-9a-fA-F]{1,5}', + IPv6ClassOfTraffic: '[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]', + 'IPv6-IGMPv3Src': typeBase.IPv6, + 'IP-IGMPv3Sr': typeBase.IP, + 'IP-ClassOfTraffic': typeBase.OSI, +}; + +/** Whether `P` element is required within `Address` */ export const typeNullable: Partial> = { + IP: false, + 'IP-SUBNET': false, + 'IP-GATEWAY': false, + 'OSI-TSEL': false, + 'OSI-SSEL': false, + 'OSI-PSEL': false, + 'OSI-AP-Title': true, + 'OSI-AP-Invoke': true, + 'OSI-AE-Qualifier': true, + 'OSI-AE-Invoke': true, + 'OSI-NSAP': true, 'MAC-Address': false, APPID: false, 'VLAN-ID': true, 'VLAN-PRIORITY': true, + 'SNTP-Port': true, + 'MMS-Port': true, + DNSName: true, + 'UDP-Port': true, + 'TCP-Port': true, + 'C37-118-IP-Port': true, + IPv6: true, + 'IPv6-SUBNET': true, + 'IPv6-GATEWAY': true, + IPv6FlowLabel: true, + IPv6ClassOfTraffic: true, + 'IPv6-IGMPv3Src': true, + 'IP-IGMPv3Sr': true, + 'IP-ClassOfTraffic': true, }; -export const typePattern: Partial> = { - 'MAC-Address': '([0-9A-F]{2}-){5}[0-9A-F]{2}', - APPID: '[0-9A-F]{4}', - 'VLAN-ID': '[0-9A-F]{3}', - 'VLAN-PRIORITY': '[0-7]', +/** Max length definition for all `P` element */ +export const typeMaxLength: Partial> = { + 'OSI-TSEL': 8, + 'OSI-SSEL': 16, + 'OSI-PSEL': 16, + 'OSI-AP-Invoke': 5, + 'OSI-AE-Qualifier': 5, + 'OSI-AE-Invoke': 5, + 'OSI-NSAP': 40, + 'IP-ClassOfTraffic': 2, }; diff --git a/test/foundation.ts b/test/foundation.ts index dfcf92999f..bce08975a2 100644 --- a/test/foundation.ts +++ b/test/foundation.ts @@ -3,6 +3,16 @@ import fc, { Arbitrary, array, hexaString, integer, tuple } from 'fast-check'; import { patterns } from '../src/foundation.js'; +export function invertedRegex( + re: RegExp, + minLength = 0, + maxLength?: number +): Arbitrary { + return fc + .string({ minLength, maxLength: maxLength ?? 2 * minLength + 10 }) + .filter(char => !re.test(char)); +} + export function regexString( re: RegExp, minLength = 0, @@ -36,6 +46,7 @@ export const regExp = { tName: new RegExp(`^${patterns.normalizedString}$`), desc: new RegExp(`^${patterns.normalizedString}$`), IPv4: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/, + IPv6: /^([0-9A-F]{2}-){5}[0-9A-F]{2}$/, OSI: /^[0-9A-F]+$/, OSIAPi: /^[0-9\u002C]+$/, OSIid: /^[0-9]+$/, diff --git a/test/integration/editors/communication/__snapshots__/connectedap-editor-wizarding.test.snap.js b/test/integration/editors/communication/__snapshots__/connectedap-editor-wizarding.test.snap.js deleted file mode 100644 index 4dd617a620..0000000000 --- a/test/integration/editors/communication/__snapshots__/connectedap-editor-wizarding.test.snap.js +++ /dev/null @@ -1,489 +0,0 @@ -/* @web/test-runner snapshot v1 */ -export const snapshots = {}; - -snapshots["conductingap-editor wizarding integration for schema 2003 (Edition1) projects looks like the latest snapshot"] = -` -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
-`; -/* end snapshot conductingap-editor wizarding integration for schema 2003 (Edition1) projects looks like the latest snapshot */ - -snapshots["conductingap-editor wizarding integration for schema 2007B (Edition2) projects looks like the latest snapshot"] = -` -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
-`; -/* end snapshot conductingap-editor wizarding integration for schema 2007B (Edition2) projects looks like the latest snapshot */ - -snapshots["conductingap-editor wizarding integration for schema 2007B4 (Edition2.1) projects looks like the latest snapshot"] = -` -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
-`; -/* end snapshot conductingap-editor wizarding integration for schema 2007B4 (Edition2.1) projects looks like the latest snapshot */ - diff --git a/test/integration/editors/communication/__snapshots__/subnetwork-editor-wizarding.test.snap.js b/test/integration/editors/communication/__snapshots__/subnetwork-editor-wizarding.test.snap.js index a07b8baed4..d9a7bc7275 100644 --- a/test/integration/editors/communication/__snapshots__/subnetwork-editor-wizarding.test.snap.js +++ b/test/integration/editors/communication/__snapshots__/subnetwork-editor-wizarding.test.snap.js @@ -59,98 +59,3 @@ snapshots["subnetwork-editor wizarding integration edit/add Subnetwork wizard lo `; /* end snapshot subnetwork-editor wizarding integration edit/add Subnetwork wizard looks like the latest snapshot */ -snapshots["subnetwork-editor wizarding integration add ConnectedAP wizard looks like the latest snapshot"] = -` -
- - - - P2 - - - IED3 - - - - - P1 - - - IED1 - - - - - P1 - - - IED2 - - - - - P1 - - - IED3 - - - -
- - - - -
-`; -/* end snapshot subnetwork-editor wizarding integration add ConnectedAP wizard looks like the latest snapshot */ - diff --git a/test/integration/editors/communication/connectedap-editor-wizarding-editing.test.ts b/test/integration/editors/communication/connectedap-editor-wizarding-editing.test.ts index 92e486c48d..6602e8bc40 100644 --- a/test/integration/editors/communication/connectedap-editor-wizarding-editing.test.ts +++ b/test/integration/editors/communication/connectedap-editor-wizarding-editing.test.ts @@ -51,12 +51,14 @@ describe('connectedap-editor wizarding editing integration', () => { ) ); }); + it('closes on secondary action', async () => { expect(parent.wizardUI.dialog).to.exist; secondaryAction.click(); await new Promise(resolve => setTimeout(resolve, 100)); // await animation expect(parent.wizardUI.dialog).to.not.exist; }); + it('changes name attribute on primary action', async () => { expect( doc.querySelector('ConnectedAP > Address > P[type="IP"]')?.textContent @@ -69,6 +71,7 @@ describe('connectedap-editor wizarding editing integration', () => { doc.querySelector('ConnectedAP > Address > P[type="IP"]')?.textContent ).to.equal('192.168.210.116'); }); + it('does not change Address if no changes have been made', async () => { const reference = doc.querySelector('ConnectedAP'); primaryAction.click(); @@ -76,6 +79,7 @@ describe('connectedap-editor wizarding editing integration', () => { .true; }); }); + describe('remove action', () => { let doc: XMLDocument; let parent: MockWizardEditor; @@ -103,6 +107,7 @@ describe('connectedap-editor wizarding editing integration', () => { element?.shadowRoot?.querySelector('mwc-fab[icon="delete"]') ); }); + it('removes ConnectedAP on delete button click', async () => { expect(doc.querySelector('SubNetwork[name="StationBus"] > ConnectedAP')) .to.exist; diff --git a/test/integration/editors/communication/connectedap-editor-wizarding.test.ts b/test/integration/editors/communication/connectedap-editor-wizarding.test.ts deleted file mode 100644 index c448684080..0000000000 --- a/test/integration/editors/communication/connectedap-editor-wizarding.test.ts +++ /dev/null @@ -1,506 +0,0 @@ -import { fixture, html, expect } from '@open-wc/testing'; -import fc, { integer, ipV4, nat } from 'fast-check'; - -import '../../../mock-wizard.js'; -import { MockWizard } from '../../../mock-wizard.js'; - -import '../../../../src/editors/communication/connectedap-editor.js'; -import { regexString, regExp, ipV6, ipV6SubNet } from '../../../foundation.js'; - -describe('conductingap-editor wizarding integration', () => { - describe('for schema 2003 (Edition1) projects', () => { - let doc: XMLDocument; - let parent: MockWizard; - - beforeEach(async () => { - doc = await fetch('/test/testfiles/valid2003.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - parent = ( - await fixture( - html` ConnectedAP' - )} - >` - ) - ); - - (( - parent - ?.querySelector('connectedap-editor') - ?.shadowRoot?.querySelector('mwc-fab[icon="edit"]') - )).click(); - await parent.updateComplete; - }); - it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); - }); - describe('the 1st input element', () => { - it('edits the attribute IP', async () => { - expect(parent.wizardUI.inputs[0].label).to.equal('IP'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(ipV4(), async name => { - parent.wizardUI.inputs[0].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[0].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 2nd input element', () => { - it('edits the attribute IP-SUBNET', async () => { - expect(parent.wizardUI.inputs[1].label).to.equal('IP-SUBNET'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(ipV4(), async name => { - parent.wizardUI.inputs[1].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[1].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 3rd input element', () => { - it('edits the attribute IP-GATEWAY', async () => { - expect(parent.wizardUI.inputs[2].label).to.equal('IP-GATEWAY'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(ipV4(), async name => { - parent.wizardUI.inputs[2].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[2].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 4th input element', () => { - it('edits the attribute OSI-TSEL', async () => { - expect(parent.wizardUI.inputs[3].label).to.equal('OSI-TSEL'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSI, 1, 8), async name => { - parent.wizardUI.inputs[3].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[3].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 5th input element', () => { - it('edits the attribute OSI-SSEL', async () => { - expect(parent.wizardUI.inputs[4].label).to.equal('OSI-SSEL'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSI, 1, 16), async name => { - parent.wizardUI.inputs[4].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[4].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 6th input element', () => { - it('edits the attribute OSI-PSEL', async () => { - expect(parent.wizardUI.inputs[5].label).to.equal('OSI-PSEL'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSI, 1, 8), async name => { - parent.wizardUI.inputs[5].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[5].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 7th input element', () => { - it('edits the attribute OSI-AP-Title', async () => { - expect(parent.wizardUI.inputs[6].label).to.equal('OSI-AP-Title'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSIAPi, 1), async name => { - parent.wizardUI.inputs[6].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[6].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 8th input element', () => { - it('edits the attribute OSI-AP-Invoke', async () => { - expect(parent.wizardUI.inputs[7].label).to.equal('OSI-AP-Invoke'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSIid, 1, 5), async name => { - parent.wizardUI.inputs[7].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[7].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 9th input element', () => { - it('edits the attribute OSI-AE-Qualifier', async () => { - expect(parent.wizardUI.inputs[8].label).to.equal('OSI-AE-Qualifier'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSIid, 1, 5), async name => { - parent.wizardUI.inputs[8].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[8].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 10th input element', () => { - it('edits the attribute OSI-AE-Invoke', async () => { - expect(parent.wizardUI.inputs[9].label).to.equal('OSI-AE-Invoke'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSIid, 1, 5), async name => { - parent.wizardUI.inputs[9].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[9].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 11th input element', () => { - it('edits the attribute OSI-NSAP', async () => { - expect(parent.wizardUI.inputs[10].label).to.equal('OSI-NSAP'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSI, 1, 40), async name => { - parent.wizardUI.inputs[10].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[10].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 12th input element', () => { - it('edits the attribute VLAN-ID', async () => { - expect(parent.wizardUI.inputs[11].label).to.equal('VLAN-ID'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSI, 3, 3), async name => { - parent.wizardUI.inputs[11].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[11].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 13th input element', () => { - it('edits the attribute VLAN-PRIORITY', async () => { - expect(parent.wizardUI.inputs[12].label).to.equal('VLAN-PRIORITY'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty( - nat({ max: 7 }).map(nap => `${nap}`), - async name => { - parent.wizardUI.inputs[12].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[12].checkValidity()).to.be.true; - } - ) - ); - }); - }); - }); - describe('for schema 2007B (Edition2) projects', () => { - let doc: XMLDocument; - let parent: MockWizard; - - beforeEach(async () => { - doc = await fetch('/test/testfiles/valid2007B.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - parent = ( - await fixture( - html` ConnectedAP' - )} - >` - ) - ); - - (( - parent - ?.querySelector('connectedap-editor') - ?.shadowRoot?.querySelector('mwc-fab[icon="edit"]') - )).click(); - await parent.updateComplete; - }); - it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); - }); - describe('the 14th input element', () => { - it('edits the attribute SNTP-Port', async () => { - expect(parent.wizardUI.inputs[13].label).to.equal('SNTP-Port'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty( - nat({ max: 65535 }).map(num => `${num}`), - async name => { - parent.wizardUI.inputs[13].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[13].checkValidity()).to.be.true; - } - ) - ); - }); - }); - describe('the 15th input element', () => { - it('edits the attribute MMS-Port', async () => { - expect(parent.wizardUI.inputs[14].label).to.equal('MMS-Port'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty( - nat({ max: 65535 }).map(num => `${num}`), - async name => { - parent.wizardUI.inputs[14].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[14].checkValidity()).to.be.true; - } - ) - ); - }); - }); - describe('the 16th input element', () => { - it('edits the attribute DNSName', async () => { - expect(parent.wizardUI.inputs[15].label).to.equal('DNSName'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(/^\S*$/), async name => { - parent.wizardUI.inputs[15].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[15].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 17th input element', () => { - it('edits the attribute UDP-Port', async () => { - expect(parent.wizardUI.inputs[16].label).to.equal('UDP-Port'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty( - nat({ max: 65535 }).map(num => `${num}`), - async name => { - parent.wizardUI.inputs[16].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[16].checkValidity()).to.be.true; - } - ) - ); - }); - }); - describe('the 18th input element', () => { - it('edits the attribute TCP-Port', async () => { - expect(parent.wizardUI.inputs[17].label).to.equal('TCP-Port'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty( - nat({ max: 65535 }).map(num => `${num}`), - async name => { - parent.wizardUI.inputs[17].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[17].checkValidity()).to.be.true; - } - ) - ); - }); - }); - describe('the 19th input element', () => { - it('edits the attribute C37-118-IP-Port', async () => { - expect(parent.wizardUI.inputs[18].label).to.equal('C37-118-IP-Port'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty( - integer({ min: 1025, max: 65535 }).map(num => `${num}`), - async name => { - parent.wizardUI.inputs[18].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[18].checkValidity()).to.be.true; - } - ) - ); - }); - }); - }); - describe('for schema 2007B4 (Edition2.1) projects', () => { - let doc: XMLDocument; - let parent: MockWizard; - - beforeEach(async () => { - doc = await fetch('/test/testfiles/valid2007B4.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - parent = ( - await fixture( - html` ConnectedAP' - )} - >` - ) - ); - - (( - parent - ?.querySelector('connectedap-editor') - ?.shadowRoot?.querySelector('mwc-fab[icon="edit"]') - )).click(); - await parent.updateComplete; - }); - it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); - }); - describe('the 20th input element', () => { - it('edits the attribute IPv6', async () => { - expect(parent.wizardUI.inputs[19].label).to.equal('IPv6'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(ipV6(), async name => { - parent.wizardUI.inputs[19].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[19].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 21th input element', () => { - it('edits the attribute IPv6-SUBNET', async () => { - expect(parent.wizardUI.inputs[20].label).to.equal('IPv6-SUBNET'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(ipV6SubNet(), async name => { - parent.wizardUI.inputs[20].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[20].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 22nd input element', () => { - it('edits the attribute IPv6-GATEWAY', async () => { - expect(parent.wizardUI.inputs[21].label).to.equal('IPv6-GATEWAY'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(ipV6(), async name => { - parent.wizardUI.inputs[21].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[21].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 23rd input element', () => { - it('edits the attribute IPv6FlowLabel', async () => { - expect(parent.wizardUI.inputs[22].label).to.equal('IPv6FlowLabel'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty( - fc.hexaString({ minLength: 1, maxLength: 5 }), - async name => { - parent.wizardUI.inputs[22].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[22].checkValidity()).to.be.true; - } - ) - ); - }); - }); - describe('the 24th input element', () => { - it('edits the attribute IPv6ClassOfTraffic', async () => { - expect(parent.wizardUI.inputs[23].label).to.equal('IPv6ClassOfTraffic'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty( - nat({ max: 255 }).map(num => `${num}`), - async name => { - parent.wizardUI.inputs[23].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[23].checkValidity()).to.be.true; - } - ) - ); - }); - }); - describe('the 25th input element', () => { - it('edits the attribute IPv6-IGMPv3Src', async () => { - expect(parent.wizardUI.inputs[24].label).to.equal('IPv6-IGMPv3Src'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(ipV6(), async name => { - parent.wizardUI.inputs[24].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[24].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 26th input element', () => { - it('edits the attribute IP-IGMPv3Sr', async () => { - expect(parent.wizardUI.inputs[25].label).to.equal('IP-IGMPv3Sr'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(ipV4(), async name => { - parent.wizardUI.inputs[25].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[25].checkValidity()).to.be.true; - }) - ); - }); - }); - describe('the 27th input element', () => { - it('edits the attribute IP-ClassOfTraffic', async () => { - expect(parent.wizardUI.inputs[26].label).to.equal('IP-ClassOfTraffic'); - }); - it('edits only for valid inputs', async () => { - await fc.assert( - fc.asyncProperty(regexString(regExp.OSI, 1, 2), async name => { - parent.wizardUI.inputs[26].value = name; - await parent.updateComplete; - expect(parent.wizardUI.inputs[26].checkValidity()).to.be.true; - }) - ); - }); - }); - }); -}); diff --git a/test/integration/editors/communication/subnetwork-editor-wizarding.test.ts b/test/integration/editors/communication/subnetwork-editor-wizarding.test.ts index 276602c442..5c070ba2e1 100644 --- a/test/integration/editors/communication/subnetwork-editor-wizarding.test.ts +++ b/test/integration/editors/communication/subnetwork-editor-wizarding.test.ts @@ -107,56 +107,4 @@ describe('subnetwork-editor wizarding integration', () => { }); }); }); - describe('add ConnectedAP wizard', () => { - let doc: XMLDocument; - let parent: MockWizard; - - beforeEach(async () => { - doc = await fetch('/test/testfiles/valid2007B4.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - parent = ( - await fixture( - html`` - ) - ); - - (( - parent - ?.querySelector('subnetwork-editor') - ?.shadowRoot?.querySelector('mwc-icon-button[icon="playlist_add"]') - )).click(); - await parent.updateComplete; - }); - it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); - }); - it('display all access point in the project', async () => { - expect(parent.wizardUI.dialog).to.exist; - expect(parent.wizardUI.dialogs.length).to.equal(1); - expect( - parent.wizardUI.dialog?.querySelectorAll('mwc-check-list-item').length - ).to.equal(doc.querySelectorAll(':root > IED > AccessPoint').length); - }); - it('only allows to select non-connected access points', async () => { - expect( - Array.from( - parent.wizardUI.dialog!.querySelectorAll('mwc-check-list-item') - ).filter(item => item.disabled).length - ).to.equal(3); - }); - it('sorts non-conneted access points to the top', async () => { - expect( - (( - parent.wizardUI.dialog!.querySelector( - 'mwc-check-list-item:nth-child(1)' - ) - )).value - ).to.equal('{"iedName":"IED3","apName":"P2"}'); - }); - }); }); diff --git a/test/unit/editors/communication/ConnectedAPEditor.test.ts b/test/unit/editors/communication/ConnectedAPEditor.test.ts deleted file mode 100644 index 0dc18a457f..0000000000 --- a/test/unit/editors/communication/ConnectedAPEditor.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { fixture, html, expect } from '@open-wc/testing'; - -import '../../../../src/wizard-textfield.js'; -import { editConnectedApAction } from '../../../../src/editors/communication/connectedap-editor.js'; -import { - WizardInput, - isCreate, - isDelete, - isSimple, - ComplexAction, -} from '../../../../src/foundation.js'; - -describe('ConnectedAPEditor', () => { - describe('has a editorAction that', () => { - describe('with existing Address element', () => { - const noOp = () => { - return; - }; - const newWizard = (done = noOp) => { - const element = document.createElement('mwc-dialog'); - element.close = done; - return element; - }; - - let inputs: WizardInput[]; - beforeEach(async () => { - inputs = await Promise.all( - ['IP', 'IP-SUBNET', 'IP-GATEWAY'].map( - label => - >( - fixture( - html`` - ) - ) - ) - ); - }); - - let scl: Element; - let element: Element; - beforeEach(async () => { - scl = new DOMParser().parseFromString( - ` - - - -
-

192.168.210.111

-

255.255.255.0

-

192.168.210.1

-

1,3,9999,23

-

23

-

00000001

-

0001

-

0001

-
-
-
-
-
- `, - 'application/xml' - ).documentElement; - - element = scl.querySelector('ConnectedAP')!; - }); - - it('returns a WizardAction with the returned EditorAction beeing a ComplexAction', () => { - const wizardAction = editConnectedApAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(1); - expect(wizardAction(inputs, newWizard())[0]).to.not.satisfy(isSimple); - }); - - it('the complexAction containing two EditorActions', () => { - const wizardAction = editConnectedApAction(element); - expect( - (wizardAction(inputs, newWizard())[0]).actions.length - ).to.equal(2); - }); - - it('the 1st beeing a Delete', () => { - const wizardAction = editConnectedApAction(element); - expect( - (wizardAction(inputs, newWizard())[0]).actions[0] - ).to.satisfy(isDelete); - }); - - it('the 2nd beeing a Create', () => { - const wizardAction = editConnectedApAction(element); - expect( - (wizardAction(inputs, newWizard())[0]).actions[1] - ).to.satisfy(isCreate); - }); - }); - describe('with missing Address element', () => { - const noOp = () => { - return; - }; - const newWizard = (done = noOp) => { - const element = document.createElement('mwc-dialog'); - element.close = done; - return element; - }; - - let inputs: WizardInput[]; - beforeEach(async () => { - inputs = await Promise.all( - ['IP', 'IP-SUBNET', 'IP-GATEWAY'].map( - label => - >( - fixture( - html`` - ) - ) - ) - ); - }); - - let scl: Element; - let element: Element; - beforeEach(async () => { - scl = new DOMParser().parseFromString( - ` - - - - - - - - `, - 'application/xml' - ).documentElement; - - element = scl.querySelector('ConnectedAP')!; - }); - - it('returns a WizardAction with the returned EditorAction beeing a ComplexAction', () => { - const wizardAction = editConnectedApAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(1); - expect(wizardAction(inputs, newWizard())[0]).to.not.satisfy(isSimple); - }); - - it('the complexAction containing one EditorAction', () => { - const wizardAction = editConnectedApAction(element); - expect( - (wizardAction(inputs, newWizard())[0]).actions.length - ).to.equal(1); - }); - - it('beeing an Delete', () => { - const wizardAction = editConnectedApAction(element); - expect( - (wizardAction(inputs, newWizard())[0]).actions[0] - ).to.satisfy(isCreate); - }); - }); - }); -}); diff --git a/test/unit/wizards/__snapshots__/connectedap-pattern.test.snap.js b/test/unit/wizards/__snapshots__/connectedap-pattern.test.snap.js new file mode 100644 index 0000000000..b43cea84d5 --- /dev/null +++ b/test/unit/wizards/__snapshots__/connectedap-pattern.test.snap.js @@ -0,0 +1,970 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Edit wizard for SCL element ConnectedAP for Edition 1 projects looks like the latest snapshot"] = +` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+`; +/* end snapshot Edit wizard for SCL element ConnectedAP for Edition 1 projects looks like the latest snapshot */ + +snapshots["Edit wizard for SCL element ConnectedAP for Edition 2 projects looks like the latest snapshot"] = +` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+`; +/* end snapshot Edit wizard for SCL element ConnectedAP for Edition 2 projects looks like the latest snapshot */ + +snapshots["Edit wizard for SCL element ConnectedAP for Edition 2.1 projects looks like the latest snapshot"] = +` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+`; +/* end snapshot Edit wizard for SCL element ConnectedAP for Edition 2.1 projects looks like the latest snapshot */ + +snapshots["Edit wizard for SCL element ConnectedAP include an edit wizard that for Edition 1 projects looks like the latest snapshot"] = +` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+`; +/* end snapshot Edit wizard for SCL element ConnectedAP include an edit wizard that for Edition 1 projects looks like the latest snapshot */ + +snapshots["Edit wizard for SCL element ConnectedAP include an edit wizard that for Edition 2 projects looks like the latest snapshot"] = +` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+`; +/* end snapshot Edit wizard for SCL element ConnectedAP include an edit wizard that for Edition 2 projects looks like the latest snapshot */ + +snapshots["Edit wizard for SCL element ConnectedAP include an edit wizard that for Edition 2.1 projects looks like the latest snapshot"] = +` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+`; +/* end snapshot Edit wizard for SCL element ConnectedAP include an edit wizard that for Edition 2.1 projects 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 new file mode 100644 index 0000000000..272fc5804e --- /dev/null +++ b/test/unit/wizards/__snapshots__/connectedap.test.snap.js @@ -0,0 +1,82 @@ +/* @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-pattern.test.ts b/test/unit/wizards/connectedap-pattern.test.ts new file mode 100644 index 0000000000..068a0a0741 --- /dev/null +++ b/test/unit/wizards/connectedap-pattern.test.ts @@ -0,0 +1,872 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import fc, { integer, ipV4, nat } from 'fast-check'; + +import '../../mock-wizard.js'; +import { MockWizard } from '../../mock-wizard.js'; + +import '../../../src/editors/communication/connectedap-editor.js'; +import { WizardInput } from '../../../src/foundation.js'; + +import { + ipV6, + ipV6SubNet, + invertedRegex, + regExp, + regexString, +} from '../../foundation.js'; +import { WizardTextField } from '../../../src/wizard-textfield.js'; +import { editConnectedApWizard } from '../../../src/wizards/connectedap.js'; + +describe('Edit wizard for SCL element ConnectedAP', () => { + let doc: XMLDocument; + let element: MockWizard; + let inputs: WizardInput[]; + let input: WizardInput | undefined; + + beforeEach(async () => { + element = await fixture(html``); + }); + + describe('include an edit wizard that', () => { + describe('for Edition 1 projects', () => { + beforeEach(async () => { + doc = await fetch('/test/testfiles/valid2003.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + const wizard = editConnectedApWizard(doc.querySelector('ConnectedAP')!); + 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(); + }); + + describe('contains an input to edit P element of type IP', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'IP'); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(ipV4(), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.IPv4), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type IP-SUBNET', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'IP-SUBNET'); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(ipV4(), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.IPv4), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type IP-GATEWAY', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'IP-GATEWAY'); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(ipV4(), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.IPv4), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type OSI-TSEL', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'OSI-TSEL'); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(regexString(regExp.OSI, 1, 8), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSI), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type OSI-SSEL', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'OSI-SSEL'); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + regexString(regExp.OSI, 1, 16), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSI), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type OSI-PSEL', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'OSI-PSEL'); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + regexString(regExp.OSI, 1, 16), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSI), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type OSI-AP-Title', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'OSI-AP-Title'); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(regexString(regExp.OSIAPi, 1), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSIAPi), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type OSI-AP-Invoke', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'OSI-AP-Invoke'); + if (input && (input).maybeValue === null) + await (input).nullSwitch?.click(); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + regexString(regExp.OSIid, 1, 5), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSIid), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type OSI-AE-Qualifier', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'OSI-AE-Qualifier'); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + regexString(regExp.OSIid, 1, 5), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSIid), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type OSI-AE-Invoke', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'OSI-AE-Invoke'); + if (input && (input).maybeValue === null) + await (input).nullSwitch?.click(); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + regexString(regExp.OSIid, 1, 5), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSIid), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type OSI-NSAP', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'OSI-NSAP'); + if (input && (input).maybeValue === null) + await (input).nullSwitch?.click(); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + regexString(regExp.OSI, 1, 40), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSI), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type VLAN-ID', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'VLAN-ID'); + if (input && (input).maybeValue === null) + await (input).nullSwitch?.click(); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(regexString(regExp.OSI, 3, 3), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSI), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type VLAN-PRIORITY', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'VLAN-PRIORITY'); + if (input && (input).maybeValue === null) + await (input).nullSwitch?.click(); + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(regexString(/^[0-7]$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(/^[0-7]$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + }); + + describe('for Edition 2 projects', () => { + beforeEach(async () => { + doc = await fetch('/test/testfiles/valid2007B.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + const wizard = editConnectedApWizard(doc.querySelector('ConnectedAP')!); + 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(); + }); + + describe('contains an input to edit P element of type SNTP-Port', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'SNTP-Port'); + if (input && (input).maybeValue === null) { + await (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + nat({ max: 65535 }).map(num => `${num}`), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(/^[0-9]*$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type MMS-Port', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'MMS-Port'); + if (input && (input).maybeValue === null) { + await (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + nat({ max: 65535 }).map(num => `${num}`), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(/^[0-9]*$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type DNSName', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'DNSName'); + if (input && (input).maybeValue === null) { + await (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(regexString(/^\S*$/, 1), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + }); + + describe('contains an input to edit P element of type UDP-Port', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'UDP-Port'); + if (input && (input).maybeValue === null) { + (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + nat({ max: 65535 }).map(num => `${num}`), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(/^[0-9]*$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type TCP-Port', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'TCP-Port'); + if (input && (input).maybeValue === null) { + (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + nat({ max: 65535 }).map(num => `${num}`), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(/^[0-9]*$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type C37-118-IP-Port', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'C37-118-IP-Port'); + if (input && (input).maybeValue === null) { + (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + integer({ min: 1025, max: 65535 }).map(num => `${num}`), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(/^[0-9]*$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + }); + + describe('for Edition 2.1 projects', () => { + beforeEach(async () => { + doc = await fetch('/test/testfiles/valid2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + const wizard = editConnectedApWizard(doc.querySelector('ConnectedAP')!); + 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(); + }); + + describe('contains an input to edit P element of type IPv6', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'IPv6'); + if (input && (input).maybeValue === null) { + await (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(ipV6(), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.IPv6), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type IPv6-SUBNET', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'IPv6-SUBNET'); + if (input && (input).maybeValue === null) { + await (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(ipV6SubNet(), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(/^[0-9/]*$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type IPv6-GATEWAY', () => { + beforeEach(async () => { + input = inputs.find(input => input.label === 'IPv6-GATEWAY'); + if (input && (input).maybeValue === null) { + await (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(ipV6(), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.IPv6), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type IPv6FlowLabel', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'IPv6FlowLabel'); + if (input && (input).maybeValue === null) { + (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + fc.hexaString({ minLength: 1, maxLength: 5 }), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty( + invertedRegex(/^[0-9a-fA-F]{1,5}$/), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + } + ) + )); + }); + + describe('contains an input to edit P element of type IPv6ClassOfTraffic', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'IPv6ClassOfTraffic'); + if (input && (input).maybeValue === null) { + (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty( + nat({ max: 255 }).map(num => `${num}`), + async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + } + ) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(/^[0-9]*$/), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type IPv6-IGMPv3Src', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'IPv6-IGMPv3Src'); + if (input && (input).maybeValue === null) { + (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(ipV6(), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.IPv6), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type IP-IGMPv3Sr', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'IP-IGMPv3Sr'); + if (input && (input).maybeValue === null) { + (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(ipV4(), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.IPv4), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + + describe('contains an input to edit P element of type IP-ClassOfTraffic', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'IP-ClassOfTraffic'); + if (input && (input).maybeValue === null) { + (input).nullSwitch?.click(); + } + }); + + it('is always rendered', () => expect(input).to.exist); + + it('allow to edit for valid input', async () => + await fc.assert( + fc.asyncProperty(regexString(regExp.OSI, 1, 2), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.true; + }) + )); + + it('does not allow to edit for invalid input', async () => + await fc.assert( + fc.asyncProperty(invertedRegex(regExp.OSI), async testValue => { + input!.value = testValue; + await element.requestUpdate(); + expect(input!.checkValidity()).to.be.false; + }) + )); + }); + }); + }); +}); diff --git a/test/unit/wizards/connectedap.test.ts b/test/unit/wizards/connectedap.test.ts new file mode 100644 index 0000000000..80cb37c59e --- /dev/null +++ b/test/unit/wizards/connectedap.test.ts @@ -0,0 +1,241 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../mock-wizard.js'; +import { MockWizard } from '../../mock-wizard.js'; + +import { Checkbox } from '@material/mwc-checkbox'; + +import { WizardTextField } from '../../../src/wizard-textfield.js'; +import { + ComplexAction, + Delete, + isCreate, + isDelete, + isSimple, + Create, + WizardInput, +} from '../../../src/foundation.js'; +import { + createConnectedApWizard, + editConnectedApWizard, +} from '../../../src/wizards/connectedap.js'; +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; + +describe('Wizards for SCL element ConnectedAP', () => { + let doc: XMLDocument; + let element: MockWizard; + let inputs: WizardInput[]; + let input: WizardInput | undefined; + let primaryAction: HTMLElement; + + let actionEvent: SinonSpy; + + beforeEach(async () => { + element = await fixture(html``); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('include an edit wizard that', () => { + beforeEach(async () => { + doc = await fetch('/test/testfiles/valid2003.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + const wizard = editConnectedApWizard(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('does not edit any P element with unchanged wizard inputs', async () => { + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.not.have.been.called; + }); + + it('triggers a complex editor action to update P elements(s)', async () => { + input = inputs.find(input => input.label === 'IP'); + input.value = '192.168.210.158'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.not.satisfy(isSimple); + }); + + it('triggers a complex action as combination of delete and create with prior existing Address field', async () => { + input = inputs.find(input => input.label === 'IP'); + input.value = '192.168.210.158'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + const complexAction = actionEvent.args[0][0].detail.action; + expect(complexAction.actions).to.have.lengthOf(2); + expect(complexAction.actions[0]).to.satisfy(isDelete); + expect(complexAction.actions[1]).to.satisfy(isCreate); + }); + + it('triggers a complex action being a pure create with prior missing Address field', async () => { + doc + .querySelector('ConnectedAP') + ?.removeChild(doc.querySelector('ConnectedAP > Address')!); + + input = inputs.find(input => input.label === 'IP'); + input.value = '192.168.210.158'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + const complexAction = actionEvent.args[0][0].detail.action; + expect(complexAction.actions).to.have.lengthOf(1); + expect(complexAction.actions[0]).to.satisfy(isCreate); + }); + + it('properly updates a P element of type IP', async () => { + input = inputs.find(input => input.label === 'IP'); + input.value = '192.168.210.158'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + const complexAction = actionEvent.args[0][0].detail.action; + + const oldAddress = (complexAction.actions[0]).old.element; + const newAddress = (complexAction.actions[1]).new.element; + + expect( + oldAddress.querySelector('P[type="IP"]')?.textContent + ).to.equal('192.168.210.111'); + expect( + newAddress.querySelector('P[type="IP"]')?.textContent + ).to.equal('192.168.210.158'); + }); + + it('adds type restrictions with selected option type restriction', async () => { + (( + element.wizardUI.shadowRoot?.querySelector('#typeRestriction') + )).checked = true; + await element.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + const complexAction = actionEvent.args[0][0].detail.action; + + const oldAddress = (complexAction.actions[0]).old.element; + const newAddress = (complexAction.actions[1]).new.element; + + const oldIP = oldAddress.querySelector('P[type="IP"]'); + const newIP = newAddress.querySelector('P[type="IP"]'); + + expect( + oldIP?.getAttributeNS( + 'http://www.w3.org/2001/XMLSchema-instance', + 'type' + ) + ).to.not.exist; + expect( + newIP?.getAttributeNS( + 'http://www.w3.org/2001/XMLSchema-instance', + 'type' + ) + ).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); + }); + }); +});