diff --git a/packages/openscd/src/translations/de.ts b/packages/openscd/src/translations/de.ts index c6a8233c7f..c3be050ffb 100644 --- a/packages/openscd/src/translations/de.ts +++ b/packages/openscd/src/translations/de.ts @@ -284,10 +284,23 @@ export const de: Translations = { noResults: 'Keine Logical Node Types gefunden', }, }, + accesspoint: { + wizard: { + nameHelper: 'AccessPoint Name', + descHelper: 'AccessPoint Beschreibung', + title: { + add: 'AccessPoint hinzufügen', + edit: 'AccessPoint bearbeiten', + delete: 'AccessPoint mit Abhängigkeiten entfernen', + references: 'Gelöschte Abhängikeiten', + }, + }, + }, ied: { wizard: { nameHelper: 'Name des IED', descHelper: 'Beschreibung des IED', + manufacturerHelper: 'Hersteller des IED', title: { edit: 'IED bearbeiten', delete: 'IED mit Abhängigkeiten entfernen', @@ -304,6 +317,7 @@ export const de: Translations = { nameHelper: 'Name des Logisches Gerät', noNameSupportHelper: 'IED unterstützt keine funktionale Benennung', descHelper: 'Beschreibung des Logisches Gerät', + instHelper: 'Instanz des Logisches Gerät', title: { edit: 'Logisches Gerät bearbeiten', }, diff --git a/packages/openscd/src/translations/en.ts b/packages/openscd/src/translations/en.ts index 8345e33ac2..9d98b081b1 100644 --- a/packages/openscd/src/translations/en.ts +++ b/packages/openscd/src/translations/en.ts @@ -281,10 +281,23 @@ export const en = { noResults: 'No Logical Node Types found', }, }, + accesspoint: { + wizard: { + nameHelper: 'AccessPoint name', + descHelper: 'AccessPoint description', + title: { + add: 'Add AccessPoint', + edit: 'Edit AccessPoint', + delete: 'Remove AccessPoint with references', + references: 'References to be removed', + }, + }, + }, ied: { wizard: { nameHelper: 'IED name', descHelper: 'IED description', + manufacturerHelper: 'IED manufacturer', title: { edit: 'Edit IED', delete: 'Remove IED with references', @@ -301,6 +314,7 @@ export const en = { nameHelper: 'Logical device name', noNameSupportHelper: "IED doesn't support Functional Naming", descHelper: 'Logical device description', + instHelper: 'Logical device inst', title: { edit: 'Edit logical device', }, diff --git a/packages/plugins/src/editors/ied/access-point-container.ts b/packages/plugins/src/editors/ied/access-point-container.ts index 88be47fdc9..ee9614cc1b 100644 --- a/packages/plugins/src/editors/ied/access-point-container.ts +++ b/packages/plugins/src/editors/ied/access-point-container.ts @@ -8,7 +8,7 @@ import { TemplateResult, } from 'lit-element'; import { nothing } from 'lit-html'; -import { get } from 'lit-translate'; +import { get, translate } from 'lit-translate'; import { getDescriptionAttribute, @@ -16,7 +16,10 @@ import { newWizardEvent, } from '@openscd/open-scd/src/foundation.js'; import { accessPointIcon } from '@openscd/open-scd/src/icons/ied-icons.js'; +import { newActionEvent } from '@openscd/core/foundation/deprecated/editor.js'; +import { wizards } from '../../wizards/wizard-library.js'; import { editServicesWizard } from '../../wizards/services.js'; +import { removeAccessPointWizard } from '../../wizards/accesspoint.js'; import '@openscd/open-scd/src/action-pane.js'; import './server-container.js'; @@ -29,6 +32,16 @@ export class AccessPointContainer extends Container { @property() selectedLNClasses: string[] = []; + @state() + private get lnElements(): Element[] { + return Array.from(this.element.querySelectorAll(':scope > LN')).filter( + element => { + const lnClass = element.getAttribute('lnClass') ?? ''; + return this.selectedLNClasses.includes(lnClass); + } + ); + } + protected updated(_changedProperties: PropertyValues): void { super.updated(_changedProperties); @@ -45,27 +58,22 @@ export class AccessPointContainer extends Container { return html``; } - return html` - this.openSettingsWizard(services)} - > - `; + return html` this.openSettingsWizard(services)} + >`; } - private openSettingsWizard(services: Element): void { - const wizard = editServicesWizard(services); + private openEditWizard(): void { + const wizard = wizards['AccessPoint'].edit(this.element); if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } - @state() - private get lnElements(): Element[] { - return Array.from(this.element.querySelectorAll(':scope > LN')).filter( - element => { - const lnClass = element.getAttribute('lnClass') ?? ''; - return this.selectedLNClasses.includes(lnClass); - } - ); + private openSettingsWizard(services: Element): void { + const wizard = editServicesWizard(services); + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } private header(): TemplateResult { @@ -75,11 +83,37 @@ export class AccessPointContainer extends Container { return html`${name}${desc ? html` — ${desc}` : nothing}`; } + private removeAccessPoint(): void { + const wizard = removeAccessPointWizard(this.element); + if (wizard) { + this.dispatchEvent(newWizardEvent(() => wizard)); + } else { + // If no Wizard is needed, just remove the element. + this.dispatchEvent( + newActionEvent({ + old: { parent: this.element.parentElement!, element: this.element }, + }) + ); + } + } + render(): TemplateResult { const lnElements = this.lnElements; return html` ${accessPointIcon} + this.removeAccessPoint()} + > + this.openEditWizard()} + > ${this.renderServicesIcon()} ${Array.from(this.element.querySelectorAll(':scope > Server')).map( server => diff --git a/packages/plugins/src/editors/ied/ldevice-container.ts b/packages/plugins/src/editors/ied/ldevice-container.ts index a18ddb7747..e15f711cb0 100644 --- a/packages/plugins/src/editors/ied/ldevice-container.ts +++ b/packages/plugins/src/editors/ied/ldevice-container.ts @@ -23,6 +23,7 @@ import { getLdNameAttribute, newWizardEvent, } from '@openscd/open-scd/src/foundation.js'; +import { newActionEvent } from '@openscd/core/foundation/deprecated/editor.js'; import { wizards } from '../../wizards/wizard-library.js'; import { Container } from './foundation.js'; @@ -104,11 +105,25 @@ export class LDeviceContainer extends Container { this.dispatchEvent(newEditEventV2(inserts)); } + private removeLDevice(): void { + this.dispatchEvent( + newActionEvent({ + old: { parent: this.element.parentElement!, element: this.element }, + }) + ); + } + render(): TemplateResult { const lnElements = this.lnElements; return html` ${logicalDeviceIcon} + this.removeLDevice()} + > ${doElements.length > 0 - ? html` - - - - this.requestUpdate()} - > - ` + ? html`${this.element.tagName === 'LN' + ? html` this.removeElement()} + >` + : nothing} + + + + this.requestUpdate()} + > + ` : nothing} ${this.toggleButton?.on ? doElements.map( diff --git a/packages/plugins/src/editors/ied/server-container.ts b/packages/plugins/src/editors/ied/server-container.ts index 99825a0fbb..4e948638c2 100644 --- a/packages/plugins/src/editors/ied/server-container.ts +++ b/packages/plugins/src/editors/ied/server-container.ts @@ -81,6 +81,7 @@ export class ServerContainer extends Container { const ln0 = createElement(this.doc, 'LN0', { lnClass: 'LLN0', + inst: '', lnType: lnTypeId, }); diff --git a/packages/plugins/src/wizards/accesspoint.ts b/packages/plugins/src/wizards/accesspoint.ts new file mode 100644 index 0000000000..356a99ea8c --- /dev/null +++ b/packages/plugins/src/wizards/accesspoint.ts @@ -0,0 +1,136 @@ +import { html, TemplateResult } from 'lit-element'; +import { get } from 'lit-translate'; + +import '@openscd/open-scd/src/wizard-textfield.js'; +import { + newWizardEvent, + Wizard, + WizardInputElement, + WizardActor, + isPublic, + identity, +} from '@openscd/open-scd/src/foundation.js'; +import { + Delete, + ComplexAction, + EditorAction, +} from '@openscd/core/foundation/deprecated/editor.js'; +import { updateNamingAttributeWithReferencesAction } from './foundation/actions.js'; +import { deleteReferences } from './foundation/references.js'; +import { patterns } from './foundation/limits.js'; + +export function renderAccessPointWizard( + name: string | null, + desc: string | null, + reservedNames: string[] +): TemplateResult[] { + return [ + html` + `, + html` + `, + ]; +} +export function removeAccessPointWizard(element: Element): Wizard | null { + const references = deleteReferences(element); + if (references.length > 0) { + return [ + { + title: get('accesspoint.wizard.title.delete'), + content: renderAccessPointReferencesWizard(references), + primary: { + icon: 'delete', + label: get('remove'), + action: removeAccessPointAndReferences(element), + }, + }, + ]; + } + return null; +} + +export function editAccessPointWizard(element: Element): Wizard { + const name = element.getAttribute('name'); + const desc = element.getAttribute('desc'); + return [ + { + title: get('accesspoint.wizard.title.edit'), + element, + primary: { + icon: 'edit', + label: get('save'), + action: updateNamingAttributeWithReferencesAction( + element, + 'accesspoint.action.updateAccessPoint' + ), + }, + content: renderAccessPointWizard( + name, + desc, + reservedNamesAccessPoint(element) + ), + }, + ]; +} + +function renderAccessPointReferencesWizard( + references: Delete[] +): TemplateResult[] { + return [ + html`
+

${get('accesspoint.wizard.title.references')}

+ + ${references.map(reference => { + const oldElement = reference.old.element; + return html` + ${oldElement.tagName} + ${identity(reference.old.element)} + `; + })} + +
`, + ]; +} + +function reservedNamesAccessPoint(currentElement: Element): string[] { + const ied = currentElement.closest('IED'); + if (!ied) return []; + + return Array.from(ied.querySelectorAll(':scope > AccessPoint')) + .filter(isPublic) + .map(ap => ap.getAttribute('name') ?? '') + .filter(name => name !== currentElement.getAttribute('name')); +} + +export function removeAccessPointAndReferences(element: Element): WizardActor { + return (inputs: WizardInputElement[], wizard: Element): EditorAction[] => { + wizard.dispatchEvent(newWizardEvent()); + const referencesDeleteActions = deleteReferences(element); + const name = element.getAttribute('name') ?? 'Unknown'; + const complexAction: ComplexAction = { + actions: [], + title: get('ied.action.deleteAccessPoint', { name }), + }; + complexAction.actions.push({ + old: { parent: element.parentElement!, element }, + }); + complexAction.actions.push(...referencesDeleteActions); + return [complexAction]; + }; +} diff --git a/packages/plugins/src/wizards/foundation/actions.ts b/packages/plugins/src/wizards/foundation/actions.ts index 67393f1b3d..3cc95f8b63 100644 --- a/packages/plugins/src/wizards/foundation/actions.ts +++ b/packages/plugins/src/wizards/foundation/actions.ts @@ -4,14 +4,12 @@ import { WizardInputElement, } from '@openscd/open-scd/src/foundation.js'; -import { - cloneElement, -} from '@openscd/xml'; +import { cloneElement } from '@openscd/xml'; import { ComplexAction, EditorAction, - createUpdateAction + createUpdateAction, } from '@openscd/core/foundation/deprecated/editor'; import { get } from 'lit-translate'; import { updateReferences } from './references.js'; @@ -69,6 +67,13 @@ export function updateNamingAttributeWithReferencesAction( return (inputs: WizardInputElement[]): EditorAction[] => { const newAttributes: Record = {}; processNamingAttributes(newAttributes, element, inputs); + processOptionalAttribute( + newAttributes, + element, + inputs, + 'manufacturer', + 'manufacturer' + ); if (Object.keys(newAttributes).length == 0) { return []; } @@ -103,6 +108,21 @@ export function processNamingAttributes( } } +function processOptionalAttribute( + newAttributes: Record, + element: Element, + inputs: WizardInputElement[], + inputLabel: string, + attrName: string +): void { + const input = inputs.find(i => i.label === inputLabel); + if (!input) return; + const value = getValue(input); + if (value !== element.getAttribute(attrName)) { + newAttributes[attrName] = value; + } +} + export function addMissingAttributes( element: Element, newAttributes: Record diff --git a/packages/plugins/src/wizards/foundation/references.ts b/packages/plugins/src/wizards/foundation/references.ts index 6e6f48a41f..7893ac9521 100644 --- a/packages/plugins/src/wizards/foundation/references.ts +++ b/packages/plugins/src/wizards/foundation/references.ts @@ -2,11 +2,14 @@ import { getNameAttribute, isPublic, } from '@openscd/open-scd/src/foundation.js'; -import { - Delete, - Replace -} from '@openscd/core/foundation/deprecated/editor'; -const referenceInfoTags = ['IED', 'Substation', 'VoltageLevel', 'Bay'] as const; +import { Delete, Replace } from '@openscd/core/foundation/deprecated/editor'; +const referenceInfoTags = [ + 'IED', + 'AccessPoint', + 'Substation', + 'VoltageLevel', + 'Bay', +] as const; type ReferencesInfoTag = (typeof referenceInfoTags)[number]; type FilterFunction = ( @@ -66,6 +69,12 @@ const referenceInfos: Record< filter: simpleTextContentFilter(`LN > DOI > DAI > Val`), }, ], + AccessPoint: [ + { + attributeName: 'apName', + filter: simpleAttributeFilter(`ServerAt`), + }, + ], Substation: [ { attributeName: 'substationName', diff --git a/packages/plugins/src/wizards/ied.ts b/packages/plugins/src/wizards/ied.ts index e4fbb4693e..56c5581b12 100644 --- a/packages/plugins/src/wizards/ied.ts +++ b/packages/plugins/src/wizards/ied.ts @@ -15,11 +15,11 @@ import { WizardInputElement, WizardMenuActor, } from '@openscd/open-scd/src/foundation.js'; -import { +import { ComplexAction, Delete, EditorAction, - newActionEvent + newActionEvent, } from '@openscd/core/foundation/deprecated/editor.js'; import { patterns } from './foundation/limits.js'; @@ -72,9 +72,9 @@ export function renderIEDWizard( >`, html``, html``, html``, ]; } @@ -75,10 +79,21 @@ function ldNameIsAllowed(element: Element): boolean { return false; } +function reservedInstLDevice(currentElement: Element): string[] { + const ied = currentElement.closest('IED'); + if (!ied) return []; + + return Array.from( + ied.querySelectorAll(':scope > AccessPoint > Server > LDevice') + ) + .map(ld => ld.getAttribute('inst') ?? '') + .filter(name => name !== currentElement.getAttribute('inst')); +} + function updateAction(element: Element): WizardActor { return (inputs: WizardInputElement[]): SimpleAction[] => { const ldAttrs: Record = {}; - const ldKeys = ['ldName', 'desc']; + const ldKeys = ['desc', 'inst']; ldKeys.forEach(key => { ldAttrs[key] = getValue(inputs.find(i => i.label === key)!); }); @@ -110,7 +125,8 @@ export function editLDeviceWizard(element: Element): Wizard { element.getAttribute('ldName'), !ldNameIsAllowed(element), element.getAttribute('desc'), - element.getAttribute('inst') + element.getAttribute('inst'), + reservedInstLDevice(element) ), }, ]; diff --git a/packages/plugins/src/wizards/ln.ts b/packages/plugins/src/wizards/ln.ts index 0bf172882b..59722af218 100644 --- a/packages/plugins/src/wizards/ln.ts +++ b/packages/plugins/src/wizards/ln.ts @@ -12,14 +12,14 @@ import { import { cloneElement } from '@openscd/xml'; import { SimpleAction } from '@openscd/core/foundation/deprecated/editor.js'; -import { patterns } from './foundation/limits.js'; export function renderLNWizard( lnType: string | null, desc: string | null, prefix: string | null, lnClass: string | null, - inst: string | null + inst: string | null, + reservedInst: string[] ): TemplateResult[] { return [ html``, @@ -52,8 +51,10 @@ export function renderLNWizard( html``, ]; } @@ -66,19 +67,66 @@ function updateAction(element: Element): WizardActor { ldAttrs[key] = getValue(inputs.find(i => i.label === key)!); }); - if (ldKeys.some(key => ldAttrs[key] !== element.getAttribute(key))) { - const newElement = cloneElement(element, ldAttrs); - return [ - { - old: { element }, - new: { element: newElement }, - }, - ]; + if (!ldKeys.some(key => ldAttrs[key] !== element.getAttribute(key))) { + return []; + } + + const ldevice = element.closest('LDevice'); + if (ldevice) { + const newPrefix = ldAttrs['prefix'] || ''; + const newLnClass = ldAttrs['lnClass']; + const newInst = ldAttrs['inst']; + + const isDuplicate = Array.from( + ldevice.querySelectorAll(':scope > LN') + ).some( + ln => + ln !== element && + (ln.getAttribute('prefix') || '') === newPrefix && + ln.getAttribute('lnClass') === newLnClass && + ln.getAttribute('inst') === newInst + ); + + if (isDuplicate) { + return []; + } } - return []; + + const newElement = cloneElement(element, ldAttrs); + return [ + { + old: { element }, + new: { element: newElement }, + }, + ]; }; } +function reservedInstLN( + currentElement: Element, + prefixInput?: string +): string[] { + const ldevice = currentElement.closest('LDevice'); + if (!ldevice) return []; + + const currentLnClass = currentElement.getAttribute('lnClass'); + + const targetPrefix = + prefixInput !== undefined + ? prefixInput + : currentElement.getAttribute('prefix') || ''; + + const lnElements = Array.from(ldevice.querySelectorAll(':scope > LN')).filter( + ln => + ln !== currentElement && + (ln.getAttribute('prefix') || '') === targetPrefix && + ln.getAttribute('lnClass') === currentLnClass + ); + + return lnElements + .map(ln => ln.getAttribute('inst')) + .filter(inst => inst !== null) as string[]; +} export function editLNWizard(element: Element): Wizard { return [ @@ -95,7 +143,8 @@ export function editLNWizard(element: Element): Wizard { element.getAttribute('desc'), element.getAttribute('prefix'), element.getAttribute('lnClass'), - element.getAttribute('inst') + element.getAttribute('inst'), + reservedInstLN(element) ), }, ]; diff --git a/packages/plugins/src/wizards/ln0.ts b/packages/plugins/src/wizards/ln0.ts index ad06a7e1a6..1a77faa076 100644 --- a/packages/plugins/src/wizards/ln0.ts +++ b/packages/plugins/src/wizards/ln0.ts @@ -1,7 +1,9 @@ import { html, TemplateResult } from 'lit-element'; import { get } from 'lit-translate'; +import '@material/mwc-list/mwc-list-item'; import '@openscd/open-scd/src/wizard-textfield.js'; +import '@openscd/open-scd/src/wizard-select.js'; import { getValue, Wizard, @@ -14,20 +16,31 @@ import { cloneElement } from '@openscd/xml'; import { SimpleAction } from '@openscd/core/foundation/deprecated/editor.js'; import { patterns } from './foundation/limits.js'; +function getLNodeTypeOptions(element: Element): string[] { + const doc = element.ownerDocument; + const lNodeTypes = Array.from( + doc.querySelectorAll('DataTypeTemplates > LNodeType[lnClass="LLN0"]') + ); + return lNodeTypes.map(lnt => lnt.getAttribute('id')!).filter(id => id); +} + export function renderLN0Wizard( + lnodeTypeIds: string[], lnType: string | null, desc: string | null, lnClass: string | null, inst: string | null ): TemplateResult[] { return [ - html``, + >${lnodeTypeIds.map( + id => html`${id}` + )}`, html` + + + +
@@ -17,6 +29,18 @@ snapshots["access-point-container with LN Elements and all LN Classes displayed ` + + + +
@@ -43,6 +67,18 @@ snapshots["access-point-container with LN Elements and some LN Classes hidden lo ` + + + +
@@ -61,6 +97,18 @@ snapshots["access-point-container with LN Elements and all LN Classes hidden loo ` + + + +
diff --git a/packages/plugins/test/unit/editors/ied/__snapshots__/ldevice-container.test.snap.js b/packages/plugins/test/unit/editors/ied/__snapshots__/ldevice-container.test.snap.js index 9c399766b4..5dd245472c 100644 --- a/packages/plugins/test/unit/editors/ied/__snapshots__/ldevice-container.test.snap.js +++ b/packages/plugins/test/unit/editors/ied/__snapshots__/ldevice-container.test.snap.js @@ -5,6 +5,12 @@ snapshots["ldevice-container LDevice Element with LN Elements and all LN Element ` + + + + + + + + + + + + +
+ + + + +
+ + + + + +`; +/* end snapshot Wizards for SCL element AccessPoint edit AccessPoint looks like the latest snapshot */ + +snapshots["Wizards for SCL element AccessPoint remove AccessPoint with references looks like the latest snapshot"] = +` +
+
+

+ [accesspoint.wizard.title.references] +

+ + + + ServerAt + + + test>AP2 + + + +
+
+ + + + +
+`; +/* end snapshot Wizards for SCL element AccessPoint remove AccessPoint with references looks like the latest snapshot */ + diff --git a/packages/plugins/test/unit/wizards/__snapshots__/ied.test.snap.js b/packages/plugins/test/unit/wizards/__snapshots__/ied.test.snap.js index fd95e37b4e..e3570944f8 100644 --- a/packages/plugins/test/unit/wizards/__snapshots__/ied.test.snap.js +++ b/packages/plugins/test/unit/wizards/__snapshots__/ied.test.snap.js @@ -56,9 +56,9 @@ snapshots["Wizards for SCL element IED edit IED looks like the latest snapshot"] >
diff --git a/packages/plugins/test/unit/wizards/accesspoint.test.ts b/packages/plugins/test/unit/wizards/accesspoint.test.ts new file mode 100644 index 0000000000..c11e8a3abf --- /dev/null +++ b/packages/plugins/test/unit/wizards/accesspoint.test.ts @@ -0,0 +1,195 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import { + ComplexAction, + isSimple, +} from '@openscd/core/foundation/deprecated/editor.js'; +import '@openscd/open-scd/src/addons/Wizards.js'; +import { OscdWizards } from '@openscd/open-scd/src/addons/Wizards.js'; +import { WizardInputElement } from '@openscd/open-scd/src/foundation.js'; +import { WizardTextField } from '@openscd/open-scd/src/wizard-textfield.js'; + +import { + editAccessPointWizard, + removeAccessPointAndReferences, + removeAccessPointWizard, +} from '../../../src/wizards/accesspoint.js'; +import { updateNamingAttributeWithReferencesAction } from '../../../src/wizards/foundation/actions.js'; +import { + expectDeleteAction, + expectUpdateAction, + expectWizardNoUpdateAction, + fetchDoc, + setWizardTextFieldValue, +} from './test-support.js'; + +describe('Wizards for SCL element AccessPoint', () => { + let doc: XMLDocument; + let accessPoint: Element; + let element: OscdWizards; + let inputs: WizardInputElement[]; + + describe('edit AccessPoint', () => { + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); + accessPoint = doc.querySelector( + 'IED[name="IED3"] > AccessPoint[name="P1"]' + )!; + + element = await fixture( + html`` + ); + const wizard = editAccessPointWizard(accessPoint); + element.workflow.push(() => wizard); + await element.requestUpdate(); + inputs = Array.from(element.wizardUI.inputs); + }); + + it('contains a wizard-textfield with the current "name" value', async () => { + expect( + (inputs).find(textField => textField.label == 'name') + ?.value + ).to.be.equal(accessPoint.getAttribute('name')); + }); + + it('contains a wizard-textfield with the current "desc" value', async () => { + expect( + (inputs).find(textField => textField.label == 'desc') + ?.value + ).to.be.equal(accessPoint.getAttribute('desc') || ''); + }); + + it('update name should be updated in document', async function () { + await setWizardTextFieldValue(inputs[0], 'P2'); + + const complexAction = updateNamingAttributeWithReferencesAction( + accessPoint, + 'accesspoint.action.updateAccessPoint' + )(inputs, element.wizardUI); + expect(complexAction.length).to.equal(1); + expect(complexAction[0]).to.not.satisfy(isSimple); + + const simpleActions = (complexAction[0]).actions; + expect(simpleActions.length).to.equal(1); + + expectUpdateAction(simpleActions[0], 'AccessPoint', 'name', 'P1', 'P2'); + }); + + it('update name should be unique within IED', async function () { + accessPoint = doc.querySelector( + 'IED[name="IED3"] > AccessPoint[name="P2"]' + )!; + + element = await fixture( + html`` + ); + const wizard = editAccessPointWizard(accessPoint); + element.workflow.push(() => wizard); + await element.requestUpdate(); + inputs = Array.from(element.wizardUI.inputs); + + await setWizardTextFieldValue(inputs[0], 'P1'); + expect(inputs[0].checkValidity()).to.be.false; + }); + + it('update description should be updated in document', async function () { + await setWizardTextFieldValue( + inputs[1], + 'New description' + ); + + const complexAction = updateNamingAttributeWithReferencesAction( + accessPoint, + 'accesspoint.action.updateAccessPoint' + )(inputs, element.wizardUI); + expect(complexAction.length).to.equal(1); + expect(complexAction[0]).to.not.satisfy(isSimple); + + const simpleActions = (complexAction[0]).actions; + expect(simpleActions.length).to.equal(1); + + expectUpdateAction( + simpleActions[0], + 'AccessPoint', + 'desc', + null, + 'New description' + ); + }); + + it('when no fields changed there will be no update action', async function () { + expectWizardNoUpdateAction( + updateNamingAttributeWithReferencesAction( + accessPoint, + 'accesspoint.action.updateAccessPoint' + ), + element.wizardUI, + inputs + ); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }); + }); + + describe('remove AccessPoint', () => { + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/editors/minimalVirtualIED.scd'); + }); + describe('with references', () => { + beforeEach(async () => { + accessPoint = doc.querySelector( + 'IED[name="test"] > AccessPoint[name="AP1"]' + )!; + + element = await fixture( + html`` + ); + const wizard = removeAccessPointWizard(accessPoint); + element.workflow.push(() => wizard!); + await element.requestUpdate(); + inputs = Array.from(element.wizardUI.inputs); + }); + + it('should return a wizard when AccessPoint has references', async function () { + const wizard = removeAccessPointWizard(accessPoint); + expect(wizard!.length).to.eq(1); + expect(wizard![0]?.title).to.eq('[accesspoint.wizard.title.delete]'); + }); + + it('remove AccessPoint should return expected actions including references', async function () { + const complexAction = removeAccessPointAndReferences(accessPoint)( + inputs, + element.wizardUI + ); + + expect(complexAction.length).to.equal(1); + expect(complexAction[0]).to.not.satisfy(isSimple); + + const simpleActions = (complexAction[0]).actions; + expect(simpleActions.length).to.be.greaterThan(1); + + expectDeleteAction(simpleActions[0], 'AccessPoint'); + expectDeleteAction(simpleActions[1], 'ServerAt'); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }); + }); + + describe('without references', () => { + beforeEach(async () => { + accessPoint = doc.querySelector( + 'IED[name="test"] > AccessPoint[name="AP2"]' + )!; + }); + + it('should return null when AccessPoint has no references', async function () { + const wizard = removeAccessPointWizard(accessPoint); + expect(wizard).to.be.null; + }); + }); + }); +}); diff --git a/packages/plugins/test/unit/wizards/ldevice.test.ts b/packages/plugins/test/unit/wizards/ldevice.test.ts index 8946bd49fe..b688f2e57d 100644 --- a/packages/plugins/test/unit/wizards/ldevice.test.ts +++ b/packages/plugins/test/unit/wizards/ldevice.test.ts @@ -40,7 +40,7 @@ describe('Wizards for SCL element LDevice', () => { it('contains a wizard-textfield with a non-empty "inst" value', async () => { expect( - (inputs).find(textField => textField.label == 'ldInst') + (inputs).find(textField => textField.label == 'inst') ?.value ).to.be.equal(ldevice.getAttribute('inst')); }); diff --git a/packages/plugins/test/unit/wizards/ln.test.ts b/packages/plugins/test/unit/wizards/ln.test.ts index 784c7bec10..a4f98420b1 100644 --- a/packages/plugins/test/unit/wizards/ln.test.ts +++ b/packages/plugins/test/unit/wizards/ln.test.ts @@ -7,8 +7,6 @@ import { WizardInputElement } from '@openscd/open-scd/src/foundation.js'; import { fetchDoc, setWizardTextFieldValue } from './test-support.js'; import { WizardTextField } from '@openscd/open-scd/src/wizard-textfield.js'; - - describe('ln wizards', () => { let doc: XMLDocument; let element: OscdWizards; @@ -21,12 +19,7 @@ describe('ln wizards', () => { lnClass: 'LN-class', inst: '1', }; - const readonlyFields = [ - 'lnType', - 'prefix', - 'lnClass', - 'inst' - ]; + const readonlyFields = ['lnType', 'lnClass']; const ln = ( new DOMParser().parseFromString( @@ -52,16 +45,16 @@ describe('ln wizards', () => { it(`contains a wizard-textfield with a non-empty "${key}" value`, async () => { expect( (inputs).find( - (textField) => textField.label === key + textField => textField.label === key )?.value ).to.equal(value); }); }); - readonlyFields.forEach((field) => { + readonlyFields.forEach(field => { it(`is a readonly field ${field}`, async () => { const input = (inputs).find( - (textField) => textField.label === field + textField => textField.label === field ) as WizardTextField; expect(input.readOnly).to.be.true; diff --git a/packages/plugins/test/unit/wizards/ln0.test.ts b/packages/plugins/test/unit/wizards/ln0.test.ts index b5e605059c..495878e119 100644 --- a/packages/plugins/test/unit/wizards/ln0.test.ts +++ b/packages/plugins/test/unit/wizards/ln0.test.ts @@ -4,38 +4,23 @@ import { OscdWizards } from '@openscd/open-scd/src/addons/Wizards'; import { editLN0Wizard } from '../../../src/wizards/ln0.js'; import { WizardInputElement } from '@openscd/open-scd/src/foundation.js'; -import { fetchDoc, setWizardTextFieldValue } from './test-support.js'; +import { fetchDoc } from './test-support.js'; import { WizardTextField } from '@openscd/open-scd/src/wizard-textfield.js'; - - describe('ln0 wizards', () => { let doc: XMLDocument; let element: OscdWizards; let inputs: WizardInputElement[]; + let ln: Element; - const values = { - lnType: 'LN0-type', - desc: 'LN0-description', - lnClass: 'LN0-class', - inst: '1', - }; - const readonlyFields = [ - 'lnType', - 'lnClass', - 'inst' - ]; - - const ln = ( - new DOMParser().parseFromString( - ``, - 'application/xml' - ).documentElement - ); + const readonlyFields = ['lnClass', 'inst']; beforeEach(async () => { doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); + ln = doc.querySelector('LN0')!; + expect(ln).to.exist; + element = await fixture( html`` ); @@ -46,20 +31,36 @@ describe('ln0 wizards', () => { inputs = Array.from(element.wizardUI.inputs); }); - Object.entries(values).forEach(([key, value]) => { - it(`contains a wizard-textfield with a non-empty "${key}" value`, async () => { - expect( - (inputs).find( - (textField) => textField.label === key - )?.value - ).to.equal(value); + it('contains a wizard-select for lnType with available LNodeTypes', async () => { + const lnTypeInput = (inputs).find( + input => input.label === 'lnType' + ); + expect(lnTypeInput).to.exist; + expect(lnTypeInput?.tagName).to.equal('WIZARD-SELECT'); + }); + + it('lnType select contains LNodeType options with lnClass="LLN0"', async () => { + const lnTypeSelect = (inputs).find( + input => input.label === 'lnType' + ); + const options = lnTypeSelect?.querySelectorAll('mwc-list-item'); + expect(options).to.have.length.greaterThan(0); + + const lln0Types = doc.querySelectorAll('LNodeType[lnClass="LLN0"]'); + expect(options).to.have.lengthOf(lln0Types.length); + }); + + it('contains required fields', async () => { + ['lnType', 'lnClass'].forEach(field => { + const input = (inputs).find(i => i.label === field); + expect(input).to.exist; }); }); - readonlyFields.forEach((field) => { + readonlyFields.forEach(field => { it(`is a readonly field ${field}`, async () => { const input = (inputs).find( - (textField) => textField.label === field + textField => textField.label === field ) as WizardTextField; expect(input.readOnly).to.be.true;