From 432851d94767939272e4f997ab30c92acf1ca340 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Mon, 23 May 2022 21:16:31 +0200 Subject: [PATCH 01/11] refactor(foundation): change menu action API --- src/wizards/ied.ts | 73 +++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/src/wizards/ied.ts b/src/wizards/ied.ts index 8fafe5870d..92f25aaa5f 100644 --- a/src/wizards/ied.ts +++ b/src/wizards/ied.ts @@ -21,16 +21,17 @@ import { } from '../foundation.js'; import { patterns } from './foundation/limits.js'; -import { updateNamingAttributeWithReferencesAction } from "./foundation/actions.js"; -import { deleteReferences } from "./foundation/references.js"; -import { emptyInputsDeleteActions } from "../foundation/ied.js"; +import { updateNamingAttributeWithReferencesAction } from './foundation/actions.js'; +import { deleteReferences } from './foundation/references.js'; +import { emptyInputsDeleteActions } from '../foundation/ied.js'; -const iedNamePattern = "[A-Za-z][0-9A-Za-z_]{0,2}|" + - "[A-Za-z][0-9A-Za-z_]{4,63}|" + - "[A-MO-Za-z][0-9A-Za-z_]{3}|" + - "N[0-9A-Za-np-z_][0-9A-Za-z_]{2}|" + - "No[0-9A-Za-mo-z_][0-9A-Za-z_]|" + - "Non[0-9A-Za-df-z_]"; +const iedNamePattern = + '[A-Za-z][0-9A-Za-z_]{0,2}|' + + '[A-Za-z][0-9A-Za-z_]{4,63}|' + + '[A-MO-Za-z][0-9A-Za-z_]{3}|' + + 'N[0-9A-Za-np-z_][0-9A-Za-z_]{2}|' + + 'No[0-9A-Za-mo-z_][0-9A-Za-z_]|' + + 'Non[0-9A-Za-df-z_]'; export function renderIEDWizard( name: string | null, @@ -59,26 +60,26 @@ export function renderIEDWizard( } function renderIEDReferencesWizard(references: Delete[]): TemplateResult[] { - return [html ` -
-

${translate('ied.wizard.title.references')}

- - ${references.map(reference => { - const oldElement = reference.old.element - return html ` - - ${oldElement.tagName} - ${identity(reference.old.element)} - `; - })} - -
`]; + return [ + html`
+

${translate('ied.wizard.title.references')}

+ + ${references.map(reference => { + const oldElement = reference.old.element; + return html` + ${oldElement.tagName} + ${identity(reference.old.element)} + `; + })} + +
`, + ]; } export function reservedNamesIED(currentElement: Element): string[] { - return Array.from( - currentElement.parentNode!.querySelectorAll('IED') - ) + return Array.from(currentElement.parentNode!.querySelectorAll('IED')) .filter(isPublic) .map(ied => ied.getAttribute('name') ?? '') .filter(name => name !== currentElement.getAttribute('name')); @@ -92,21 +93,24 @@ export function removeIEDAndReferences(element: Element): WizardActor { // Get Delete Actions for other elements that also need to be removed const referencesDeleteActions = deleteReferences(element); // Use the ExtRef Elements to check if after removing the ExtRef there are empty Inputs that can also be removed. - const extRefsDeleteActions = referencesDeleteActions - .filter(deleteAction => (deleteAction.old.element).tagName === 'ExtRef') - const inputsDeleteActions = emptyInputsDeleteActions(extRefsDeleteActions) + const extRefsDeleteActions = referencesDeleteActions.filter( + deleteAction => (deleteAction.old.element).tagName === 'ExtRef' + ); + const inputsDeleteActions = emptyInputsDeleteActions(extRefsDeleteActions); // Create Complex Action to remove IED and all references. const name = element.getAttribute('name') ?? 'Unknown'; const complexAction: ComplexAction = { actions: [], - title: get('ied.action.deleteied', {name}), + title: get('ied.action.deleteied', { name }), }; - complexAction.actions.push({ old: { parent: element.parentElement!, element } }); + complexAction.actions.push({ + old: { parent: element.parentElement!, element }, + }); complexAction.actions.push(...referencesDeleteActions); complexAction.actions.push(...inputsDeleteActions); return [complexAction]; - } + }; } export function removeIEDWizard(element: Element): Wizard | null { @@ -157,7 +161,10 @@ export function editIEDWizard(element: Element): Wizard { primary: { icon: 'edit', label: get('save'), - action: updateNamingAttributeWithReferencesAction(element, 'ied.action.updateied'), + action: updateNamingAttributeWithReferencesAction( + element, + 'ied.action.updateied' + ), }, content: renderIEDWizard( element.getAttribute('name'), From 87651a3908f746ee8cf1ee7c62bd74ea4eabbc75 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Sun, 22 May 2022 21:05:28 +0200 Subject: [PATCH 02/11] feat(wizards/lnode): add create wizards --- src/wizards/lnode.ts | 107 +++++ .../wizards/__snapshots__/lnode.test.snap.js | 453 ++++++++++++++++++ test/unit/wizards/lnode.test.ts | 166 +++++++ 3 files changed, 726 insertions(+) create mode 100644 test/unit/wizards/__snapshots__/lnode.test.snap.js create mode 100644 test/unit/wizards/lnode.test.ts diff --git a/src/wizards/lnode.ts b/src/wizards/lnode.ts index 130ae7695b..25ad427089 100644 --- a/src/wizards/lnode.ts +++ b/src/wizards/lnode.ts @@ -11,6 +11,7 @@ import { MultiSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; import '../filtered-list.js'; import { + Create, createElement, EditorAction, getChildElementsByTagName, @@ -23,6 +24,112 @@ import { WizardInputElement, } from '../foundation.js'; +function getUniqueLnInst(parent: Element, lnClass: string): number { + const lnInsts = Array.from( + parent.querySelectorAll(`LNode[lnClass="${lnClass}"]`) + ) + .map(lNode => Number.parseInt(lNode.getAttribute('lnInst')!)) + .sort((a, b) => a - b); + + if (lnInsts.length === 0) return 1; + + for (let i = 1; i < 99; i++) { + if (lnInsts[i - 1] !== i) return i; + } + + return NaN; +} + +function createLNodeAction(parent: Element): WizardActor { + return ( + inputs: WizardInputElement[], + wizard: Element, + list?: List | null + ): EditorAction[] => { + const selectedLNodeTypes = list!.items + .filter(item => item.selected) + .map(item => item.value) + .map(identity => { + return parent.ownerDocument.querySelector( + selector('LNodeType', identity) + ); + }) + .filter(item => item !== null); + + const clonedParent = parent.cloneNode(true); //for multiple selection of same lnClass + + const createActions: Create[] = selectedLNodeTypes + .map(selectedLNodeType => { + const lnClass = selectedLNodeType.getAttribute('lnClass'); + if (!lnClass) return null; + + const uniqueLnInst = getUniqueLnInst(clonedParent, lnClass); + if (isNaN(uniqueLnInst) || uniqueLnInst > 99) return null; + + const existLLN0 = + clonedParent.querySelector('LNode[lnClass="LLN0"]') !== null; + if (lnClass === 'LLN0' && existLLN0) return null; + + const lnInst = lnClass === 'LLN0' ? '' : `${uniqueLnInst}`; + + const element = createElement(parent.ownerDocument, 'LNode', { + iedName: 'None', + ldInst: null, + prefix: null, + lnClass, + lnInst, + lnType: selectedLNodeType.getAttribute('id')!, + }); + + clonedParent.appendChild(element); //for multiple selection of same lnClass + + return { new: { parent, element } }; + }) + .filter(action => action); + + return createActions; + }; +} + +/** @returns a Wizard for creating `LNode` instances within parent. */ +export function createLNodeWizard(parent: Element): Wizard { + const lNodeTypes = Array.from( + parent.ownerDocument.querySelectorAll('LNodeType') + ); + + return [ + { + title: get('wizard.title.add', { tagName: 'LNode' }), + primary: { + icon: 'save', + label: get('save'), + action: createLNodeAction(parent), + }, + content: [ + html`${lNodeTypes.map(lNodeType => { + const isDesabled = + (lNodeType.getAttribute('lnClass') === 'LLN0' && + parent.querySelector('LNode[lnClass="LLN0"]') !== null) || + (lNodeType.getAttribute('lnClass') === 'LPHD' && + parent.querySelector('LNode[lnClass="LPHD"]') !== null); + + return html`${lNodeType.getAttribute('lnClass')}${identity(lNodeType)}`; + })}`, + ], + }, + ]; +} + /** Description of a `ListItem` representing an `IED` or `LN[0]` */ interface ItemDescription { selected: boolean; diff --git a/test/unit/wizards/__snapshots__/lnode.test.snap.js b/test/unit/wizards/__snapshots__/lnode.test.snap.js new file mode 100644 index 0000000000..b20f2670c1 --- /dev/null +++ b/test/unit/wizards/__snapshots__/lnode.test.snap.js @@ -0,0 +1,453 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for LNode element contain a create wizard that with existing LLN0 and LPHD instances looks like the latest snapshot"] = +` +
+ + + + LLN0 + + + #Dummy.LLN0 + + + + + LPHD + + + #Dummy.LPHD1 + + + + + XCBR + + + #Dummy.XCBR1 + + + + + CSWI + + + #Dummy.CSWI + + + + + CILO + + + #Dummy.CILO + + + + + CSWI + + + #Dummy.CSWIwithoutCtlModel + + + + + XSWI + + + #Dummy.XSWI1 + + + + + GGIO + + + #Dummy.GGIO1 + + + +
+ + + + +
+`; +/* end snapshot Wizards for LNode element contain a create wizard that with existing LLN0 and LPHD instances looks like the latest snapshot */ + +snapshots["Wizards for LNode element contain a create wizard that with existing LLN0 but missing LPHD instances looks like the latest snapshot"] = +` +
+ + + + LLN0 + + + #Dummy.LLN0 + + + + + LPHD + + + #Dummy.LPHD1 + + + + + XCBR + + + #Dummy.XCBR1 + + + + + CSWI + + + #Dummy.CSWI + + + + + CILO + + + #Dummy.CILO + + + + + CSWI + + + #Dummy.CSWIwithoutCtlModel + + + + + XSWI + + + #Dummy.XSWI1 + + + + + GGIO + + + #Dummy.GGIO1 + + + +
+ + + + +
+`; +/* end snapshot Wizards for LNode element contain a create wizard that with existing LLN0 but missing LPHD instances looks like the latest snapshot */ + +snapshots["Wizards for LNode element contain a create wizard that with missing LLN0 and LPHD instances looks like the latest snapshot"] = +` +
+ + + + LLN0 + + + #Dummy.LLN0 + + + + + LPHD + + + #Dummy.LPHD1 + + + + + XCBR + + + #Dummy.XCBR1 + + + + + CSWI + + + #Dummy.CSWI + + + + + CILO + + + #Dummy.CILO + + + + + CSWI + + + #Dummy.CSWIwithoutCtlModel + + + + + XSWI + + + #Dummy.XSWI1 + + + + + GGIO + + + #Dummy.GGIO1 + + + +
+ + + + +
+`; +/* end snapshot Wizards for LNode element contain a create wizard that with missing LLN0 and LPHD instances looks like the latest snapshot */ + diff --git a/test/unit/wizards/lnode.test.ts b/test/unit/wizards/lnode.test.ts new file mode 100644 index 0000000000..78c488e72e --- /dev/null +++ b/test/unit/wizards/lnode.test.ts @@ -0,0 +1,166 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../mock-wizard-editor.js'; + +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; + +import { createLNodeWizard } from '../../../src/wizards/lnode.js'; +import { Create, isCreate } from '../../../src/foundation.js'; + +describe('Wizards for LNode element', () => { + let element: MockWizardEditor; + let doc: Document; + + let actionEvent: SinonSpy; + + beforeEach(async () => { + doc = await fetch('/test/testfiles/lnodewizard.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + element = ( + await fixture(html``) + ); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('contain a create wizard that', () => { + describe('with existing LLN0 and LPHD instances', () => { + beforeEach(async () => { + const wizard = createLNodeWizard( + doc.querySelector('Function[name="parentFunction"]')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + }); + + it('looks like the latest snapshot', async () => + await expect(element.wizardUI.dialog).to.equalSnapshot()); + }); + + describe('with existing LLN0 but missing LPHD instances', () => { + beforeEach(async () => { + const wizard = createLNodeWizard( + doc.querySelector('SubFunction[name="circuitBreaker"]')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + }); + + it('looks like the latest snapshot', async () => + await expect(element.wizardUI.dialog).to.equalSnapshot()); + }); + + describe('with missing LLN0 and LPHD instances', () => { + beforeEach(async () => { + const wizard = createLNodeWizard( + doc.querySelector('SubFunction[name="disconnector"]')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + }); + + it('looks like the latest snapshot', async () => + await expect(element.wizardUI.dialog).to.equalSnapshot()); + }); + + describe('has a primary action that', () => { + let primaryAction: HTMLElement; + let listItems: ListItemBase[]; + + beforeEach(async () => { + const wizard = createLNodeWizard( + doc.querySelector('SubFunction[name="disconnector"]')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + listItems = Array.from( + element.wizardUI!.dialog!.querySelectorAll( + 'mwc-check-list-item' + ) + ); + }); + + it('triggers Create action for all selected LNodeType', async () => { + listItems[1].selected = true; + listItems[2].selected = true; + listItems[3].selected = true; + + await primaryAction.click(); + + expect(actionEvent).to.have.be.calledThrice; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isCreate); + expect(actionEvent.args[1][0].detail.action).to.satisfy(isCreate); + expect(actionEvent.args[2][0].detail.action).to.satisfy(isCreate); + }); + + it('does set iedName, lnCalss, lnInst and lnType', async () => { + listItems[4].selected = true; + + await primaryAction.click(); + + expect(actionEvent).to.have.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action.new.element).to.have.attribute('iedName', 'None'); + expect(action.new.element).to.have.attribute('lnClass', 'CILO'); + expect(action.new.element).to.have.attribute('lnInst', '1'); + expect(action.new.element).to.have.attribute('lnType', 'Dummy.CILO'); + }); + + it('does not set ldInst and prefix', async () => { + listItems[4].selected = true; + + await primaryAction.click(); + + expect(actionEvent).to.have.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action.new.element).to.not.have.attribute('ldInst'); + expect(action.new.element).to.not.have.attribute('prefix'); + }); + + it('makes sure that lnInst is unique in case lnClass is existing already', async () => { + listItems[4].selected = true; + + await primaryAction.click(); + + expect(actionEvent).to.have.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action.new.element).to.have.attribute('lnInst', '1'); + }); + + it('makes sure that lnInst is unique if several LNodeType with same lnClass are selected', async () => { + listItems[3].selected = true; + listItems[5].selected = true; + + await primaryAction.click(); + + expect(actionEvent).to.have.be.calledTwice; + const action1 = actionEvent.args[0][0].detail.action; + const action2 = actionEvent.args[1][0].detail.action; + expect(action1.new.element).to.have.attribute('lnInst', '1'); + expect(action2.new.element).to.have.attribute('lnInst', '2'); + }); + + it('does add empty string to LNode with lnClass LLN0', async () => { + listItems[0].selected = true; + + await primaryAction.click(); + + expect(actionEvent).to.have.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action.new.element).to.have.attribute('lnInst', ''); + }); + }); + }); +}); From 7fd72c0513b31417cd1221cb7ca5b33dd25f80a7 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Sun, 22 May 2022 21:06:34 +0200 Subject: [PATCH 03/11] feat(wizards/wizard-library): add LNode create wizard to library --- src/wizards/wizard-library.ts | 4 ++-- test/testfiles/lnodewizard.scd | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index 1139bbde16..c450530caa 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -7,7 +7,7 @@ import { } from './conductingequipment.js'; import { editConnectivityNodeWizard } from './connectivitynode.js'; import { createFCDAsWizard } from './fcda.js'; -import { lNodeWizard } from './lnode.js'; +import { createLNodeWizard, lNodeWizard } from './lnode.js'; import { editOptFieldsWizard } from './optfields.js'; import { createSubstationWizard, substationEditWizard } from './substation.js'; import { editTerminalWizard } from './terminal.js'; @@ -314,7 +314,7 @@ export const wizards: Record< }, LNode: { edit: lNodeWizard, - create: lNodeWizard, + create: createLNodeWizard, }, LNodeType: { edit: emptyWizard, diff --git a/test/testfiles/lnodewizard.scd b/test/testfiles/lnodewizard.scd index 75126454f7..19cd4f222a 100644 --- a/test/testfiles/lnodewizard.scd +++ b/test/testfiles/lnodewizard.scd @@ -28,6 +28,17 @@ + + + + + + + + + + + @@ -589,4 +600,4 @@ process - \ No newline at end of file + From 424c79168d401999f1f5de04508445d33009f0ea Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Sun, 22 May 2022 21:07:32 +0200 Subject: [PATCH 04/11] test(editors/substation): test LNode instatiotion with function type editors --- .../eq-function-wizarding-editing.test.ts | 46 +++++++++++++- ...-function-editor-wizarding-editing.test.ts | 46 +++++++++++++- .../substation/function-editor.test.ts | 46 +++++++++++++- .../substation/sub-function-editor.test.ts | 62 ++++++++++++++++++- 4 files changed, 196 insertions(+), 4 deletions(-) diff --git a/test/integration/editors/substation/eq-function-wizarding-editing.test.ts b/test/integration/editors/substation/eq-function-wizarding-editing.test.ts index 1e7a149828..347d1a1aa9 100644 --- a/test/integration/editors/substation/eq-function-wizarding-editing.test.ts +++ b/test/integration/editors/substation/eq-function-wizarding-editing.test.ts @@ -3,6 +3,8 @@ import { fixture, html, expect } 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 '../../../../src/editors/substation/eq-function-editor.js'; import { EqFunctionEditor } from '../../../../src/editors/substation/eq-function-editor.js'; import { WizardTextField } from '../../../../src/wizard-textfield.js'; @@ -12,6 +14,8 @@ describe('eq-function-editor wizarding editing integration', () => { let parent: MockWizardEditor; let element: EqFunctionEditor | null; + let primaryAction: HTMLElement; + beforeEach(async () => { doc = await fetch('/test/testfiles/zeroline/functions.scd') .then(response => response.text()) @@ -33,7 +37,6 @@ describe('eq-function-editor wizarding editing integration', () => { describe('open create wizard for element EqSubFunction', () => { let nameField: WizardTextField; - let primaryAction: HTMLElement; beforeEach(async () => { (( @@ -152,6 +155,47 @@ describe('eq-function-editor wizarding editing integration', () => { }); }); + describe('open create wizard for element LNode', () => { + let listItems: ListItemBase[]; + + beforeEach(async () => { + (( + element?.shadowRoot?.querySelector('mwc-list-item[value="LNode"]') + )).click(); + await parent.updateComplete; + + listItems = Array.from( + parent.wizardUI!.dialog!.querySelectorAll( + 'mwc-check-list-item' + ) + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('add selected LNode instances to SubFcuntion parent', async () => { + listItems[3].selected = true; + listItems[5].selected = true; + + await primaryAction.click(); + + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] > EqFunction > LNode[iedName="None"][lnClass="CSWI"][lnInst="1"]' + ) + ).to.exist; + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] > EqFunction > LNode[iedName="None"][lnClass="CSWI"][lnInst="2"]' + ) + ).to.exist; + }); + }); + describe('has a delete icon button that', () => { let deleteButton: HTMLElement; diff --git a/test/integration/editors/substation/eq-sub-function-editor-wizarding-editing.test.ts b/test/integration/editors/substation/eq-sub-function-editor-wizarding-editing.test.ts index 7f82565a9d..684a40e733 100644 --- a/test/integration/editors/substation/eq-sub-function-editor-wizarding-editing.test.ts +++ b/test/integration/editors/substation/eq-sub-function-editor-wizarding-editing.test.ts @@ -3,6 +3,8 @@ import { fixture, html, expect } 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 '../../../../src/editors/substation/eq-sub-function-editor.js'; import { EqSubFunctionEditor } from '../../../../src/editors/substation/eq-sub-function-editor.js'; import { WizardTextField } from '../../../../src/wizard-textfield.js'; @@ -12,6 +14,8 @@ describe('eq-sub-function-editor wizarding editing integration', () => { let parent: MockWizardEditor; let element: EqSubFunctionEditor | null; + let primaryAction: HTMLElement; + beforeEach(async () => { doc = await fetch('/test/testfiles/zeroline/functions.scd') .then(response => response.text()) @@ -33,7 +37,6 @@ describe('eq-sub-function-editor wizarding editing integration', () => { describe('open create wizard for element EqSubFunction', () => { let nameField: WizardTextField; - let primaryAction: HTMLElement; beforeEach(async () => { (( @@ -152,6 +155,47 @@ describe('eq-sub-function-editor wizarding editing integration', () => { }); }); + describe('open create wizard for element LNode', () => { + let listItems: ListItemBase[]; + + beforeEach(async () => { + (( + element?.shadowRoot?.querySelector('mwc-list-item[value="LNode"]') + )).click(); + await parent.updateComplete; + + listItems = Array.from( + parent.wizardUI!.dialog!.querySelectorAll( + 'mwc-check-list-item' + ) + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('add selected LNode instances to SubFcuntion parent', async () => { + listItems[3].selected = true; + listItems[5].selected = true; + + await primaryAction.click(); + + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] EqSubFunction > LNode[iedName="None"][lnClass="CSWI"][lnInst="1"]' + ) + ).to.exist; + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] EqSubFunction > LNode[iedName="None"][lnClass="CSWI"][lnInst="2"]' + ) + ).to.exist; + }); + }); + describe('has a delete icon button that', () => { let deleteButton: HTMLElement; diff --git a/test/integration/editors/substation/function-editor.test.ts b/test/integration/editors/substation/function-editor.test.ts index b81172a1ad..193cbc33f7 100644 --- a/test/integration/editors/substation/function-editor.test.ts +++ b/test/integration/editors/substation/function-editor.test.ts @@ -3,6 +3,8 @@ import { fixture, html, expect } 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 '../../../../src/editors/substation/function-editor.js'; import { FunctionEditor } from '../../../../src/editors/substation/function-editor.js'; import { WizardTextField } from '../../../../src/wizard-textfield.js'; @@ -12,6 +14,8 @@ describe('function-editor wizarding editing integration', () => { let parent: MockWizardEditor; let element: FunctionEditor | null; + let primaryAction: HTMLElement; + beforeEach(async () => { doc = await fetch('/test/testfiles/zeroline/functions.scd') .then(response => response.text()) @@ -32,7 +36,6 @@ describe('function-editor wizarding editing integration', () => { describe('open create wizard for element SubFunction', () => { let nameField: WizardTextField; - let primaryAction: HTMLElement; beforeEach(async () => { (( @@ -147,6 +150,47 @@ describe('function-editor wizarding editing integration', () => { }); }); + describe('open create wizard for element LNode', () => { + let listItems: ListItemBase[]; + + beforeEach(async () => { + (( + element?.shadowRoot?.querySelector('mwc-list-item[value="LNode"]') + )).click(); + await parent.updateComplete; + + listItems = Array.from( + parent.wizardUI!.dialog!.querySelectorAll( + 'mwc-check-list-item' + ) + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('add selected LNode instances to Function parent', async () => { + listItems[3].selected = true; + listItems[5].selected = true; + + await primaryAction.click(); + + expect( + doc.querySelector( + 'Function > LNode[iedName="None"][lnClass="CSWI"][lnInst="1"]' + ) + ).to.exist; + expect( + doc.querySelector( + 'Function > LNode[iedName="None"][lnClass="CSWI"][lnInst="2"]' + ) + ).to.exist; + }); + }); + describe('has a delete icon button that', () => { let deleteButton: HTMLElement; diff --git a/test/integration/editors/substation/sub-function-editor.test.ts b/test/integration/editors/substation/sub-function-editor.test.ts index 6c04852316..32dec44aba 100644 --- a/test/integration/editors/substation/sub-function-editor.test.ts +++ b/test/integration/editors/substation/sub-function-editor.test.ts @@ -6,12 +6,15 @@ import { MockWizardEditor } from '../../../mock-wizard-editor.js'; import '../../../../src/editors/substation/sub-function-editor.js'; import { SubFunctionEditor } from '../../../../src/editors/substation/sub-function-editor.js'; import { WizardTextField } from '../../../../src/wizard-textfield.js'; +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; describe('sub-function-editor wizarding editing integration', () => { let doc: XMLDocument; let parent: MockWizardEditor; let element: SubFunctionEditor | null; + let primaryAction: HTMLElement; + beforeEach(async () => { doc = await fetch('/test/testfiles/zeroline/functions.scd') .then(response => response.text()) @@ -33,7 +36,6 @@ describe('sub-function-editor wizarding editing integration', () => { describe('open create wizard for element SubFunction', () => { let nameField: WizardTextField; - let primaryAction: HTMLElement; beforeEach(async () => { (( @@ -150,6 +152,64 @@ describe('sub-function-editor wizarding editing integration', () => { }); }); + describe('open create wizard for element LNode', () => { + let listItems: ListItemBase[]; + + beforeEach(async () => { + doc = await fetch('/test/testfiles/zeroline/functions.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html` SubFunction' + )} + >` + ) + ); + + element = parent.querySelector('sub-function-editor'); + + (( + element?.shadowRoot?.querySelector('mwc-list-item[value="LNode"]') + )).click(); + await parent.updateComplete; + + listItems = Array.from( + parent.wizardUI!.dialog!.querySelectorAll( + 'mwc-check-list-item' + ) + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('add selected LNode instances to SubFcuntion parent', async () => { + listItems[3].selected = true; + listItems[5].selected = true; + + await primaryAction.click(); + + expect( + doc.querySelector( + 'Function[name="voltLvName"] > SubFunction > LNode[iedName="None"][lnClass="CSWI"][lnInst="1"]' + ) + ).to.exist; + expect( + doc.querySelector( + 'Function[name="voltLvName"] > SubFunction > LNode[iedName="None"][lnClass="CSWI"][lnInst="2"]' + ) + ).to.exist; + }); + }); + describe('has a delete icon button that', () => { let deleteButton: HTMLElement; From e515db4ae44530a99d757a717c4914ffe462396c Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Mon, 23 May 2022 23:36:50 +0200 Subject: [PATCH 05/11] refactor(wizards/lnode): combine reference type and instace type wizard --- src/translations/de.ts | 14 +- src/translations/en.ts | 14 +- src/wizards/lnode.ts | 71 +++- src/wizards/wizard-library.ts | 2 +- .../wizards/__snapshots__/lnode.test.snap.js | 352 +++++++++++++++++- test/unit/wizards/lnode.test.ts | 38 +- 6 files changed, 446 insertions(+), 45 deletions(-) diff --git a/src/translations/de.ts b/src/translations/de.ts index ce8fe8653b..9ea5ad1774 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -324,11 +324,11 @@ export const de: Translations = { none: 'Keine Verbindung vorhanden', publisherGoose: { title: 'GOOSE-Publizierer', - subscriberTitle: 'Verbunden mit {{ selected }}' + subscriberTitle: 'Verbunden mit {{ selected }}', }, subscriberGoose: { title: 'IED-Publizierer', - publisherTitle: 'GOOSE(s) verbunden mit {{selected}}' + publisherTitle: 'GOOSE(s) verbunden mit {{selected}}', }, subscriber: { subscribed: 'Verbunden', @@ -338,9 +338,9 @@ export const de: Translations = { noIedSelected: 'Keine IED ausgewählt', }, view: { - publisherView: "Zeigt verbundene IED(s) der ausgewählten GOOSE", - subscriberView: "Zeigt verbundene GOOSE(s) des ausgewählten IED" - } + publisherView: 'Zeigt verbundene IED(s) der ausgewählten GOOSE', + subscriberView: 'Zeigt verbundene GOOSE(s) des ausgewählten IED', + }, }, sampledvalues: { none: 'Keine Verbindung vorhanden', @@ -460,8 +460,12 @@ export const de: Translations = { selectIEDs: 'Auswahl IEDs', selectLDs: 'Auswahl logische Geräte', selectLNs: 'Auswahl logische Knoten', + selectLNodeTypes: 'Auswahl logische Knoten Type', }, placeholder: 'Bitte laden Sie eine SCL-Datei, die IED-Elemente enthält.', + uniquewarning: 'Logische Knoten Klasse existiert bereits', + reference: 'Reference auf bestehenden logischen Knoten erstellen', + instance: 'Referenz auf logischen Knoten Typ erstellen', }, tooltip: 'Referenz zu logischen Knoten erstellen', }, diff --git a/src/translations/en.ts b/src/translations/en.ts index 02a695c372..30f0e5f5ce 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -321,11 +321,11 @@ export const en = { none: 'None', publisherGoose: { title: 'GOOSE Publisher', - subscriberTitle: 'Subscriber of {{ selected }}' + subscriberTitle: 'Subscriber of {{ selected }}', }, subscriberGoose: { title: 'GOOSE Subscriber', - publisherTitle: 'GOOSE(s) subscribed by {{selected}}' + publisherTitle: 'GOOSE(s) subscribed by {{selected}}', }, subscriber: { subscribed: 'Subscribed', @@ -335,9 +335,9 @@ export const en = { noIedSelected: 'No IED selected', }, view: { - publisherView: "Show subscriber IED(s) per selected GOOSE", - subscriberView: "Show subscribed GOOSE publisher for selected IED" - } + publisherView: 'Show subscriber IED(s) per selected GOOSE', + subscriberView: 'Show subscribed GOOSE publisher for selected IED', + }, }, sampledvalues: { none: 'none', @@ -457,8 +457,12 @@ export const en = { selectIEDs: 'Select IEDs', selectLDs: 'Select logical devices', selectLNs: 'Select logical nodes', + selectLNodeTypes: 'Select logical node types', }, placeholder: 'Please load an SCL file that contains IED elements.', + uniquewarning: 'Logical node class already exist', + reference: 'Add reference to existing logical node', + instance: 'Add reference to logical node type', }, tooltip: 'Create logical nodes reference', }, diff --git a/src/wizards/lnode.ts b/src/wizards/lnode.ts index 25ad427089..37dc6660ca 100644 --- a/src/wizards/lnode.ts +++ b/src/wizards/lnode.ts @@ -17,11 +17,13 @@ import { getChildElementsByTagName, identity, isPublic, + newWizardEvent, referencePath, selector, Wizard, WizardActor, WizardInputElement, + WizardMenuActor, } from '../foundation.js'; function getUniqueLnInst(parent: Element, lnClass: string): number { @@ -91,15 +93,28 @@ function createLNodeAction(parent: Element): WizardActor { }; } -/** @returns a Wizard for creating `LNode` instances within parent. */ -export function createLNodeWizard(parent: Element): Wizard { +function openLNodeReferenceWizard(parent: Element): WizardMenuActor { + return (wizard: Element): void => { + wizard.dispatchEvent(newWizardEvent()); + wizard.dispatchEvent(newWizardEvent(lNodeReferenceWizard(parent))); + }; +} + +function lNodeInstanceWizard(parent: Element): Wizard { const lNodeTypes = Array.from( parent.ownerDocument.querySelectorAll('LNodeType') ); return [ { - title: get('wizard.title.add', { tagName: 'LNode' }), + title: get('lnode.wizard.title.selectLNodeTypes'), + menuActions: [ + { + icon: '', + label: get('lnode.wizard.reference'), + action: openLNodeReferenceWizard(parent), + }, + ], primary: { icon: 'save', label: get('save'), @@ -110,9 +125,13 @@ export function createLNodeWizard(parent: Element): Wizard { >${lNodeTypes.map(lNodeType => { const isDesabled = (lNodeType.getAttribute('lnClass') === 'LLN0' && - parent.querySelector('LNode[lnClass="LLN0"]') !== null) || + getChildElementsByTagName(parent, 'LNode').some( + lnode => lnode.getAttribute('lnClass') === 'LLN0' + )) || (lNodeType.getAttribute('lnClass') === 'LPHD' && - parent.querySelector('LNode[lnClass="LPHD"]') !== null); + getChildElementsByTagName(parent, 'LNode').some( + lnode => lnode.getAttribute('lnClass') === 'LPHD' + )); return html`${lNodeType.getAttribute('lnClass')}${identity(lNodeType)}${isDesabled + ? get('lnode.wizard.uniquewarning') + : identity(lNodeType)}`; })}`; } -/** @returns a Wizard for editing `element`'s `LNode` children. */ -export function lNodeWizard(element: Element): Wizard { +function openLNodeInstanceWizard(parent: Element): WizardMenuActor { + return (wizard: Element): void => { + wizard.dispatchEvent(newWizardEvent()); + wizard.dispatchEvent(newWizardEvent(lNodeInstanceWizard(parent))); + }; +} + +function lNodeReferenceWizard(parent: Element): Wizard { return [ { title: get('lnode.wizard.title.selectIEDs'), - element, - content: [renderIEDPage(element)], + menuActions: [ + { + icon: '', + label: get('lnode.wizard.instance'), + action: openLNodeInstanceWizard(parent), + }, + ], + content: [renderIEDPage(parent)], }, { - initial: Array.from(element.children).some( + initial: Array.from(parent.children).some( child => child.tagName === 'LNode' ), title: get('lnode.wizard.title.selectLNs'), - element, primary: { icon: 'save', label: get('save'), - action: lNodeWizardAction(element), + action: lNodeWizardAction(parent), }, content: [html``], }, ]; } + +/** @returns a Wizard for editing `element`'s `LNode` children. */ +export function lNodeWizard(parent: Element): Wizard { + if ( + parent.tagName === 'Function' || + parent.tagName === 'SubFunction' || + parent.tagName === 'EqFunction' || + parent.tagName === 'EqSubFunction' + ) + return lNodeInstanceWizard(parent); + + return lNodeReferenceWizard(parent); +} diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index c450530caa..c0de98bad3 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -314,7 +314,7 @@ export const wizards: Record< }, LNode: { edit: lNodeWizard, - create: createLNodeWizard, + create: lNodeWizard, }, LNodeType: { edit: emptyWizard, diff --git a/test/unit/wizards/__snapshots__/lnode.test.snap.js b/test/unit/wizards/__snapshots__/lnode.test.snap.js index b20f2670c1..dd527a94e8 100644 --- a/test/unit/wizards/__snapshots__/lnode.test.snap.js +++ b/test/unit/wizards/__snapshots__/lnode.test.snap.js @@ -1,13 +1,32 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["Wizards for LNode element contain a create wizard that with existing LLN0 and LPHD instances looks like the latest snapshot"] = +snapshots["Wizards for LNode element contain a LNode instantiate wizard that with existing LLN0 and LPHD instances looks like the latest snapshot"] = ` +
- #Dummy.LLN0 + [lnode.wizard.uniquewarning] - #Dummy.LPHD1 + [lnode.wizard.uniquewarning] `; -/* end snapshot Wizards for LNode element contain a create wizard that with existing LLN0 and LPHD instances looks like the latest snapshot */ +/* end snapshot Wizards for LNode element contain a LNode instantiate wizard that with existing LLN0 and LPHD instances looks like the latest snapshot */ -snapshots["Wizards for LNode element contain a create wizard that with existing LLN0 but missing LPHD instances looks like the latest snapshot"] = +snapshots["Wizards for LNode element contain a LNode instantiate wizard that with existing LLN0 but missing LPHD instances looks like the latest snapshot"] = ` +
- #Dummy.LLN0 + [lnode.wizard.uniquewarning] `; -/* end snapshot Wizards for LNode element contain a create wizard that with existing LLN0 but missing LPHD instances looks like the latest snapshot */ +/* end snapshot Wizards for LNode element contain a LNode instantiate wizard that with existing LLN0 but missing LPHD instances looks like the latest snapshot */ -snapshots["Wizards for LNode element contain a create wizard that with missing LLN0 and LPHD instances looks like the latest snapshot"] = +snapshots["Wizards for LNode element contain a LNode instantiate wizard that with missing LLN0 and LPHD instances looks like the latest snapshot"] = ` +
`; -/* end snapshot Wizards for LNode element contain a create wizard that with missing LLN0 and LPHD instances looks like the latest snapshot */ +/* end snapshot Wizards for LNode element contain a LNode instantiate wizard that with missing LLN0 and LPHD instances looks like the latest snapshot */ + +snapshots["Wizards for LNode element contain a LNode reference create wizard that with references to existing logical nodes looks like the latest snapshot"] = +` +
+ + + + XSWI2 + + + IED2 | + CBSW + + + + + XCBR1 + + + IED2 | + CBSW + + + + + XSWI1 + + + IED2 | + CBSW + + + + + GGIO1 + + + IED2 | + CBSW + + + + + LLN0 + + + IED2 | + CircuitBreaker_CB1 + + + + + XCBR1 + + + IED2 | + CircuitBreaker_CB1 + + + + + CSWI1 + + + IED2 | + CircuitBreaker_CB1 + + + + + LLN0 + + account_tree + + /AA1/E1/COUPLING_BAY + + + IED2 | + CBSW + + + + + LPHD1 + + account_tree + + /AA1/E1/COUPLING_BAY + + + IED2 | + CBSW + + + + + XSWI3 + + account_tree + + /AA1/E1/COUPLING_BAY + + + IED2 | + CBSW + + + +
+ + + + + + +
+`; +/* end snapshot Wizards for LNode element contain a LNode reference create wizard that with references to existing logical nodes looks like the latest snapshot */ + +snapshots["Wizards for LNode element contain a LNode reference create wizard that with missing references to existing logical nodes looks like the latest snapshot"] = +` + +
+ + + IED1 + + + IED2 + + + IED3 + + +
+ + + + +
+`; +/* end snapshot Wizards for LNode element contain a LNode reference create wizard that with missing references to existing logical nodes looks like the latest snapshot */ diff --git a/test/unit/wizards/lnode.test.ts b/test/unit/wizards/lnode.test.ts index 78c488e72e..f97e9e5a58 100644 --- a/test/unit/wizards/lnode.test.ts +++ b/test/unit/wizards/lnode.test.ts @@ -6,7 +6,7 @@ import { MockWizardEditor } from '../../mock-wizard-editor.js'; import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; -import { createLNodeWizard } from '../../../src/wizards/lnode.js'; +import { lNodeWizard } from '../../../src/wizards/lnode.js'; import { Create, isCreate } from '../../../src/foundation.js'; describe('Wizards for LNode element', () => { @@ -28,10 +28,10 @@ describe('Wizards for LNode element', () => { window.addEventListener('editor-action', actionEvent); }); - describe('contain a create wizard that', () => { + describe('contain a LNode instantiate wizard that', () => { describe('with existing LLN0 and LPHD instances', () => { beforeEach(async () => { - const wizard = createLNodeWizard( + const wizard = lNodeWizard( doc.querySelector('Function[name="parentFunction"]')! ); element.workflow.push(() => wizard); @@ -44,7 +44,7 @@ describe('Wizards for LNode element', () => { describe('with existing LLN0 but missing LPHD instances', () => { beforeEach(async () => { - const wizard = createLNodeWizard( + const wizard = lNodeWizard( doc.querySelector('SubFunction[name="circuitBreaker"]')! ); element.workflow.push(() => wizard); @@ -57,7 +57,7 @@ describe('Wizards for LNode element', () => { describe('with missing LLN0 and LPHD instances', () => { beforeEach(async () => { - const wizard = createLNodeWizard( + const wizard = lNodeWizard( doc.querySelector('SubFunction[name="disconnector"]')! ); element.workflow.push(() => wizard); @@ -73,7 +73,7 @@ describe('Wizards for LNode element', () => { let listItems: ListItemBase[]; beforeEach(async () => { - const wizard = createLNodeWizard( + const wizard = lNodeWizard( doc.querySelector('SubFunction[name="disconnector"]')! ); element.workflow.push(() => wizard); @@ -163,4 +163,30 @@ describe('Wizards for LNode element', () => { }); }); }); + + describe('contain a LNode reference create wizard that', () => { + describe('with references to existing logical nodes', () => { + beforeEach(async () => { + const wizard = lNodeWizard( + doc.querySelector('ConductingEquipment[name="QB1"]')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + }); + + it('looks like the latest snapshot', async () => + await expect(element.wizardUI.dialog).to.equalSnapshot()); + }); + + describe('with missing references to existing logical nodes', () => { + beforeEach(async () => { + const wizard = lNodeWizard(doc.querySelector('Substation')!); + element.workflow.push(() => wizard); + await element.requestUpdate(); + }); + + it('looks like the latest snapshot', async () => + await expect(element.wizardUI.dialog).to.equalSnapshot()); + }); + }); }); From 595bf09bf01e6f480d2cedc7612f8bceec6026c5 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Tue, 24 May 2022 07:28:53 +0200 Subject: [PATCH 06/11] fix(wizard-libraray): import statement --- src/wizards/wizard-library.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index c0de98bad3..1139bbde16 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -7,7 +7,7 @@ import { } from './conductingequipment.js'; import { editConnectivityNodeWizard } from './connectivitynode.js'; import { createFCDAsWizard } from './fcda.js'; -import { createLNodeWizard, lNodeWizard } from './lnode.js'; +import { lNodeWizard } from './lnode.js'; import { editOptFieldsWizard } from './optfields.js'; import { createSubstationWizard, substationEditWizard } from './substation.js'; import { editTerminalWizard } from './terminal.js'; From 21c75b58dc3f825836dfea0693f21c84df09879d Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Tue, 24 May 2022 16:15:11 +0200 Subject: [PATCH 07/11] refactor(wizards/lnode): triggered by review --- src/translations/de.ts | 7 +++- src/translations/en.ts | 7 +++- src/wizards/lnode.ts | 59 ++++++++++++++++++++------ test/unit/wizards/lnode.test.ts | 73 +++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 14 deletions(-) diff --git a/src/translations/de.ts b/src/translations/de.ts index 9ea5ad1774..33f11a7665 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -464,9 +464,14 @@ export const de: Translations = { }, placeholder: 'Bitte laden Sie eine SCL-Datei, die IED-Elemente enthält.', uniquewarning: 'Logische Knoten Klasse existiert bereits', - reference: 'Reference auf bestehenden logischen Knoten erstellen', + reference: 'Referenz auf bestehenden logischen Knoten erstellen', instance: 'Referenz auf logischen Knoten Typ erstellen', }, + log: { + title: 'LNode vom Type {{lnClass}} kann nicht hinzugefügt werden', + nonuniquelninst: 'Keine eindeutige Instanz (lnInst)', + uniqueln0: 'Nur eine Instanz von {{lnClass}} zulässig', + }, tooltip: 'Referenz zu logischen Knoten erstellen', }, guess: { diff --git a/src/translations/en.ts b/src/translations/en.ts index 30f0e5f5ce..ff26b74e03 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -460,10 +460,15 @@ export const en = { selectLNodeTypes: 'Select logical node types', }, placeholder: 'Please load an SCL file that contains IED elements.', - uniquewarning: 'Logical node class already exist', + uniquewarning: 'Logical node class already exists', reference: 'Add reference to existing logical node', instance: 'Add reference to logical node type', }, + log: { + title: 'Cannot add LNode of class {{lnClass}}', + nonuniquelninst: 'Cannot find unique lnInst', + uniqueln0: 'Only one instance of {{lnClass}} allowed', + }, tooltip: 'Create logical nodes reference', }, guess: { diff --git a/src/wizards/lnode.ts b/src/wizards/lnode.ts index 37dc6660ca..057dec3488 100644 --- a/src/wizards/lnode.ts +++ b/src/wizards/lnode.ts @@ -17,6 +17,7 @@ import { getChildElementsByTagName, identity, isPublic, + newLogEvent, newWizardEvent, referencePath, selector, @@ -26,16 +27,17 @@ import { WizardMenuActor, } from '../foundation.js'; +const maxLnInst = 99; + function getUniqueLnInst(parent: Element, lnClass: string): number { - const lnInsts = Array.from( - parent.querySelectorAll(`LNode[lnClass="${lnClass}"]`) - ) + const lnInsts = getChildElementsByTagName(parent, 'LNode') + .filter(lnode => lnode.getAttribute('lnClass') === lnClass) .map(lNode => Number.parseInt(lNode.getAttribute('lnInst')!)) .sort((a, b) => a - b); if (lnInsts.length === 0) return 1; - for (let i = 1; i < 99; i++) { + for (let i = 1; i <= maxLnInst; i++) { if (lnInsts[i - 1] !== i) return i; } @@ -66,11 +68,44 @@ function createLNodeAction(parent: Element): WizardActor { if (!lnClass) return null; const uniqueLnInst = getUniqueLnInst(clonedParent, lnClass); - if (isNaN(uniqueLnInst) || uniqueLnInst > 99) return null; - - const existLLN0 = - clonedParent.querySelector('LNode[lnClass="LLN0"]') !== null; - if (lnClass === 'LLN0' && existLLN0) return null; + if (isNaN(uniqueLnInst)) { + wizard.dispatchEvent( + newLogEvent({ + kind: 'error', + title: get('lnode.log.title', { lnClass }), + message: get('lnode.log.nonuniquelninst'), + }) + ); + return; + } + + const hasLLN0 = getChildElementsByTagName(parent, 'LNode').some( + lnode => lnode.getAttribute('lnClass') === 'LLN0' + ); + if (lnClass === 'LLN0' && hasLLN0) { + wizard.dispatchEvent( + newLogEvent({ + kind: 'error', + title: get('lnode.log.title', { lnClass }), + message: get('lnode.log.uniqueln0', { lnClass }), + }) + ); + return; + } + + const hasLPHD = getChildElementsByTagName(parent, 'LNode').some( + lnode => lnode.getAttribute('lnClass') === 'LPHD' + ); + if (lnClass === 'LPHD' && hasLPHD) { + wizard.dispatchEvent( + newLogEvent({ + kind: 'error', + title: get('lnode.log.title', { lnClass }), + message: get('lnode.log.uniqueln0', { lnClass }), + }) + ); + return; + } const lnInst = lnClass === 'LLN0' ? '' : `${uniqueLnInst}`; @@ -123,7 +158,7 @@ function lNodeInstanceWizard(parent: Element): Wizard { content: [ html`${lNodeTypes.map(lNodeType => { - const isDesabled = + const isDisabled = (lNodeType.getAttribute('lnClass') === 'LLN0' && getChildElementsByTagName(parent, 'LNode').some( lnode => lnode.getAttribute('lnClass') === 'LLN0' @@ -136,10 +171,10 @@ function lNodeInstanceWizard(parent: Element): Wizard { return html`${lNodeType.getAttribute('lnClass')}${isDesabled + >${isDisabled ? get('lnode.wizard.uniquewarning') : identity(lNodeType)} { let doc: Document; let actionEvent: SinonSpy; + let logEvent: SinonSpy; beforeEach(async () => { doc = await fetch('/test/testfiles/lnodewizard.scd') @@ -26,6 +27,8 @@ describe('Wizards for LNode element', () => { actionEvent = spy(); window.addEventListener('editor-action', actionEvent); + logEvent = spy(); + window.addEventListener('log', logEvent); }); describe('contain a LNode instantiate wizard that', () => { @@ -40,6 +43,76 @@ describe('Wizards for LNode element', () => { it('looks like the latest snapshot', async () => await expect(element.wizardUI.dialog).to.equalSnapshot()); + + describe('has a primary action that', () => { + let primaryAction: HTMLElement; + let listItems: ListItemBase[]; + + beforeEach(async () => { + const wizard = lNodeWizard( + doc.querySelector('SubFunction[name="disconnector"]')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + listItems = Array.from( + element.wizardUI!.dialog!.querySelectorAll( + 'mwc-check-list-item' + ) + ); + }); + + it('triggers error log massage when duplicate LLN0 classes are added', async () => { + listItems[0].selected = true; + + await primaryAction.click(); + + expect(logEvent).to.have.be.calledOnce; + expect(logEvent.args[0][0].detail.message).to.contain( + 'lnode.log.uniqueln0' + ); + }); + + it('triggers error log massage when duplicate LPHD classes are added', async () => { + listItems[1].selected = true; + + await primaryAction.click(); + + expect(logEvent).to.have.be.calledOnce; + expect(logEvent.args[0][0].detail.message).to.contain( + 'lnode.log.uniqueln0' + ); + }); + + it('trigger error log message when not unique lnInst can be find', async () => { + const parent = doc.querySelector('SubFunction[name="disconnector"]')! + .parentElement!; + for (let i = 1; i <= 99; i++) { + const element = ( + doc.createElementNS(doc.documentElement.namespaceURI, 'LNode') + ); + + element.setAttribute('lnClass', 'CILO'); + element.setAttribute('lnInst', `${i}`); + parent.appendChild(element); + } + + listItems[4].selected = true; + + await primaryAction.click(); + + expect(logEvent).to.have.be.calledOnce; + expect(logEvent.args[0][0].detail.message).to.contain( + 'lnode.log.nonuniquelninst' + ); + }); + }); }); describe('with existing LLN0 but missing LPHD instances', () => { From dba15131bc5151a1682637dea11c697ab894ec59 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Tue, 24 May 2022 17:04:02 +0200 Subject: [PATCH 08/11] refactor(wizards/lnode): better uniqueLnInst function --- src/wizards/lnode.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/wizards/lnode.ts b/src/wizards/lnode.ts index 057dec3488..96cff120e8 100644 --- a/src/wizards/lnode.ts +++ b/src/wizards/lnode.ts @@ -28,20 +28,18 @@ import { } from '../foundation.js'; const maxLnInst = 99; +const lnInstRange = Array(maxLnInst) + .fill(1) + .map((_, i) => `${i + 1}`); + +function getUniqueLnInst(parent: Element, lnClass: string): string | undefined { + const lnInsts = new Set( + getChildElementsByTagName(parent, 'LNode') + .filter(lnode => lnode.getAttribute('lnClass') === lnClass) + .map(lNode => lNode.getAttribute('lnInst')!) + ); -function getUniqueLnInst(parent: Element, lnClass: string): number { - const lnInsts = getChildElementsByTagName(parent, 'LNode') - .filter(lnode => lnode.getAttribute('lnClass') === lnClass) - .map(lNode => Number.parseInt(lNode.getAttribute('lnInst')!)) - .sort((a, b) => a - b); - - if (lnInsts.length === 0) return 1; - - for (let i = 1; i <= maxLnInst; i++) { - if (lnInsts[i - 1] !== i) return i; - } - - return NaN; + return lnInstRange.find(lnInst => !lnInsts.has(lnInst)); } function createLNodeAction(parent: Element): WizardActor { @@ -68,7 +66,7 @@ function createLNodeAction(parent: Element): WizardActor { if (!lnClass) return null; const uniqueLnInst = getUniqueLnInst(clonedParent, lnClass); - if (isNaN(uniqueLnInst)) { + if (!uniqueLnInst) { wizard.dispatchEvent( newLogEvent({ kind: 'error', @@ -107,7 +105,7 @@ function createLNodeAction(parent: Element): WizardActor { return; } - const lnInst = lnClass === 'LLN0' ? '' : `${uniqueLnInst}`; + const lnInst = lnClass === 'LLN0' ? '' : uniqueLnInst; const element = createElement(parent.ownerDocument, 'LNode', { iedName: 'None', From 859940b77814270a79bcaeb5fc426fcf9a7bc43c Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Tue, 24 May 2022 17:42:09 +0200 Subject: [PATCH 09/11] refactor(wizards/lnode): better search for uniqeu lnInst --- src/wizards/lnode.ts | 37 ++++++++++++++++++++++----------- test/unit/wizards/lnode.test.ts | 6 +++--- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/wizards/lnode.ts b/src/wizards/lnode.ts index 96cff120e8..c392731cd2 100644 --- a/src/wizards/lnode.ts +++ b/src/wizards/lnode.ts @@ -32,14 +32,28 @@ const lnInstRange = Array(maxLnInst) .fill(1) .map((_, i) => `${i + 1}`); -function getUniqueLnInst(parent: Element, lnClass: string): string | undefined { - const lnInsts = new Set( - getChildElementsByTagName(parent, 'LNode') - .filter(lnode => lnode.getAttribute('lnClass') === lnClass) - .map(lNode => lNode.getAttribute('lnInst')!) - ); - - return lnInstRange.find(lnInst => !lnInsts.has(lnInst)); +function uniqueLnInstGenerator( + parent: Element +): (lnClass: string) => string | undefined { + const generators = new Map string | undefined>(); + + return (lnClass: string) => { + if (!generators.has(lnClass)) { + const lnInsts = new Set( + getChildElementsByTagName(parent, 'LNode') + .filter(lnode => lnode.getAttribute('lnClass') === lnClass) + .map(lNode => lNode.getAttribute('lnInst')!) + ); + + generators.set(lnClass, () => { + const uniqueLnInst = lnInstRange.find(lnInst => !lnInsts.has(lnInst)); + if (uniqueLnInst) lnInsts.add(uniqueLnInst); + return uniqueLnInst; + }); + } + + return generators.get(lnClass)!(); + }; } function createLNodeAction(parent: Element): WizardActor { @@ -58,14 +72,15 @@ function createLNodeAction(parent: Element): WizardActor { }) .filter(item => item !== null); - const clonedParent = parent.cloneNode(true); //for multiple selection of same lnClass + const lnInstGenerator = uniqueLnInstGenerator(parent); const createActions: Create[] = selectedLNodeTypes .map(selectedLNodeType => { const lnClass = selectedLNodeType.getAttribute('lnClass'); if (!lnClass) return null; - const uniqueLnInst = getUniqueLnInst(clonedParent, lnClass); + const uniqueLnInst = lnInstGenerator(lnClass); + if (!uniqueLnInst) { wizard.dispatchEvent( newLogEvent({ @@ -116,8 +131,6 @@ function createLNodeAction(parent: Element): WizardActor { lnType: selectedLNodeType.getAttribute('id')!, }); - clonedParent.appendChild(element); //for multiple selection of same lnClass - return { new: { parent, element } }; }) .filter(action => action); diff --git a/test/unit/wizards/lnode.test.ts b/test/unit/wizards/lnode.test.ts index 4376a80f63..d2f8ea29cc 100644 --- a/test/unit/wizards/lnode.test.ts +++ b/test/unit/wizards/lnode.test.ts @@ -71,7 +71,7 @@ describe('Wizards for LNode element', () => { it('triggers error log massage when duplicate LLN0 classes are added', async () => { listItems[0].selected = true; - await primaryAction.click(); + primaryAction.click(); expect(logEvent).to.have.be.calledOnce; expect(logEvent.args[0][0].detail.message).to.contain( @@ -82,7 +82,7 @@ describe('Wizards for LNode element', () => { it('triggers error log massage when duplicate LPHD classes are added', async () => { listItems[1].selected = true; - await primaryAction.click(); + primaryAction.click(); expect(logEvent).to.have.be.calledOnce; expect(logEvent.args[0][0].detail.message).to.contain( @@ -105,7 +105,7 @@ describe('Wizards for LNode element', () => { listItems[4].selected = true; - await primaryAction.click(); + primaryAction.click(); expect(logEvent).to.have.be.calledOnce; expect(logEvent.args[0][0].detail.message).to.contain( From 53c3736f165a5f9357fddd37a9056417b383ce88 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Tue, 24 May 2022 22:15:57 +0200 Subject: [PATCH 10/11] test(wizards/lnode): better test unique lnInst selection --- test/testfiles/lnodewizard.scd | 7 ++++++- test/unit/wizards/lnode.test.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/test/testfiles/lnodewizard.scd b/test/testfiles/lnodewizard.scd index 19cd4f222a..86d836201d 100644 --- a/test/testfiles/lnodewizard.scd +++ b/test/testfiles/lnodewizard.scd @@ -37,7 +37,12 @@ - + + + + + + diff --git a/test/unit/wizards/lnode.test.ts b/test/unit/wizards/lnode.test.ts index d2f8ea29cc..5aaf91a315 100644 --- a/test/unit/wizards/lnode.test.ts +++ b/test/unit/wizards/lnode.test.ts @@ -179,16 +179,16 @@ describe('Wizards for LNode element', () => { }); it('does set iedName, lnCalss, lnInst and lnType', async () => { - listItems[4].selected = true; + listItems[2].selected = true; await primaryAction.click(); expect(actionEvent).to.have.be.calledOnce; const action = actionEvent.args[0][0].detail.action; expect(action.new.element).to.have.attribute('iedName', 'None'); - expect(action.new.element).to.have.attribute('lnClass', 'CILO'); + expect(action.new.element).to.have.attribute('lnClass', 'XCBR'); expect(action.new.element).to.have.attribute('lnInst', '1'); - expect(action.new.element).to.have.attribute('lnType', 'Dummy.CILO'); + expect(action.new.element).to.have.attribute('lnType', 'Dummy.XCBR1'); }); it('does not set ldInst and prefix', async () => { @@ -209,7 +209,7 @@ describe('Wizards for LNode element', () => { expect(actionEvent).to.have.be.calledOnce; const action = actionEvent.args[0][0].detail.action; - expect(action.new.element).to.have.attribute('lnInst', '1'); + expect(action.new.element).to.have.attribute('lnInst', '2'); }); it('makes sure that lnInst is unique if several LNodeType with same lnClass are selected', async () => { @@ -221,8 +221,8 @@ describe('Wizards for LNode element', () => { expect(actionEvent).to.have.be.calledTwice; const action1 = actionEvent.args[0][0].detail.action; const action2 = actionEvent.args[1][0].detail.action; - expect(action1.new.element).to.have.attribute('lnInst', '1'); - expect(action2.new.element).to.have.attribute('lnInst', '2'); + expect(action1.new.element).to.have.attribute('lnInst', '2'); + expect(action2.new.element).to.have.attribute('lnInst', '4'); }); it('does add empty string to LNode with lnClass LLN0', async () => { From f0f468ad2c8cd3682fb3f9ccc39b528c28d8bd18 Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Wed, 25 May 2022 11:38:20 +0200 Subject: [PATCH 11/11] fix(wizard-dialog): stabilize jump to initial page This fixes the issue we had when opening the LNode instance reference create wizard from the overflow menu of the LNodeType reference create wizard. --- src/wizard-dialog.ts | 9 ++++++--- .../subnetwork-editor-wizarding.test.snap.js | 2 +- .../bay-editor-wizarding.test.snap.js | 2 +- ...ng-equipment-editor-wizarding.test.snap.js | 2 +- .../substation-editor-wizarding.test.snap.js | 2 +- ...oltage-level-editor-wizarding.test.snap.js | 2 +- .../datype-wizarding.test.snap.js | 4 ++-- .../dotype-wizarding.test.snap.js | 8 ++++---- .../enumtype-wizarding.test.snap.js | 8 ++++---- .../lnodetype-wizard.test.snap.js | 8 ++++---- .../bda-wizarding-editing.test.snap.js | 4 ++-- .../da-wizarding-editing.test.snap.js | 4 ++-- .../__snapshots__/wizard-dialog.test.snap.js | 2 +- .../ied/__snapshots__/da-wizard.test.snap.js | 12 +++++------ .../ied/__snapshots__/do-wizard.test.snap.js | 6 +++--- .../wizards/__snapshots__/bay.test.snap.js | 2 +- .../conductingequipment.test.snap.js | 2 +- .../powertransformer.test.snap.js | 2 +- .../UpdateDescriptionSEL.test.snap.js | 4 ++-- .../UpdateDescritionABB.test.snap.js | 4 ++-- .../__snapshots__/abstractda.test.snap.js | 2 +- .../__snapshots__/address.test.snap.js | 2 +- .../__snapshots__/clientln.test.snap.js | 4 ++-- .../__snapshots__/commmap.test.snap.js | 14 ++++++++++++- .../connectedap-pattern.test.snap.js | 6 +++--- .../__snapshots__/connectedap.test.snap.js | 2 +- .../connectivitynode.test.snap.js | 2 +- .../controlwithiedname.test.snap.js | 2 +- .../wizards/__snapshots__/dai.test.snap.js | 2 +- .../__snapshots__/dataset.test.snap.js | 2 +- .../__snapshots__/eqfunction.test.snap.js | 4 ++-- .../__snapshots__/eqsubfunction.test.snap.js | 4 ++-- .../wizards/__snapshots__/fcda.test.snap.js | 2 +- .../__snapshots__/function.test.snap.js | 4 ++-- .../wizards/__snapshots__/gse.test.snap.js | 2 +- .../__snapshots__/gsecontrol.test.snap.js | 16 +++++++-------- .../wizards/__snapshots__/ied.test.snap.js | 4 ++-- .../wizards/__snapshots__/lnode.test.snap.js | 10 +++++----- .../__snapshots__/optfields.test.snap.js | 2 +- .../powertransformer.test.snap.js | 4 ++-- .../__snapshots__/reportcontrol.test.snap.js | 20 +++++++++---------- .../sampledvaluecontrol.test.snap.js | 4 ++-- .../wizards/__snapshots__/smv.test.snap.js | 2 +- .../__snapshots__/smvopts.test.snap.js | 2 +- .../__snapshots__/subfunction.test.snap.js | 4 ++-- .../__snapshots__/subnetwork.test.snap.js | 6 +++--- .../__snapshots__/substation.test.snap.js | 4 ++-- .../__snapshots__/terminal.test.snap.js | 2 +- .../wizards/__snapshots__/trgops.test.snap.js | 2 +- test/unit/wizards/commmap.test.ts | 1 + 50 files changed, 121 insertions(+), 105 deletions(-) diff --git a/src/wizard-dialog.ts b/src/wizard-dialog.ts index c279714e66..ba004591f1 100644 --- a/src/wizard-dialog.ts +++ b/src/wizard-dialog.ts @@ -174,11 +174,15 @@ export class WizardDialog extends LitElement { } prev(): void { - if (this.pageIndex > 0) this.pageIndex--; + if (this.pageIndex <= 0) return; + this.pageIndex--; + this.dialog?.show(); } + async next(): Promise { if (dialogValid(this.dialog)) { if (this.wizard.length > this.pageIndex + 1) this.pageIndex++; + this.dialog?.show(); } else { this.dialog?.show(); await this.dialog?.updateComplete; @@ -295,8 +299,7 @@ export class WizardDialog extends LitElement { : 0; return html` @@ -649,7 +649,7 @@ snapshots["with a DO element looks like the latest snapshot"] = snapshots["with a DO element and DOI Element looks like the latest snapshot"] = ` diff --git a/test/unit/editors/ied/__snapshots__/do-wizard.test.snap.js b/test/unit/editors/ied/__snapshots__/do-wizard.test.snap.js index 8d796199dc..62c5adbdcb 100644 --- a/test/unit/editors/ied/__snapshots__/do-wizard.test.snap.js +++ b/test/unit/editors/ied/__snapshots__/do-wizard.test.snap.js @@ -3,7 +3,7 @@ export const snapshots = {}; snapshots["with no ancestors looks like the latest snapshot"] = ` @@ -36,9 +38,11 @@ snapshots["communication mapping wizard looks like the latest snapshot"] = @@ -61,9 +65,11 @@ snapshots["communication mapping wizard looks like the latest snapshot"] = @@ -86,9 +92,11 @@ snapshots["communication mapping wizard looks like the latest snapshot"] = @@ -111,9 +119,11 @@ snapshots["communication mapping wizard looks like the latest snapshot"] = @@ -136,9 +146,11 @@ snapshots["communication mapping wizard looks like the latest snapshot"] = diff --git a/test/unit/wizards/__snapshots__/connectedap-pattern.test.snap.js b/test/unit/wizards/__snapshots__/connectedap-pattern.test.snap.js index 68541ff47c..93820ea886 100644 --- a/test/unit/wizards/__snapshots__/connectedap-pattern.test.snap.js +++ b/test/unit/wizards/__snapshots__/connectedap-pattern.test.snap.js @@ -3,7 +3,7 @@ export const snapshots = {}; snapshots["Edit wizard for SCL element ConnectedAP include an edit wizard that for Edition 1 projects looks like the latest snapshot"] = ` @@ -541,7 +541,7 @@ snapshots["gsecontrol wizards define an create wizard that with existing Connect snapshots["gsecontrol wizards define an create wizard that with existing ConnectedAP element in the Communication section the third page looks like the latest snapshot"] = ` @@ -576,7 +576,7 @@ snapshots["gsecontrol wizards define an create wizard that with existing Connect snapshots["gsecontrol wizards define an create wizard that with missing ConnectedAP element in the Communication section the second page having a warning message "] = ` @@ -615,7 +615,7 @@ snapshots["gsecontrol wizards define an create wizard that with missing Connecte snapshots["gsecontrol wizards define a wizard to select the control block reference looks like the latest snapshot"] = ` @@ -595,7 +595,7 @@ snapshots["Wizards for SCL ReportControl element define an create wizard that th snapshots["Wizards for SCL ReportControl element define an create wizard that the third page looks like the latest snapshot"] = ` @@ -677,7 +677,7 @@ snapshots["Wizards for SCL ReportControl element define an create wizard that th snapshots["Wizards for SCL ReportControl element define an create wizard that the forth page looks like the latest snapshot"] = ` @@ -712,7 +712,7 @@ snapshots["Wizards for SCL ReportControl element define an create wizard that th snapshots["Wizards for SCL ReportControl element define a wizard to select the control block reference looks like the latest snapshot"] = ` { element = parent.querySelector('zeroline-pane')!; element.commmap.click(); await element.updateComplete; + await parent.updateComplete; }); it('looks like the latest snapshot', async () => {