diff --git a/src/editors/substation/process-editor.ts b/src/editors/substation/process-editor.ts index b6d34c94e..b80fe4a96 100644 --- a/src/editors/substation/process-editor.ts +++ b/src/editors/substation/process-editor.ts @@ -8,6 +8,8 @@ import { state, } from 'lit-element'; +import { translate } from 'lit-translate'; + import '@material/mwc-icon'; import '@material/mwc-icon-button'; import '@material/mwc-menu'; @@ -22,7 +24,9 @@ import './substation-editor.js'; import './process-editor.js'; import { styles } from './foundation.js'; -import { getChildElementsByTagName } from '../../foundation.js'; +import { newWizardEvent, getChildElementsByTagName } from '../../foundation.js'; + +import { wizards } from '../../wizards/wizard-library.js'; @customElement('process-editor') export class ProcessEditor extends LitElement { @@ -44,6 +48,11 @@ export class ProcessEditor extends LitElement { return `${name} ${desc ? `—${desc}` : ''}`; } + private openEditWizard(): void { + const wizard = wizards['Process'].edit(this.element); + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + } + private renderConductingEquipments(): TemplateResult { const ConductingEquipments = getChildElementsByTagName( this.element, @@ -143,6 +152,12 @@ export class ProcessEditor extends LitElement { render(): TemplateResult { return html` + + this.openEditWizard()} + > + ${this.renderConductingEquipments()}${this.renderGeneralEquipments()}${this.renderFunctions()}${this.renderLNodes()} ${this.renderLines()} ${this.renderSubstations()}${this.renderProcesses()} `; diff --git a/src/wizards/process.ts b/src/wizards/process.ts new file mode 100644 index 000000000..8ad928891 --- /dev/null +++ b/src/wizards/process.ts @@ -0,0 +1,103 @@ +import { html, TemplateResult } from 'lit-element'; +import { get, translate } from 'lit-translate'; + +import { + cloneElement, + createElement, + getChildElementsByTagName, + getValue, + SimpleAction, + Wizard, + WizardActor, + WizardInputElement, +} from '../foundation.js'; + +function updateProcessAction(element: Element): WizardActor { + return (inputs: WizardInputElement[]): SimpleAction[] => { + const tapProcessAttrs: Record = {}; + const tapProcessKeys = ['name', 'desc', 'type']; + tapProcessKeys.forEach(key => { + tapProcessAttrs[key] = getValue(inputs.find(i => i.label === key)!); + }); + + if ( + tapProcessKeys.some( + key => tapProcessAttrs[key] !== element.getAttribute(key) + ) + ) { + const newElement = cloneElement(element, tapProcessAttrs); + return [ + { + old: { element }, + new: { element: newElement }, + }, + ]; + } + return []; + }; +} + +interface ContentOptions { + name: string | null; + desc: string | null; + type: string | null; + reservedNames: string[]; +} + +export function contentProcessWizard( + content: ContentOptions +): TemplateResult[] { + return [ + html``, + html``, + html``, + ]; +} + +export function editProcessWizard(element: Element): Wizard { + const name = element.getAttribute('name'); + const desc = element.getAttribute('desc'); + const type = element.getAttribute('type'); + const reservedNames: string[] = getChildElementsByTagName( + element.parentElement!, + 'Process' + ) + .filter(sibling => sibling !== element) + .map(sibling => sibling.getAttribute('name')!); + return [ + { + title: get('wizard.title.edit', { tagName: 'Process' }), + primary: { + icon: 'save', + label: get('save'), + action: updateProcessAction(element), + }, + content: [ + ...contentProcessWizard({ + name, + desc, + type, + reservedNames, + }), + ], + }, + ]; +} diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index 035bc22b6..8729ec737 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -50,6 +50,7 @@ import { } from './transformerWinding.js'; import { createTapChangerWizard, editTapChangerWizard } from './tapchanger.js'; import { createLineWizard, editLineWizard } from './line.js'; +import { editProcessWizard } from './process.js'; type SclElementWizard = ( element: Element, @@ -388,7 +389,7 @@ export const wizards: Record< create: emptyWizard, }, Process: { - edit: emptyWizard, + edit: editProcessWizard, create: emptyWizard, }, ProtNs: { diff --git a/test/integration/editors/substation/process-editor-wizard-editing.test.ts b/test/integration/editors/substation/process-editor-wizard-editing.test.ts new file mode 100644 index 000000000..22d0f0607 --- /dev/null +++ b/test/integration/editors/substation/process-editor-wizard-editing.test.ts @@ -0,0 +1,133 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import '../../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../../mock-wizard-editor.js'; + +import '../../../../src/editors/substation/process-editor.js'; +import { ProcessEditor } from '../../../../src/editors/substation/process-editor.js'; +import { WizardTextField } from '../../../../src/wizard-textfield.js'; + +describe('process-editor wizarding editing integration', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: ProcessEditor | null; + + describe('edit wizard', () => { + let nameField: WizardTextField; + let descField: WizardTextField; + let typeField: WizardTextField; + + let primaryAction: HTMLElement; + let secondaryAction: HTMLElement; + + beforeEach(async () => { + doc = await fetch('/test/testfiles/editors/substation/Process.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html`` + ) + ); + element = parent.querySelector('process-editor'); + await (( + element?.shadowRoot?.querySelector('mwc-icon-button[icon="edit"]') + )).click(); + await parent.updateComplete; + + nameField = ( + parent.wizardUI.dialog?.querySelector('wizard-textfield[label="name"]') + ); + + typeField = ( + parent.wizardUI.dialog?.querySelector('wizard-textfield[label="type"]') + ); + + secondaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="secondaryAction"]' + ) + ); + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + it('closes on secondary action', async () => { + secondaryAction.click(); + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + expect(parent.wizardUI.dialog).to.not.exist; + }); + + it('does not change name attribute if not unique within parent element', async () => { + const oldName = nameField.value; + nameField.value = 'ProcProcSubAA1'; + primaryAction.click(); + await parent.updateComplete; + expect( + doc + .querySelector('Process[name="ProcessGenConduct"]') + ?.getAttribute('name') + ).to.equal(oldName); + }); + + it('changes name attribute on primary action', async () => { + nameField.value = 'newName'; + primaryAction.click(); + await parent.updateComplete; + expect(doc.querySelector('Process')?.getAttribute('name')).to.equal( + 'newName' + ); + }); + + it('changes desc attribute on primary action', async () => { + descField = ( + parent.wizardUI.dialog?.querySelector('wizard-textfield[label="desc"]') + ); + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + descField.nullSwitch!.click(); + await parent.updateComplete; + descField.value = 'newDesc'; + console.log(descField.value); + primaryAction.click(); + await parent.updateComplete; + expect( + doc + .querySelector('Process[name="ProcessGenConduct"]') + ?.getAttribute('desc') + ).to.equal('newDesc'); + }); + + it('deletes desc attribute if wizard-textfield is deactivated', async () => { + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + descField.nullSwitch!.click(); + await parent.updateComplete; + await primaryAction.click(); + await parent.updateComplete; + expect( + doc + .querySelector('Process[name="ProcessGenConduct"]') + ?.getAttribute('desc') + ).to.be.null; + }); + + it('changes type attribute on primary action', async () => { + await new Promise(resolve => setTimeout(resolve, 100)); + typeField.nullSwitch!.click(); + await parent.updateComplete; + typeField.value = 'newType'; + primaryAction.click(); + await parent.updateComplete; + expect( + doc + .querySelector('Process[name="ProcessGenConduct"]') + ?.getAttribute('type') + ).to.equal('newType'); + }); + }); +}); diff --git a/test/integration/editors/substation/zeroline-pane.test.ts b/test/integration/editors/substation/zeroline-pane.test.ts index b85b74121..aa36444cf 100644 --- a/test/integration/editors/substation/zeroline-pane.test.ts +++ b/test/integration/editors/substation/zeroline-pane.test.ts @@ -6,7 +6,6 @@ import { MockWizardEditor } from '../../../mock-wizard-editor.js'; import '../../../../src/editors/substation/zeroline-pane.js'; import { FilteredList } from '../../../../src/filtered-list.js'; import { ZerolinePane } from '../../../../src/editors/substation/zeroline-pane.js'; - import { WizardTextField } from '../../../../src/wizard-textfield.js'; import { IconButton } from '@material/mwc-icon-button'; import { ListItem } from '@material/mwc-list/mwc-list-item'; diff --git a/test/unit/editors/substation/__snapshots__/process-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/process-editor.test.snap.js index 8b76e7c93..cf67d1511 100644 --- a/test/unit/editors/substation/__snapshots__/process-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/process-editor.test.snap.js @@ -6,6 +6,13 @@ snapshots["web component rendering Process element rendering LNode, GeneralEquip label="ProcessGenConduct " tabindex="0" > + + + + @@ -25,6 +32,13 @@ snapshots["web component rendering Process element hides LNode and Function chil label="ProcessGenConduct " tabindex="0" > + + + + @@ -38,6 +52,13 @@ snapshots["web component rendering Process element rendering Substation and Proc label="ProcProcSubAA1 " tabindex="0" > + + + + @@ -51,6 +72,13 @@ snapshots["web component rendering Process element rendering a Line child looks label="ProcessLine " tabindex="0" > + + + + diff --git a/test/unit/wizards/__snapshots__/process.test.snap.js b/test/unit/wizards/__snapshots__/process.test.snap.js new file mode 100644 index 000000000..b54631785 --- /dev/null +++ b/test/unit/wizards/__snapshots__/process.test.snap.js @@ -0,0 +1,52 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL Process element define an edit wizard that looks like the the latest snapshot"] = +` +
+ + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL Process element define an edit wizard that looks like the the latest snapshot */ + diff --git a/test/unit/wizards/process.test.ts b/test/unit/wizards/process.test.ts new file mode 100644 index 000000000..93507a036 --- /dev/null +++ b/test/unit/wizards/process.test.ts @@ -0,0 +1,103 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import '../../mock-wizard.js'; +import { MockWizard } from '../../mock-wizard.js'; + +import { WizardTextField } from '../../../src/wizard-textfield.js'; +import { SinonSpy, spy } from 'sinon'; + +import { + isReplace, + Replace, + WizardInputElement, +} from '../../../src/foundation.js'; +import { editProcessWizard } from '../../../src/wizards/process.js'; + +describe('Wizards for SCL Process element', () => { + let doc: XMLDocument; + let element: MockWizard; + let inputs: WizardInputElement[]; + + let primaryAction: HTMLElement; + let actionEvent: SinonSpy; + + beforeEach(async () => { + element = await fixture(html``); + doc = await fetch('/test/testfiles/editors/substation/Process.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('define an edit wizard that', () => { + beforeEach(async () => { + const wizard = editProcessWizard( + doc.querySelector('Process[name="ProcessGenConduct"]')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + inputs = Array.from(element.wizardUI.inputs); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + await element.wizardUI.requestUpdate(); // make sure wizard is rendered + }); + + it('looks like the the latest snapshot', async () => + await expect(element.wizardUI.dialog).dom.to.equalSnapshot()); + + it('triggers simple edit action on primary action click', async () => { + inputs[0].value = 'someNonEmptyName'; + + await element.requestUpdate(); + await primaryAction.click(); + + expect(actionEvent).to.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isReplace); + const editAction = action; + + expect(editAction.new.element).to.have.attribute( + 'name', + 'someNonEmptyName' + ); + }); + + it('allows to create non required attribute desc', async () => { + (inputs[1]).nullSwitch?.click(); + inputs[1].value = 'someDesc'; + + await element.requestUpdate(); + await primaryAction.click(); + + expect(actionEvent).to.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isReplace); + const editAction = action; + expect(editAction.new.element).to.have.attribute('desc', 'someDesc'); + }); + it('allows to create non required attribute type', async () => { + (inputs[2]).nullSwitch?.click(); + inputs[2].value = 'someNonEmptyType'; + + await element.requestUpdate(); + await primaryAction.click(); + + expect(actionEvent).to.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isReplace); + const editAction = action; + + expect(editAction.new.element).to.have.attribute( + 'type', + 'someNonEmptyType' + ); + }); + }); +});