diff --git a/src/translations/de.ts b/src/translations/de.ts index ab3c12776..50fff2096 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -31,11 +31,15 @@ export const de: Translations = { qchg: 'Qualitätsanderung ist Auslöser', dupd: 'Datenupdate ist Auslöser', fixedOffs: 'Fester Offset', - securityEnabled: 'Aktive Sicherungsmaßnahmen', + securityEnable: 'Aktive Sicherungsmaßnahmen', DataSet: 'Datensetz', Communication: 'Kommunikation', TrgOps: 'Triggerbedingungen', OptFields: 'Optionale felder', + multicast: 'SMV nach IEC 61850 9-2', + smpMod: 'Abtast-Art', + smpRate: 'Abtastrate', + nofASDU: 'Abtastpunkte pro Datenpacket', }, settings: { title: 'Einstellungen', diff --git a/src/translations/en.ts b/src/translations/en.ts index 2ed247105..8c23605f8 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -29,11 +29,15 @@ export const en = { qchg: 'Trigger on quality change', dupd: 'Trigger on data update', fixedOffs: 'Fixed offset', - securityEnabled: 'Security enabled', + securityEnable: 'Security enabled', DataSet: 'Dataset', Communication: 'Communication', TrgOps: 'Trigger options', OptFields: 'Optional fields', + multicast: 'SMV acc. to IEC 61850 9-2', + smpMod: 'Sample mode', + smpRate: 'Sample rate', + nofASDU: 'Samples per paket', }, settings: { title: 'Settings', diff --git a/src/wizards/foundation/enums.ts b/src/wizards/foundation/enums.ts index 98990027f..845792783 100644 --- a/src/wizards/foundation/enums.ts +++ b/src/wizards/foundation/enums.ts @@ -53,3 +53,11 @@ export const predefinedBasicTypeEnum = [ ]; export const valKindEnum = ['Spec', 'Conf', 'RO', 'Set']; + +export const smpModEnum = ['SmpPerPeriod', 'SmpPerSec', 'SecPerSmp']; + +export const securityEnableEnum = [ + 'None', + 'Signature', + 'SignatureAndEncryption', +]; diff --git a/src/wizards/gsecontrol.ts b/src/wizards/gsecontrol.ts index 28de53deb..fe2116d0c 100644 --- a/src/wizards/gsecontrol.ts +++ b/src/wizards/gsecontrol.ts @@ -28,6 +28,7 @@ import { import { maxLength, patterns } from './foundation/limits.js'; import { editDataSetWizard } from './dataset.js'; import { editGseWizard } from './gse.js'; +import { securityEnableEnum } from './foundation/enums.js'; function getGSE(element: Element): Element | null | undefined { const cbName = element.getAttribute('name'); @@ -100,8 +101,8 @@ export function renderGseAttributes( .maybeValue=${securityEnabled} nullable required - helper="${translate('scl.securityEnabled')}" - >${['None', 'Signature', 'SignatureAndEncryption'].map( + helper="${translate('scl.securityEnable')}" + >${securityEnableEnum.map( type => html`<mwc-list-item value="${type}">${type}</mwc-list-item>` )}</wizard-select >`, diff --git a/src/wizards/sampledvaluecontrol.ts b/src/wizards/sampledvaluecontrol.ts index 54036fd0d..003be8e3a 100644 --- a/src/wizards/sampledvaluecontrol.ts +++ b/src/wizards/sampledvaluecontrol.ts @@ -1,7 +1,188 @@ -import { html } from 'lit-element'; -import { get } from 'lit-translate'; +import { html, TemplateResult } from 'lit-element'; +import { get, translate } from 'lit-translate'; -import { identity, isPublic, Wizard } from '../foundation.js'; +import '@material/mwc-list/mwc-list-item.js'; +import { List } from '@material/mwc-list'; +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; +import { SingleSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; + +import '../filtered-list.js'; +import '../wizard-select.js'; +import '../wizard-textfield.js'; +import { + cloneElement, + EditorAction, + getValue, + identity, + isPublic, + newSubWizardEvent, + selector, + Wizard, + WizardActor, + WizardInput, +} from '../foundation.js'; +import { securityEnableEnum, smpModEnum } from './foundation/enums.js'; +import { maxLength, patterns } from './foundation/limits.js'; + +interface ContentOptions { + name: string | null; + desc: string | null; + multicast: string | null; + smvID: string | null; + smpMod: string | null; + smpRate: string | null; + nofASDU: string | null; + securityEnable: string | null; +} + +function contentSampledValueControlWizard( + options: ContentOptions +): TemplateResult[] { + return [ + html`<wizard-textfield + label="name" + .maybeValue=${options.name} + helper="${translate('scl.name')}" + required + validationMessage="${translate('textfield.required')}" + pattern="${patterns.asciName}" + maxLength="${maxLength.cbName}" + dialogInitialFocus + ></wizard-textfield>`, + html`<wizard-textfield + label="desc" + .maybeValue=${options.desc} + nullable + pattern="${patterns.normalizedString}" + helper="${translate('scl.desc')}" + ></wizard-textfield>`, + html`<wizard-select + label="multicast" + .maybeValue=${options.multicast} + helper="${translate('scl.multicast')}" + disabled + >${['true', 'false'].map( + option => + html`<mwc-list-item value="${option}">${option}</mwc-list-item>` + )}</wizard-select + >`, + html`<wizard-textfield + label="smvID" + .maybeValue=${options.smvID} + helper="${translate('scl.id')}" + required + validationMessage="${translate('textfield.nonempty')}" + ></wizard-textfield>`, + html`<wizard-select + label="smpMod" + .maybeValue=${options.smpMod} + nullable + required + helper="${translate('scl.smpMod')}" + >${smpModEnum.map( + option => + html`<mwc-list-item value="${option}">${option}</mwc-list-item>` + )}</wizard-select + >`, + html`<wizard-textfield + label="smpRate" + .maybeValue=${options.smpRate} + helper="${translate('scl.smpRate')}" + required + type="number" + min="0" + ></wizard-textfield>`, + html`<wizard-textfield + label="nofASDU" + .maybeValue=${options.nofASDU} + helper="${translate('scl.nofASDU')}" + required + type="number" + min="0" + ></wizard-textfield>`, + html`<wizard-select + label="securityEnable" + .maybeValue=${options.securityEnable} + nullable + required + helper="${translate('scl.securityEnable')}" + >${securityEnableEnum.map( + option => + html`<mwc-list-item value="${option}">${option}</mwc-list-item>` + )}</wizard-select + >`, + ]; +} + +function updateSampledValueControlAction(element: Element): WizardActor { + return (inputs: WizardInput[]): EditorAction[] => { + const attributes: Record<string, string | null> = {}; + const attributeKeys = [ + 'name', + 'desc', + 'multicast', + 'smvID', + 'smpMod', + 'smpRate', + 'nofASDU', + 'securityEnable', + ]; + + attributeKeys.forEach(key => { + attributes[key] = getValue(inputs.find(i => i.label === key)!); + }); + + let sampledValueControlAction: EditorAction | null = null; + if ( + attributeKeys.some(key => attributes[key] !== element.getAttribute(key)) + ) { + const newElement = cloneElement(element, attributes); + sampledValueControlAction = { + old: { element }, + new: { element: newElement }, + }; + } + + const actions: EditorAction[] = []; + if (sampledValueControlAction) actions.push(sampledValueControlAction); + return actions; + }; +} + +export function editSampledValueControlWizard(element: Element): Wizard { + const name = element.getAttribute('name'); + const desc = element.getAttribute('desc'); + const multicast = element.getAttribute('multicast'); + const smvID = element.getAttribute('smvID'); + const smpMod = element.getAttribute('smpMod'); + const smpRate = element.getAttribute('smpRate'); + const nofASDU = element.getAttribute('nofASDU'); + const securityEnable = element.getAttribute('securityEnabled'); + + return [ + { + title: get('wizard.title.edit', { tagName: element.tagName }), + element, + primary: { + icon: 'save', + label: get('save'), + action: updateSampledValueControlAction(element), + }, + content: [ + ...contentSampledValueControlWizard({ + name, + desc, + multicast, + smvID, + smpMod, + smpRate, + nofASDU, + securityEnable, + }), + ], + }, + ]; +} export function selectSampledValueControlWizard(element: Element): Wizard { const smvControls = Array.from( @@ -13,6 +194,19 @@ export function selectSampledValueControlWizard(element: Element): Wizard { title: get('wizard.title.select', { tagName: 'SampledValueControl' }), content: [ html`<filtered-list + @selected=${(e: SingleSelectedEvent) => { + const identity = (<ListItemBase>(<List>e.target).selected).value; + const sampledValueControl = element.querySelector( + selector('SampledValueControl', identity) + ); + if (!sampledValueControl) return; + + e.target?.dispatchEvent( + newSubWizardEvent(() => + editSampledValueControlWizard(sampledValueControl) + ) + ); + }} >${smvControls.map( smvControl => html`<mwc-list-item twoline value="${identity(smvControl)}" diff --git a/src/zeroline-pane.ts b/src/zeroline-pane.ts index 4dad80604..88a4ef73e 100644 --- a/src/zeroline-pane.ts +++ b/src/zeroline-pane.ts @@ -19,7 +19,7 @@ import './zeroline/ied-editor.js'; import { Settings } from './Setting.js'; import { communicationMappingWizard } from './wizards/commmap-wizards.js'; import { gooseIcon, smvIcon, reportIcon } from './icons.js'; -import { isPublic, newSubWizardEvent, newWizardEvent } from './foundation.js'; +import { isPublic, newWizardEvent } from './foundation.js'; import { selectGseControlWizard } from './wizards/gsecontrol.js'; import { wizards } from './wizards/wizard-library.js'; import { getAttachedIeds } from './zeroline/foundation.js'; @@ -66,21 +66,22 @@ export class ZerolinePane extends LitElement { openReportControlSelection(): void { this.dispatchEvent( - newSubWizardEvent(() => - selectReportControlWizard(this.doc.documentElement) - ) + newWizardEvent(() => selectReportControlWizard(this.doc.documentElement)) ); } openGseControlSelection(): void { this.dispatchEvent( - newSubWizardEvent(() => selectGseControlWizard(this.doc.documentElement)) + newWizardEvent(() => selectGseControlWizard(this.doc.documentElement)) ); } openSampledValueControlSelection(): void { - const wizard = selectSampledValueControlWizard(this.doc.documentElement); - if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + this.dispatchEvent( + newWizardEvent(() => + selectSampledValueControlWizard(this.doc.documentElement) + ) + ); } toggleShowIEDs(): void { diff --git a/src/zeroline/ied-editor.ts b/src/zeroline/ied-editor.ts index 8baf78cba..d9a13b23e 100644 --- a/src/zeroline/ied-editor.ts +++ b/src/zeroline/ied-editor.ts @@ -15,7 +15,7 @@ import '../action-icon.js'; import { createClientLnWizard } from '../wizards/clientln.js'; import { gooseIcon, smvIcon, reportIcon } from '../icons.js'; import { wizards } from '../wizards/wizard-library.js'; -import { newSubWizardEvent, newWizardEvent } from '../foundation.js'; +import { newWizardEvent } from '../foundation.js'; import { selectGseControlWizard } from '../wizards/gsecontrol.js'; import { selectSampledValueControlWizard } from '../wizards/sampledvaluecontrol.js'; import { selectReportControlWizard } from '../wizards/reportcontrol.js'; @@ -41,19 +41,20 @@ export class IedEditor extends LitElement { private openReportControlSelection(): void { this.dispatchEvent( - newSubWizardEvent(() => selectReportControlWizard(this.element)) + newWizardEvent(() => selectReportControlWizard(this.element)) ); } private openGseControlSelection(): void { this.dispatchEvent( - newSubWizardEvent(() => selectGseControlWizard(this.element)) + newWizardEvent(() => selectGseControlWizard(this.element)) ); } private openSmvControlSelection(): void { - const wizard = selectSampledValueControlWizard(this.element); - if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + this.dispatchEvent( + newWizardEvent(() => selectSampledValueControlWizard(this.element)) + ); } private openCommunicationMapping(): void { diff --git a/test/integration/wizards/sampledvaluecontrol-wizarding-editing.test.ts b/test/integration/wizards/sampledvaluecontrol-wizarding-editing.test.ts new file mode 100644 index 000000000..41554180f --- /dev/null +++ b/test/integration/wizards/sampledvaluecontrol-wizarding-editing.test.ts @@ -0,0 +1,173 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import '../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../mock-wizard-editor.js'; + +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; + +import { FilteredList } from '../../../src/filtered-list.js'; +import { WizardTextField } from '../../../src/wizard-textfield.js'; +import { selectSampledValueControlWizard } from '../../../src/wizards/sampledvaluecontrol.js'; + +describe('Wizards for SCL element SampledValueControl', () => { + let doc: XMLDocument; + let element: MockWizardEditor; + + beforeEach(async () => { + element = await fixture(html`<mock-wizard-editor></mock-wizard-editor>`); + doc = await fetch('/test/testfiles/wizards/sampledvaluecontrol.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + }); + + describe('define a select wizards that ', () => { + let sampledValueControlList: FilteredList; + + beforeEach(async () => { + const wizard = selectSampledValueControlWizard(doc.documentElement); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + sampledValueControlList = <FilteredList>( + element.wizardUI.dialog?.querySelector('filtered-list') + ); + await sampledValueControlList.updateComplete; + }); + + it('shows all SampledValueControl elements within a project', () => + expect(sampledValueControlList.items.length).to.equal( + doc.querySelectorAll('SampledValueControl').length + )); + + it('allows to filter SampledValueControl elements per IED', async () => { + const wizard = selectSampledValueControlWizard(doc.querySelector('IED')!); + element.workflow.pop(); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + sampledValueControlList = <FilteredList>( + element.wizardUI.dialog?.querySelector('filtered-list') + ); + await sampledValueControlList.updateComplete; + + expect(sampledValueControlList.items.length).to.equal( + doc.querySelector('IED')!.querySelectorAll('SampledValueControl').length + ); + }); + + it('opens edit wizard for selected SampledValueControl element on click', async () => { + const reportItem = <ListItemBase>sampledValueControlList.items[0]; + reportItem.click(); + await new Promise(resolve => setTimeout(resolve, 20)); // await animation + + const nameField = <WizardTextField>( + element.wizardUI.dialog?.querySelector('wizard-textfield[label="name"]') + ); + await nameField.requestUpdate(); + + expect(nameField.value).to.equal( + doc.querySelectorAll('SampledValueControl')[0].getAttribute('name') + ); + }); + }); + + describe('defines an edit wizard that', () => { + let nameField: WizardTextField; + let secondaryAction: HTMLElement; + let primaryAction: HTMLElement; + let parentIED: Element; + + beforeEach(async () => { + element.workflow.length = 0; // remove all wizard from FIFO queue + parentIED = doc.querySelectorAll('IED')[1]; + element.workflow.push(() => selectSampledValueControlWizard(parentIED)); + await element.requestUpdate(); + await new Promise(resolve => setTimeout(resolve, 20)); // await animation + + const sampledValueControlBlock = <ListItemBase>( + (<FilteredList>element.wizardUI.dialog?.querySelector('filtered-list')) + .items[0] + ); + sampledValueControlBlock.click(); + await new Promise(resolve => setTimeout(resolve, 20)); // await animation + + nameField = element.wizardUI.dialog!.querySelector( + 'wizard-textfield[label="name"]' + )!; + primaryAction = <HTMLElement>( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + secondaryAction = <HTMLElement>( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="secondaryAction"]' + ) + ); + await nameField.updateComplete; + }); + + it('rejects name attribute starting with decimals', async () => { + expect( + parentIED + .querySelectorAll('SampledValueControl')[0] + ?.getAttribute('name') + ).to.not.equal('4adsasd'); + + nameField.value = '4adsasd'; + await element.requestUpdate(); + primaryAction.click(); + + expect( + parentIED + .querySelectorAll('SampledValueControl')[0] + ?.getAttribute('name') + ).to.not.equal('4adsasd'); + }); + + it('edits name attribute on primary action', async () => { + expect( + parentIED + .querySelectorAll('SampledValueControl')[0] + ?.getAttribute('name') + ).to.not.equal('myNewName'); + + nameField.value = 'myNewName'; + await element.requestUpdate(); + primaryAction.click(); + + expect( + parentIED + .querySelectorAll('SampledValueControl')[0] + ?.getAttribute('name') + ).to.equal('myNewName'); + }); + + it('dynamically updates wizards after attribute change', async () => { + nameField.value = 'myNewName'; + primaryAction.click(); + + await new Promise(resolve => setTimeout(resolve, 20)); // await animation + + const sampledValueControlBlock = <ListItemBase>( + (<FilteredList>element.wizardUI.dialog?.querySelector('filtered-list')) + .items[0] + ); + + expect(sampledValueControlBlock.innerHTML).to.contain('myNewName'); + }); + + it('returns back to its starting wizard on secondary action', async () => { + secondaryAction.click(); + + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + + const sampledValueControlBlock = <ListItemBase>( + (<FilteredList>element.wizardUI.dialog?.querySelector('filtered-list')) + .items[0] + ); + + expect(sampledValueControlBlock.innerHTML).to.contain('MSVCB01'); + }); + }); +}); diff --git a/test/testfiles/wizards/sampledvaluecontrol.scd b/test/testfiles/wizards/sampledvaluecontrol.scd index 78f25eeaa..1ccb144f9 100644 --- a/test/testfiles/wizards/sampledvaluecontrol.scd +++ b/test/testfiles/wizards/sampledvaluecontrol.scd @@ -180,7 +180,7 @@ <TrgOps dchg="true" qchg="true" dupd="false" period="false" gi="true"/> <OptFields seqNum="true" timeStamp="true" dataSet="true" reasonCode="true" dataRef="false" entryID="false" configRef="true" bufOvfl="false"/> </ReportControl> - <SampledValueControl smvID="asdfads" multicast="true" smpRate="80" nofASDU="1" confRev="1" name="MSVCB01" datSet="GooseDataSet1"> + <SampledValueControl smvID="some/reference" multicast="true" smpRate="80" nofASDU="1" confRev="1" name="MSVCB01" datSet="GooseDataSet1"> <SmvOpts/> </SampledValueControl> </LN0> diff --git a/test/unit/wizards/__snapshots__/gsecontrol.test.snap.js b/test/unit/wizards/__snapshots__/gsecontrol.test.snap.js index 595f08486..e07091db1 100644 --- a/test/unit/wizards/__snapshots__/gsecontrol.test.snap.js +++ b/test/unit/wizards/__snapshots__/gsecontrol.test.snap.js @@ -144,7 +144,7 @@ snapshots["gsecontrol wizards renderGseAttribute looks like the latest snapshot" </wizard-select> <wizard-select disabled="" - helper="[scl.securityEnabled]" + helper="[scl.securityEnable]" label="securityEnabled" nullable="" required="" @@ -270,7 +270,7 @@ snapshots["gsecontrol wizards editGseControlWizard looks like the latest snapsho </wizard-select> <wizard-select disabled="" - helper="[scl.securityEnabled]" + helper="[scl.securityEnable]" label="securityEnabled" nullable="" required="" diff --git a/test/unit/wizards/__snapshots__/sampledvaluecontrol.test.snap.js b/test/unit/wizards/__snapshots__/sampledvaluecontrol.test.snap.js index 4506ae2c6..a2d796594 100644 --- a/test/unit/wizards/__snapshots__/sampledvaluecontrol.test.snap.js +++ b/test/unit/wizards/__snapshots__/sampledvaluecontrol.test.snap.js @@ -1,6 +1,168 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; +snapshots["Wizards for SCL element SampledValueControl define an edit wizard that looks like the latest snapshot"] = +`<mwc-dialog + defaultaction="close" + heading="[wizard.title.edit]" + open="" +> + <div id="wizard-content"> + <wizard-textfield + dialoginitialfocus="" + helper="[scl.name]" + label="name" + maxlength="32" + pattern="[A-Za-z][0-9,A-Z,a-z_]*" + required="" + validationmessage="[textfield.required]" + > + </wizard-textfield> + <wizard-textfield + disabled="" + helper="[scl.desc]" + label="desc" + nullable="" + pattern="([ -~]|[ ]|[ -]|[-�])*" + > + </wizard-textfield> + <wizard-select + disabled="" + helper="[scl.multicast]" + label="multicast" + > + <mwc-list-item + aria-disabled="false" + mwc-list-item="" + role="option" + tabindex="-1" + value="true" + > + true + </mwc-list-item> + <mwc-list-item + aria-disabled="false" + mwc-list-item="" + role="option" + tabindex="-1" + value="false" + > + false + </mwc-list-item> + </wizard-select> + <wizard-textfield + helper="[scl.id]" + label="smvID" + required="" + validationmessage="[textfield.nonempty]" + > + </wizard-textfield> + <wizard-select + disabled="" + helper="[scl.smpMod]" + label="smpMod" + nullable="" + required="" + > + <mwc-list-item + aria-disabled="false" + mwc-list-item="" + role="option" + tabindex="-1" + value="SmpPerPeriod" + > + SmpPerPeriod + </mwc-list-item> + <mwc-list-item + aria-disabled="false" + mwc-list-item="" + role="option" + tabindex="-1" + value="SmpPerSec" + > + SmpPerSec + </mwc-list-item> + <mwc-list-item + aria-disabled="false" + mwc-list-item="" + role="option" + tabindex="-1" + value="SecPerSmp" + > + SecPerSmp + </mwc-list-item> + </wizard-select> + <wizard-textfield + helper="[scl.smpRate]" + label="smpRate" + min="0" + required="" + type="number" + > + </wizard-textfield> + <wizard-textfield + helper="[scl.nofASDU]" + label="nofASDU" + min="0" + required="" + type="number" + > + </wizard-textfield> + <wizard-select + disabled="" + helper="[scl.securityEnable]" + label="securityEnable" + nullable="" + required="" + > + <mwc-list-item + aria-disabled="false" + mwc-list-item="" + role="option" + tabindex="-1" + value="None" + > + None + </mwc-list-item> + <mwc-list-item + aria-disabled="false" + mwc-list-item="" + role="option" + tabindex="-1" + value="Signature" + > + Signature + </mwc-list-item> + <mwc-list-item + aria-disabled="false" + mwc-list-item="" + role="option" + tabindex="-1" + value="SignatureAndEncryption" + > + SignatureAndEncryption + </mwc-list-item> + </wizard-select> + </div> + <mwc-button + dialogaction="close" + label="[cancel]" + slot="secondaryAction" + style="--mdc-theme-primary: var(--mdc-theme-error)" + > + </mwc-button> + <mwc-button + dialoginitialfocus="" + icon="save" + label="[save]" + slot="primaryAction" + trailingicon="" + > + </mwc-button> +</mwc-dialog> +`; +/* end snapshot Wizards for SCL element SampledValueControl define an edit wizard that looks like the latest snapshot */ + snapshots["Wizards for SCL element SampledValueControl define a select wizard that looks like the latest snapshot"] = `<mwc-dialog defaultaction="close" @@ -12,7 +174,7 @@ snapshots["Wizards for SCL element SampledValueControl define a select wizard th <mwc-list-item aria-disabled="false" mwc-list-item="" - tabindex="-1" + tabindex="0" twoline="" value="IED2>>CBSW>MSVCB01" > diff --git a/test/unit/wizards/sampledvaluecontrol.test.ts b/test/unit/wizards/sampledvaluecontrol.test.ts index 2a25464cd..b001a441b 100644 --- a/test/unit/wizards/sampledvaluecontrol.test.ts +++ b/test/unit/wizards/sampledvaluecontrol.test.ts @@ -4,11 +4,21 @@ import { SinonSpy, spy } from 'sinon'; import '../../mock-wizard.js'; import { MockWizard } from '../../mock-wizard.js'; -import { selectSampledValueControlWizard } from '../../../src/wizards/sampledvaluecontrol.js'; +import { isUpdate, Update, WizardInput } from '../../../src/foundation.js'; +import { + editSampledValueControlWizard, + selectSampledValueControlWizard, +} from '../../../src/wizards/sampledvaluecontrol.js'; +import fc, { integer } from 'fast-check'; +import { inverseRegExp, regExp, regexString } from '../../foundation.js'; +import { WizardTextField } from '../../../src/wizard-textfield.js'; describe('Wizards for SCL element SampledValueControl', () => { let doc: XMLDocument; let element: MockWizard; + let inputs: WizardInput[]; + + let primaryAction: HTMLElement; let actionEvent: SinonSpy; @@ -22,6 +32,241 @@ describe('Wizards for SCL element SampledValueControl', () => { window.addEventListener('editor-action', actionEvent); }); + describe('define an edit wizard that', () => { + beforeEach(async () => { + const wizard = editSampledValueControlWizard( + doc.querySelector('SampledValueControl')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + inputs = Array.from(element.wizardUI.inputs); + + primaryAction = <HTMLElement>( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + await element.wizardUI.requestUpdate(); // make sure wizard is rendered + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }).timeout(5000); + + it('edits name attribute only for valid inputs', async () => { + await fc.assert( + fc.asyncProperty(regexString(regExp.tAsciName, 1, 32), async name => { + inputs[0].value = name; + await (<WizardTextField>inputs[0]).requestUpdate(); + expect(inputs[0].checkValidity()).to.be.true; + }) + ); + }); + + it('rejects name attribute starting with decimals', async () => { + await fc.assert( + fc.asyncProperty(regexString(regExp.decimal, 1, 1), async name => { + inputs[0].value = name; + await (<WizardTextField>inputs[0]).requestUpdate(); + expect(inputs[0].checkValidity()).to.be.false; + }) + ); + }); + + it('edits smpRate attribute only for valid inputs', async () => { + await fc.assert( + fc.asyncProperty( + integer({ min: 0 }).map(num => `${num}`), + async smpRate => { + inputs[5].value = smpRate; + await (<WizardTextField>inputs[5]).requestUpdate(); + expect(inputs[5].checkValidity()).to.be.true; + } + ) + ); + }); + + it('rejects smpRate attribute in case input is not unsigned int', async () => { + await fc.assert( + fc.asyncProperty(regexString(inverseRegExp.uint, 1), async smpRate => { + inputs[5].value = smpRate; + await (<WizardTextField>inputs[5]).requestUpdate(); + expect(inputs[5].checkValidity()).to.be.false; + }) + ); + }); + + it('edits nofASDU attribute only for valid inputs', async () => { + const input = <WizardTextField>inputs[6]; + input.nullSwitch?.click(); + await input.requestUpdate(); + + await fc.assert( + fc.asyncProperty( + integer({ min: 0 }).map(num => `${num}`), + async nofASDU => { + input.value = nofASDU; + await input.requestUpdate(); + expect(input.checkValidity()).to.be.true; + } + ) + ); + }); + + it('rejects nofASDU attribute in case input is not unsigned int', async () => { + const input = <WizardTextField>inputs[6]; + input.nullSwitch?.click(); + await input.requestUpdate(); + + await fc.assert( + fc.asyncProperty(regexString(inverseRegExp.uint, 1), async nofASDU => { + input.value = nofASDU; + await input.requestUpdate(); + expect(input.checkValidity()).to.be.false; + }) + ); + }); + + it('does not update the SampledValueControl element when no attribute has changed', async () => { + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent.notCalled).to.be.true; + }); + + it('update a SampledValueControl element when only name attribute changed', async () => { + const input = <WizardTextField>inputs[0]; + input.value = 'myNewCbName'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = <Update>action; + expect(updateAction.old.element).to.have.attribute('name', 'MSVCB01'); + expect(updateAction.new.element).to.have.attribute('name', 'myNewCbName'); + }); + + it('update a SampledValueControl element when only desc attribute changed', async () => { + const input = <WizardTextField>inputs[1]; + input.nullSwitch?.click(); + input.value = 'myDesc'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = <Update>action; + expect(updateAction.old.element).to.not.have.attribute('desc'); + expect(updateAction.new.element).to.have.attribute('desc', 'myDesc'); + }); + + it('update a SampledValueControl element when smvID attribute changed', async () => { + const input = <WizardTextField>inputs[3]; + input.value = 'myNewType/ID'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = <Update>action; + expect(updateAction.old.element).to.have.attribute( + 'smvID', + 'some/reference' + ); + expect(updateAction.new.element).to.have.attribute( + 'smvID', + 'myNewType/ID' + ); + }); + + it('update a SampledValueControl element when smpMod attribute changed', async () => { + const input = <WizardTextField>inputs[4]; + input.nullSwitch?.click(); + input.value = 'SmpPerSec'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = <Update>action; + expect(updateAction.old.element).to.not.have.attribute('smpMod'); + expect(updateAction.new.element).to.have.attribute('smpMod', 'SmpPerSec'); + }); + + it('update a SampledValueControl element when smpRate attribute changed', async () => { + const input = <WizardTextField>inputs[5]; + input.value = '4000'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = <Update>action; + expect(updateAction.old.element).to.have.attribute('smpRate', '80'); + expect(updateAction.new.element).to.have.attribute('smpRate', '4000'); + }); + + it('update a SampledValueControl element when nofASDU attribute changed', async () => { + const input = <WizardTextField>inputs[6]; + input.value = '2'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = <Update>action; + expect(updateAction.old.element).to.have.attribute('nofASDU', '1'); + expect(updateAction.new.element).to.have.attribute('nofASDU', '2'); + }); + + it('updates the SampledValueEnable element when securityEnable changed', async () => { + const input = <WizardTextField>inputs[7]; + input.nullSwitch?.click(); + input.value = 'Signature'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = <Update>action; + expect(updateAction.old.element).to.not.have.attribute('securityEnable'); + expect(updateAction.new.element).to.have.attribute( + 'securityEnable', + 'Signature' + ); + }); + }); + describe('define a select wizard that', () => { beforeEach(async () => { const wizard = selectSampledValueControlWizard(doc.documentElement);