diff --git a/src/editors/Communication.ts b/src/editors/Communication.ts index e7b4b0b36..9ed9a81d7 100644 --- a/src/editors/Communication.ts +++ b/src/editors/Communication.ts @@ -3,14 +3,14 @@ import { translate, get } from 'lit-translate'; import '@material/mwc-fab'; +import './communication/subnetwork-editor.js'; import { newWizardEvent, newActionEvent, createElement, + isPublic, } from '../foundation.js'; -import { selectors, styles } from './communication/foundation.js'; -import './communication/subnetwork-editor.js'; -import { subNetworkWizard } from './communication/subnetwork-editor.js'; +import { createSubNetworkWizard } from '../wizards/subnetwork.js'; /** An editor [[`plugin`]] for editing the `Communication` section. */ export default class CommunicationPlugin extends LitElement { @@ -18,7 +18,7 @@ export default class CommunicationPlugin extends LitElement { @property() doc!: XMLDocument; - createCommunication(): void { + private createCommunication(): void { this.dispatchEvent( newActionEvent({ new: { @@ -30,21 +30,15 @@ export default class CommunicationPlugin extends LitElement { } /** Opens a [[`WizardDialog`]] for creating a new `SubNetwork` element. */ - openCreateSubNetworkWizard(): void { - if (!this.doc.querySelector(selectors.Communication)) - this.createCommunication(); + private openCreateSubNetworkWizard(): void { + const parent = this.doc.querySelector(':root > Communication'); + if (!parent) this.createCommunication(); - this.dispatchEvent( - newWizardEvent( - subNetworkWizard({ - parent: this.doc.querySelector('Communication')!, - }) - ) - ); + this.dispatchEvent(newWizardEvent(createSubNetworkWizard(parent!))); } render(): TemplateResult { - if (!this.doc?.querySelector(selectors.SubNetwork)) + if (!this.doc?.querySelector(':root > Communication >SubNetwork')) return html`

${translate('communication.missing')} this.openCreateSubNetworkWizard()} >

`; + return html` this.openCreateSubNetworkWizard()} - >${Array.from(this.doc.querySelectorAll(selectors.SubNetwork) ?? []).map( - subnetwork => - html`` - )}`; + > +
+ ${Array.from(this.doc.querySelectorAll('SubNetwork') ?? []) + .filter(isPublic) + .map( + subnetwork => + html`` + )} +
`; } static styles = css` - ${styles} + :host { + width: 100vw; + } + + section { + outline: none; + padding: 8px 12px 16px; + } + + subnetwork-editor { + margin: 8px 12px 16px; + } mwc-fab { position: fixed; bottom: 32px; right: 32px; } - - :host { - width: 100vw; - } `; } diff --git a/src/editors/communication/foundation.ts b/src/editors/communication/foundation.ts deleted file mode 100644 index 0006a19b6..000000000 --- a/src/editors/communication/foundation.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { css } from 'lit-element'; - -export type ElementEditor = Element & { - element: Element; -}; - -interface UpdateOptions { - element: Element; -} -interface CreateOptions { - parent: Element; -} -export type WizardOptions = UpdateOptions | CreateOptions; - -export function isCreateOptions( - options: WizardOptions -): options is CreateOptions { - return (options).parent !== undefined; -} - -// Communication element hierarchy -const substationPath = [ - ':root', - 'Communication', - 'SubNetwork', - 'ConnectedAP', - 'Address', -]; - -export type CommunicationTag = - | 'Communication' - | 'SubNetwork' - | 'ConnectedAP' - | 'Address'; - -/** `Private`-safeguarded selectors for `Communication` and its descendants */ -export const selectors = >( - Object.fromEntries( - substationPath.map((e, i, a) => [e, a.slice(0, i + 1).join(' > ')]) - ) -); - -/** Common `CSS` styles used by communication subeditors */ -export const styles = css` - :host(.moving) section { - opacity: 0.3; - } - - section { - background-color: var(--mdc-theme-surface); - transition: all 200ms linear; - outline-color: var(--mdc-theme-primary); - outline-style: solid; - outline-width: 0px; - margin: 8px 12px 16px; - opacity: 1; - } - - section:focus { - box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); - } - - section:focus-within { - outline-width: 2px; - transition: all 250ms linear; - } - - h1, - h2, - h3 { - color: var(--mdc-theme-on-surface); - font-family: 'Roboto', sans-serif; - font-weight: 300; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin: 0px; - line-height: 48px; - padding-left: 0.3em; - transition: background-color 150ms linear; - } - - section:focus-within > h1, - section:focus-within > h2, - section:focus-within > h3 { - color: var(--mdc-theme-surface); - background-color: var(--mdc-theme-primary); - transition: background-color 200ms linear; - } - - h1 > nav, - h2 > nav, - h3 > nav, - h1 > abbr > mwc-icon-button, - h2 > abbr > mwc-icon-button, - h3 > abbr > mwc-icon-button { - float: right; - } - - abbr { - text-decoration: none; - border-bottom: none; - } -`; diff --git a/src/editors/communication/subnetwork-editor.ts b/src/editors/communication/subnetwork-editor.ts index 3cb5f133a..22ed4a981 100644 --- a/src/editors/communication/subnetwork-editor.ts +++ b/src/editors/communication/subnetwork-editor.ts @@ -6,282 +6,58 @@ import { property, css, } from 'lit-element'; -import { translate, get } from 'lit-translate'; +import { translate } from 'lit-translate'; import '@material/mwc-icon-button'; -import '../../wizard-textfield.js'; import './connectedap-editor.js'; import { - EditorAction, newWizardEvent, - Wizard, - WizardActor, - WizardInput, newActionEvent, - getValue, - getMultiplier, - patterns, compareNames, - createElement, - cloneElement, } from '../../foundation.js'; -import { styles, WizardOptions, isCreateOptions } from './foundation.js'; import { createConnectedApWizard } from '../../wizards/connectedap.js'; - -/** Initial attribute values suggested for `SubNetwork` creation */ -const initial = { - type: '8-MMS', - bitrate: '100', - multiplier: 'M', -}; - -function getBitRateAction( - oldBitRate: Element | null, - BitRate: string | null, - multiplier: string | null, - SubNetwork: Element -): EditorAction { - if (oldBitRate === null) - return { - new: { - parent: SubNetwork, - element: new DOMParser().parseFromString( - `${BitRate === null ? '' : BitRate}`, - 'application/xml' - ).documentElement, - reference: SubNetwork.firstElementChild, - }, - }; - - if (BitRate === null) - return { - old: { - parent: SubNetwork, - element: oldBitRate, - reference: oldBitRate.nextSibling, - }, - }; - - const newBitRate = cloneElement(oldBitRate, { multiplier }); - newBitRate.textContent = BitRate; - - return { - old: { element: oldBitRate }, - new: { element: newBitRate }, - }; -} - -export function updateSubNetworkAction(element: Element): WizardActor { - return (inputs: WizardInput[], wizard: Element): EditorAction[] => { - const name = inputs.find(i => i.label === 'name')!.value!; - const desc = getValue(inputs.find(i => i.label === 'desc')!); - const type = getValue(inputs.find(i => i.label === 'type')!); - const BitRate = getValue(inputs.find(i => i.label === 'BitRate')!); - const multiplier = getMultiplier(inputs.find(i => i.label === 'BitRate')!); - - let subNetworkAction: EditorAction | null; - let bitRateAction: EditorAction | null; - - if ( - name === element.getAttribute('name') && - desc === element.getAttribute('desc') && - type === element.getAttribute('type') - ) { - subNetworkAction = null; - } else { - const newElement = cloneElement(element, { name, desc, type }); - subNetworkAction = { old: { element }, new: { element: newElement } }; - } - - if ( - BitRate === - (element.querySelector('SubNetwork > BitRate')?.textContent?.trim() ?? - null) && - multiplier === - (element - .querySelector('SubNetwork > BitRate') - ?.getAttribute('multiplier') ?? null) - ) { - bitRateAction = null; - } else { - bitRateAction = getBitRateAction( - element.querySelector('SubNetwork > BitRate'), - BitRate, - multiplier, - subNetworkAction?.new.element ?? element - ); - } - - const actions: EditorAction[] = []; - if (subNetworkAction) actions.push(subNetworkAction); - if (bitRateAction) actions.push(bitRateAction); - return actions; - }; -} - -export function createSubNetworkAction(parent: Element): WizardActor { - return (inputs: WizardInput[], wizard: Element): EditorAction[] => { - const name = getValue(inputs.find(i => i.label === 'name')!); - const desc = getValue(inputs.find(i => i.label === 'desc')!); - const type = getValue(inputs.find(i => i.label === 'type')!); - const BitRate = getValue(inputs.find(i => i.label === 'BitRate')!); - const multiplier = getMultiplier(inputs.find(i => i.label === 'BitRate')!); - - const element = createElement(parent.ownerDocument, 'SubNetwork', { - name, - desc, - type, - }); - - if (BitRate !== null) { - const bitRateElement = createElement(parent.ownerDocument, 'BitRate', { - unit: 'b/s', - multiplier, - }); - bitRateElement.textContent = BitRate; - element.appendChild(bitRateElement); - } - - const action = { - new: { - parent, - element, - }, - }; - - return [action]; - }; -} - -export function subNetworkWizard(options: WizardOptions): Wizard { - const [ - heading, - actionName, - actionIcon, - action, - name, - desc, - type, - BitRate, - multiplier, - element, - ] = isCreateOptions(options) - ? [ - get('subnetwork.wizard.title.add'), - get('add'), - 'add', - createSubNetworkAction(options.parent), - '', - '', - initial.type, - initial.bitrate, - initial.multiplier, - undefined, - ] - : [ - get('subnetwork.wizard.title.edit'), - get('save'), - 'edit', - updateSubNetworkAction(options.element), - options.element.getAttribute('name'), - options.element.getAttribute('desc'), - options.element.getAttribute('type'), - options.element - .querySelector('SubNetwork > BitRate') - ?.textContent?.trim() ?? null, - options.element - .querySelector('SubNetwork > BitRate') - ?.getAttribute('multiplier') ?? null, - options.element, - ]; - - return [ - { - title: heading, - element, - primary: { - icon: actionIcon, - label: actionName, - action: action, - }, - content: [ - html``, - html``, - html``, - html``, - ], - }, - ]; -} +import { wizards } from '../../wizards/wizard-library.js'; /** [[`Communication`]] subeditor for a `SubNetwork` element. */ @customElement('subnetwork-editor') export class SubNetworkEditor extends LitElement { - @property() + /** SCL element SubNetwork */ + @property({ attribute: false }) element!: Element; - + /** SubNetwork attribute name */ @property() get name(): string { - return this.element.getAttribute('name') ?? ''; + return this.element.getAttribute('name') ?? 'UNDEFINED'; } + /** SubNetwork attribute desc */ @property() get desc(): string | null { return this.element.getAttribute('desc') ?? null; } + /** SubNetwork attribute type */ @property() get type(): string | null { return this.element.getAttribute('type') ?? null; } + /** SubNetwork child elements BitRate label */ @property() get bitrate(): string | null { - const V = this.element.querySelector('BitRate'); - if (V === null) return null; - const v = V.textContent ?? ''; - const m = V.getAttribute('multiplier'); - const u = m === null ? 'b/s' : ' ' + m + 'b/s'; - return v ? v + u : null; + const bitRate = this.element.querySelector('BitRate'); + if (bitRate === null) return null; + const bitRateValue = bitRate.textContent ?? ''; + const m = bitRate.getAttribute('multiplier'); + const unit = m === null ? 'b/s' : ' ' + m + 'b/s'; + return bitRateValue ? bitRateValue + unit : null; } - openConnectedAPwizard(): void { + private openConnectedAPwizard(): void { this.dispatchEvent(newWizardEvent(createConnectedApWizard(this.element))); } - openEditWizard(): void { - this.dispatchEvent( - newWizardEvent(subNetworkWizard({ element: this.element })) - ); + private openEditWizard(): void { + const wizard = wizards['SubNetwork'].edit(this.element); + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } remove(): void { @@ -297,81 +73,73 @@ export class SubNetworkEditor extends LitElement { ); } - renderSubNetworkSpecs(): TemplateResult { - if (!this.type && !this.bitrate) return html``; - - return html`(${this.type}${this.type && this.bitrate - ? html`—` - : html``}${this.bitrate})`; - } - - renderHeader(): TemplateResult { - return html`

- ${this.name} ${this.desc === null ? '' : html`—`} ${this.desc} - ${this.renderSubNetworkSpecs()} - - - - -

`; - } - - renderIedContainer(): TemplateResult[] { - return Array.from(this.element.querySelectorAll('ConnectedAP') ?? []) + private renderIedContainer(): TemplateResult[] { + return Array.from(this.element.querySelectorAll(':scope > ConnectedAP')) .map(connAP => connAP.getAttribute('iedName')!) .filter((v, i, a) => a.indexOf(v) === i) .sort(compareNames) .map( - iedName => html`
-

${iedName}

-
- ${Array.from( - this.element.ownerDocument.querySelectorAll( - `ConnectedAP[iedName="${iedName}"]` - ) - ).map( - connectedAP => - html`` - )} -
-
` + iedName => html` + ${Array.from( + this.element.parentElement?.querySelectorAll( + `:scope > SubNetwork > ConnectedAP[iedName="${iedName}"]` + ) ?? [] + ).map( + connectedAP => + html`` + )} + ` ); } + private subNetworkSpecs(): string { + if (!this.type && !this.bitrate) return ''; + + return `(${this.type}${ + this.type && this.bitrate ? ` — ${this.bitrate}` : `` + })`; + } + + private header(): string { + return ` ${this.name} ${this.desc === null ? '' : `— ${this.desc}`} + ${this.subNetworkSpecs()}`; + } + render(): TemplateResult { - return html`
- ${this.renderHeader()} -
${this.renderIedContainer()}
-
`; + return html` + + this.openEditWizard()} + > + + + this.remove()} + > + + +
${this.renderIedContainer()}
+
`; } static styles = css` - ${styles} - - #iedSection { - background-color: var(--mdc-theme-on-primary); - margin: 0px; + #iedContainer { + display: grid; + box-sizing: border-box; + gap: 12px; + padding: 8px 12px 16px; + grid-template-columns: repeat(auto-fit, minmax(150px, auto)); } #iedSection:not(:focus):not(:focus-within) .disabled { @@ -383,19 +151,9 @@ export class SubNetworkEditor extends LitElement { opacity: 0.5; } - #connAPContainer { - display: grid; - box-sizing: border-box; - gap: 12px; - padding: 8px 12px 16px; - grid-template-columns: repeat(auto-fit, minmax(150px, auto)); - } - - #connApContainer { - display: grid; - box-sizing: border-box; - padding: 8px 12px 8px; - grid-template-columns: repeat(auto-fit, minmax(64px, auto)); + abbr { + text-decoration: none; + border-bottom: none; } `; } diff --git a/src/wizards/subnetwork.ts b/src/wizards/subnetwork.ts new file mode 100644 index 000000000..47f3f8f47 --- /dev/null +++ b/src/wizards/subnetwork.ts @@ -0,0 +1,232 @@ +import { html, TemplateResult } from 'lit-element'; +import { get, translate } from 'lit-translate'; + +import '../wizard-textfield.js'; +import { + cloneElement, + createElement, + EditorAction, + getMultiplier, + getValue, + patterns, + Wizard, + WizardActor, + WizardInput, +} from '../foundation.js'; + +/** Initial attribute values suggested for `SubNetwork` creation */ +const initial = { + type: '8-MMS', + bitrate: '100', + multiplier: 'M', +}; + +interface ContentOptions { + name: string | null; + desc: string | null; + type: string | null; + BitRate: string | null; + multiplier: string | null; +} + +function contentSubNetwork(options: ContentOptions): TemplateResult[] { + return [ + html``, + html``, + html``, + html``, + ]; +} + +export function createSubNetworkAction(parent: Element): WizardActor { + return (inputs: WizardInput[]): EditorAction[] => { + const name = getValue(inputs.find(i => i.label === 'name')!); + const desc = getValue(inputs.find(i => i.label === 'desc')!); + const type = getValue(inputs.find(i => i.label === 'type')!); + const BitRate = getValue(inputs.find(i => i.label === 'BitRate')!); + const multiplier = getMultiplier(inputs.find(i => i.label === 'BitRate')!); + + const element = createElement(parent.ownerDocument, 'SubNetwork', { + name, + desc, + type, + }); + + if (BitRate !== null) { + const bitRateElement = createElement(parent.ownerDocument, 'BitRate', { + unit: 'b/s', + multiplier, + }); + bitRateElement.textContent = BitRate; + element.appendChild(bitRateElement); + } + + const action = { + new: { + parent, + element, + }, + }; + + return [action]; + }; +} + +export function createSubNetworkWizard(parent: Element): Wizard { + return [ + { + title: get('wizard.title.create', { tagName: 'SubNetwork' }), + primary: { + icon: 'add', + label: get('add'), + action: createSubNetworkAction(parent), + }, + content: contentSubNetwork({ + name: '', + desc: '', + type: initial.type, + BitRate: initial.bitrate, + multiplier: initial.multiplier, + }), + }, + ]; +} + +function getBitRateAction( + oldBitRate: Element | null, + BitRate: string | null, + multiplier: string | null, + SubNetwork: Element +): EditorAction { + if (oldBitRate === null) + return { + new: { + parent: SubNetwork, + element: new DOMParser().parseFromString( + `${BitRate === null ? '' : BitRate}`, + 'application/xml' + ).documentElement, + reference: SubNetwork.firstElementChild, + }, + }; + + if (BitRate === null) + return { + old: { + parent: SubNetwork, + element: oldBitRate, + reference: oldBitRate.nextSibling, + }, + }; + + const newBitRate = cloneElement(oldBitRate, { multiplier }); + newBitRate.textContent = BitRate; + + return { + old: { element: oldBitRate }, + new: { element: newBitRate }, + }; +} + +function updateSubNetworkAction(element: Element): WizardActor { + return (inputs: WizardInput[]): EditorAction[] => { + const name = inputs.find(i => i.label === 'name')!.value!; + const desc = getValue(inputs.find(i => i.label === 'desc')!); + const type = getValue(inputs.find(i => i.label === 'type')!); + const BitRate = getValue(inputs.find(i => i.label === 'BitRate')!); + const multiplier = getMultiplier(inputs.find(i => i.label === 'BitRate')!); + + let subNetworkAction: EditorAction | null; + let bitRateAction: EditorAction | null; + + if ( + name === element.getAttribute('name') && + desc === element.getAttribute('desc') && + type === element.getAttribute('type') + ) { + subNetworkAction = null; + } else { + const newElement = cloneElement(element, { name, desc, type }); + subNetworkAction = { old: { element }, new: { element: newElement } }; + } + + if ( + BitRate === + (element.querySelector('SubNetwork > BitRate')?.textContent?.trim() ?? + null) && + multiplier === + (element + .querySelector('SubNetwork > BitRate') + ?.getAttribute('multiplier') ?? null) + ) { + bitRateAction = null; + } else { + bitRateAction = getBitRateAction( + element.querySelector('SubNetwork > BitRate'), + BitRate, + multiplier, + subNetworkAction?.new.element ?? element + ); + } + + const actions: EditorAction[] = []; + if (subNetworkAction) actions.push(subNetworkAction); + if (bitRateAction) actions.push(bitRateAction); + return actions; + }; +} + +export function editSubNetworkWizard(element: Element): Wizard { + const name = element.getAttribute('name'); + const desc = element.getAttribute('desc'); + const type = element.getAttribute('type'); + const BitRate = + element.querySelector('SubNetwork > BitRate')?.textContent?.trim() ?? null; + const multiplier = + element.querySelector('SubNetwork > BitRate')?.getAttribute('multiplier') ?? + null; + + return [ + { + title: get('wizard.title.edit', { tagName: element.tagName }), + element, + primary: { + icon: 'save', + label: get('save'), + action: updateSubNetworkAction(element), + }, + content: contentSubNetwork({ name, desc, type, BitRate, multiplier }), + }, + ]; +} diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index 99de9ec0e..d9a609954 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -16,6 +16,7 @@ import { voltageLevelEditWizard, } from './voltagelevel.js'; import { editPowerTransformerWizard } from './powertransformer.js'; +import { editSubNetworkWizard } from './subnetwork.js'; import { editIEDWizard } from './ied.js'; import { editTrgOpsWizard } from './trgops.js'; @@ -469,7 +470,7 @@ export const wizards: Record< create: emptyWizard, }, SubNetwork: { - edit: emptyWizard, + edit: editSubNetworkWizard, create: emptyWizard, }, Subject: { 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 d9a7bc727..0c1f2978c 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 @@ -4,7 +4,7 @@ export const snapshots = {}; snapshots["subnetwork-editor wizarding integration edit/add Subnetwork wizard looks like the latest snapshot"] = `
@@ -49,7 +49,7 @@ snapshots["subnetwork-editor wizarding integration edit/add Subnetwork wizard lo + +
+ TrainingIEC61850 + + + +
+ + + 100.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/test/testfiles/valid2003.scd b/test/testfiles/valid2003.scd index bd2b924c1..61e270ce9 100644 --- a/test/testfiles/valid2003.scd +++ b/test/testfiles/valid2003.scd @@ -36,7 +36,7 @@ - + 100.0
diff --git a/test/unit/editors/communication/SubNetwork.test.ts b/test/unit/editors/communication/SubNetwork.test.ts deleted file mode 100644 index d4150677c..000000000 --- a/test/unit/editors/communication/SubNetwork.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { fixture, html, expect } from '@open-wc/testing'; - -import '../../../../src/wizard-textfield.js'; -import { - createSubNetworkAction, - updateSubNetworkAction, -} from '../../../../src/editors/communication/subnetwork-editor.js'; -import { - WizardInput, - isCreate, - isUpdate, - isDelete, -} from '../../../../src/foundation.js'; - -describe('SubNetworkEditor', () => { - describe('with no nulled properties', () => { - 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( - ['name', 'desc', 'type', 'BitRate'].map( - label => - >( - fixture( - html`` - ) - ) - ) - ); - }); - - describe('has a createAction that', () => { - let parent: Element; - beforeEach(() => { - parent = new DOMParser().parseFromString( - '', - 'application/xml' - ).documentElement; - }); - - it('returns a WizardAction which returns a Create EditorAction', () => { - const wizardAction = createSubNetworkAction(parent); - expect(wizardAction(inputs, newWizard())[0]).to.satisfy(isCreate); - }); - }); - - describe('has an updateAction that', () => { - let element: Element; - beforeEach(() => { - element = new DOMParser().parseFromString( - '', - 'application/xml' - ).documentElement; - }); - - describe('with missing child element BitRate', () => { - let element: Element; - beforeEach(() => { - element = new DOMParser().parseFromString( - '', - 'application/xml' - ).documentElement; - }); - - it('returns a WizardAction which retruns two EditorActions', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(2); - }); - - it('returns a WizardAction with the first returned EditorAction beeing an Update', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard())[0]).to.satisfy(isUpdate); - }); - - it('returns a WizardAction with the second returned EditorAction beeing a Create', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard())[1]).to.satisfy(isCreate); - }); - }); - - describe('with present child element BitRate', () => { - let element: Element; - beforeEach(() => { - element = new DOMParser().parseFromString( - ` - 100 - `, - 'application/xml' - ).documentElement; - }); - - it('returns a WizardAction which returns two EditorActions', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(2); - }); - - it('returns a WizardAction with the first returned EditorAction beeing an Update', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard())[0]).to.satisfy(isUpdate); - }); - - it('returns a WizardAction with the second returned EditorAction beeing a Update', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard())[1]).to.satisfy(isUpdate); - }); - }); - - describe('with no change in element SubNetwork but changes in the child element BitRate', () => { - let element: Element; - beforeEach(() => { - element = new DOMParser().parseFromString( - ` - 100 - `, - 'application/xml' - ).documentElement; - }); - - it('returns a WizardAction which returns one EditorActions', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(1); - }); - - it('returns a WizardAction with the first returned EditorAction beeing an Update', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard())[0]).to.satisfy(isUpdate); - }); - }); - - describe('with no change in SubNetwork nor BitRate', () => { - let element: Element; - beforeEach(() => { - element = new DOMParser().parseFromString( - '', - 'application/xml' - ).documentElement; - }); - - it('returns a WizardAction with an empty EditorActions array', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(0); - }); - }); - }); - }); - - describe('with nulled properties', () => { - 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( - ['name', 'desc', 'type', 'BitRate'].map( - label => - >( - fixture( - html`` - ) - ) - ) - ); - }); - - describe('has an updateAction that', () => { - describe('with present child element Voltage', () => { - let element: Element; - beforeEach(async () => { - element = new DOMParser().parseFromString( - ` - 100 - `, - 'application/xml' - ).documentElement; - - inputs[3] = await fixture(html``); - }); - - it('returns a WizardAction which returns two EditorActions', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(2); - }); - - it('returns a WizardAction with the first returned EditorAction beeing an Update', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard())[0]).to.satisfy(isUpdate); - }); - - it('returns a WizardAction with the second returned EditorAction beeing a Delete', () => { - const wizardAction = updateSubNetworkAction(element); - expect(wizardAction(inputs, newWizard())[1]).to.satisfy(isDelete); - }); - }); - }); - }); -}); diff --git a/test/unit/editors/communication/__snapshots__/subnetwork-editor.test.snap.js b/test/unit/editors/communication/__snapshots__/subnetwork-editor.test.snap.js index 4f71771fe..a0e9c3362 100644 --- a/test/unit/editors/communication/__snapshots__/subnetwork-editor.test.snap.js +++ b/test/unit/editors/communication/__snapshots__/subnetwork-editor.test.snap.js @@ -2,40 +2,41 @@ export const snapshots = {}; snapshots["subnetwork-editor looks like the latest snapshot"] = -`
-

- StationBus — desc - (8-MMS—100.0b/s) - - - - - -

-
-
+ + + + + + + + + + + + +
+ -

- IED1 -

-
- - -
-
+ + + + +
-
+ `; /* end snapshot subnetwork-editor looks like the latest snapshot */ diff --git a/test/unit/editors/communication/subnetwork-editor.test.ts b/test/unit/editors/communication/subnetwork-editor.test.ts index 18b115225..a6ef4ebe3 100644 --- a/test/unit/editors/communication/subnetwork-editor.test.ts +++ b/test/unit/editors/communication/subnetwork-editor.test.ts @@ -5,16 +5,17 @@ import { SubNetworkEditor } from '../../../../src/editors/communication/subnetwo describe('subnetwork-editor', () => { let element: SubNetworkEditor; - let validSCL: XMLDocument; + let subNetwork: Element; + beforeEach(async () => { - validSCL = await fetch('/test/testfiles/valid2007B4.scd') + const validSCL = await fetch('/test/testfiles/communication.scd') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + subNetwork = validSCL.querySelector('SubNetwork')!; element = ( await fixture( - html`` + html`` ) ); }); @@ -22,13 +23,46 @@ describe('subnetwork-editor', () => { it('has a name property', () => expect(element).to.have.property('name', 'StationBus')); + it('indicates missing required name as UNDEFINED', async () => { + subNetwork.removeAttribute('name'); + await element.requestUpdate(); + + expect(element).to.have.property('name', 'UNDEFINED'); + }); + it('has a desc property', () => expect(element).to.have.property('desc', 'desc')); it('has a type property', () => expect(element).to.have.property('type', '8-MMS')); - it('looks like the latest snapshot', async () => { - await expect(element).shadowDom.to.equalSnapshot(); + it('return null with missing type attribute', async () => { + subNetwork.removeAttribute('type'); + await element.requestUpdate(); + + expect(element).to.have.property('type', null); + }); + + it('has a BitRate property', () => + expect(element).to.have.property('bitrate', '100.0b/s')); + + it('includes multiplier to bitrate property', async () => { + const bitrate = subNetwork.querySelector('BitRate'); + bitrate?.setAttribute('multiplier', 'M'); + await element.requestUpdate(); + + expect(element).to.have.property('bitrate', '100.0 Mb/s'); }); + + it('returns null with missing BitRate content event though BitRate exist as element', async () => { + const bitrate = subNetwork.querySelector('BitRate'); + bitrate!.textContent = null; + bitrate?.setAttribute('multiplier', 'M'); + await element.requestUpdate(); + + expect(element).to.have.property('bitrate', null); + }); + + it('looks like the latest snapshot', async () => + await expect(element).shadowDom.to.equalSnapshot()); }); diff --git a/test/unit/wizards/__snapshots__/subnetwork.test.snap.js b/test/unit/wizards/__snapshots__/subnetwork.test.snap.js new file mode 100644 index 000000000..867ece682 --- /dev/null +++ b/test/unit/wizards/__snapshots__/subnetwork.test.snap.js @@ -0,0 +1,238 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL element SubNetwork include an edit wizard that with existing BitRate child element looks like the latest snapshot"] = +` +
+ + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL element SubNetwork include an edit wizard that with existing BitRate child element looks like the latest snapshot */ + +snapshots["Wizards for SCL element SubNetwork include an edit wizard that with missing BitRate child element looks like the latest snapshot"] = +` +
+ + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL element SubNetwork include an edit wizard that with missing BitRate child element looks like the latest snapshot */ + +snapshots["Wizards for SCL element SubNetwork include an edit wizard that looks like the latest snapshot"] = +` +
+ + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL element SubNetwork include an edit wizard that looks like the latest snapshot */ + +snapshots["Wizards for SCL element SubNetwork include an create wizard that looks like the latest snapshot"] = +` +
+ + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL element SubNetwork include an create wizard that looks like the latest snapshot */ + diff --git a/test/unit/wizards/subnetwork.test.ts b/test/unit/wizards/subnetwork.test.ts new file mode 100644 index 000000000..9e694f2a0 --- /dev/null +++ b/test/unit/wizards/subnetwork.test.ts @@ -0,0 +1,359 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../mock-wizard.js'; +import { MockWizard } from '../../mock-wizard.js'; + +import { WizardTextField } from '../../../src/wizard-textfield.js'; +import { + isCreate, + isDelete, + WizardInput, + isUpdate, + Update, + Delete, + Create, +} from '../../../src/foundation.js'; +import { + createSubNetworkWizard, + editSubNetworkWizard, +} from '../../../src/wizards/subnetwork.js'; + +describe('Wizards for SCL element SubNetwork', () => { + 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')); + }); + + describe('with existing BitRate child element', () => { + beforeEach(async () => { + const wizard = editSubNetworkWizard(doc.querySelector('SubNetwork')!); + 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 edit any attributes with unchanged wizard inputs', async () => { + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.not.have.been.called; + }); + + it('triggers an editor action to update name attribute', async () => { + input = inputs.find(input => input.label === 'name'); + input.value = 'newSubNetName'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isUpdate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.old.element).to.have.a.attribute( + 'name', + 'StationBus' + ); + expect(updateAction.new.element).to.have.a.attribute( + 'name', + 'newSubNetName' + ); + }); + + it('triggers an editor action to update desc attribute', async () => { + input = inputs.find(input => input.label === 'desc'); + await input.requestUpdate(); + + (input).nullSwitch?.click(); + input.value = 'myNewSubNetworkDesc'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isUpdate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.old.element).to.not.have.a.attribute('desc'); + expect(updateAction.new.element).to.have.a.attribute( + 'desc', + 'myNewSubNetworkDesc' + ); + }); + + it('triggers an editor action to update type attribute', async () => { + input = inputs.find(input => input.label === 'type'); + input.value = 'myNewSubNetType'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isUpdate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.old.element).to.have.a.attribute('type', '8-MMS'); + expect(updateAction.new.element).to.have.a.attribute( + 'type', + 'myNewSubNetType' + ); + }); + + it('triggers an editor action to update BitRate element', async () => { + input = ( + inputs.find(input => input.label === 'BitRate') + ); + input.value = '200.'; + (input).multiplier = 'M'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isUpdate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.old.element.innerHTML.trim()).to.equal('100.0'); + expect(updateAction.old.element).to.not.have.attribute('multiplier'); + expect(updateAction.new.element.innerHTML.trim()).to.equal('200.'); + expect(updateAction.new.element).to.have.attribute('multiplier', 'M'); + }); + + it('triggers an editor action to remove BitRate element', async () => { + input = ( + inputs.find(input => input.label === 'BitRate') + ); + await input.requestUpdate(); + + (input).nullSwitch?.click(); + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isDelete); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.old.element.innerHTML.trim()).to.equal('100.0'); + expect(updateAction.old.element).to.not.have.attribute('multiplier'); + }); + }); + + describe('with missing BitRate child element', () => { + beforeEach(async () => { + const wizard = editSubNetworkWizard( + doc.querySelector('SubNetwork[name="ProcessBus"]')! + ); + 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('triggers an editor action to create a complete BitRate element', async () => { + input = ( + inputs.find(input => input.label === 'BitRate') + ); + await input.requestUpdate(); + + (input).nullSwitch?.click(); + (input).value = '100.0'; + (input).multiplier = 'M'; + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isCreate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.new.element.innerHTML.trim()).to.equal('100.0'); + expect(updateAction.new.element).to.have.attribute('multiplier', 'M'); + }); + + it('triggers an editor action to create BitRate element with multiplier only', async () => { + input = ( + inputs.find(input => input.label === 'BitRate') + ); + await input.requestUpdate(); + + (input).nullSwitch?.click(); + (input).multiplier = 'M'; + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isCreate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.new.element.innerHTML.trim()).to.equal(''); + expect(updateAction.new.element).to.have.attribute('multiplier'); + }); + + it('triggers an editor action to create BitRate element with bit rate only', async () => { + input = ( + inputs.find(input => input.label === 'BitRate') + ); + await input.requestUpdate(); + + (input).nullSwitch?.click(); + (input).value = '100.0'; + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isCreate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.new.element.innerHTML.trim()).to.equal('100.0'); + expect(updateAction.new.element).to.not.have.attribute('multiplier'); + }); + }); + }); + + describe('include an create 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 = createSubNetworkWizard( + doc.querySelector('Communication')! + ); + 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 creating SubNetwork with empty name attribute', async () => { + input = inputs.find(input => input.label === 'name'); + input.value = ''; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.not.be.called; + }); + + it('triggers an editor action to create SubNetwork element including BitRate', async () => { + input = inputs.find(input => input.label === 'name'); + input.value = 'myNewSubNetworkName'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isCreate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.new.element).to.have.a.attribute( + 'name', + 'myNewSubNetworkName' + ); + expect(updateAction.new.element).to.have.a.attribute('desc', ''); + expect(updateAction.new.element).to.have.a.attribute('type', '8-MMS'); + expect(updateAction.new.element.querySelector('BitRate')).to.exist; + expect( + updateAction.new.element.querySelector('BitRate') + ).to.have.attribute('multiplier', 'M'); + expect( + updateAction.new.element.querySelector('BitRate')?.textContent?.trim() + ).to.equal('100'); + }); + + it('triggers an editor action to create SubNetwork element excluding non required /BitRate', async () => { + const name = ( + inputs.find(input => input.label === 'name') + ); + const desc = ( + inputs.find(input => input.label === 'desc') + ); + const type = ( + inputs.find(input => input.label === 'type') + ); + const bitrate = ( + inputs.find(input => input.label === 'BitRate') + ); + await element.requestUpdate(); + + desc.nullSwitch?.click(); + type.nullSwitch?.click(); + bitrate.nullSwitch?.click(); + name.value = 'myNewSubNetworkName'; + await name.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + + expect(actionEvent).to.be.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isCreate); + + const updateAction = actionEvent.args[0][0].detail.action; + expect(updateAction.new.element).to.have.a.attribute( + 'name', + 'myNewSubNetworkName' + ); + expect(updateAction.new.element).to.not.have.a.attribute('desc'); + expect(updateAction.new.element).to.not.have.a.attribute('type'); + expect(updateAction.new.element.querySelector('BitRate')).to.not.exist; + }); + }); +});