diff --git a/src/editors/subscription/goose-message.ts b/src/editors/subscription/elements/goose-message.ts similarity index 52% rename from src/editors/subscription/goose-message.ts rename to src/editors/subscription/elements/goose-message.ts index 16a11499be..29eb73e196 100644 --- a/src/editors/subscription/goose-message.ts +++ b/src/editors/subscription/elements/goose-message.ts @@ -1,13 +1,16 @@ import { - css, customElement, html, LitElement, property, TemplateResult, } from 'lit-element'; -import { newGOOSESelectEvent } from '../../foundation.js'; -import { gooseIcon } from '../../icons.js'; + +import '@material/mwc-icon'; +import '@material/mwc-list/mwc-list-item'; + +import { gooseIcon } from '../../../icons.js'; +import { newGOOSESelectEvent } from '../foundation.js'; @customElement('goose-message') export class GOOSEMessage extends LitElement { @@ -17,24 +20,16 @@ export class GOOSEMessage extends LitElement { private onGooseSelect = () => { const ln = this.element.parentElement; - const dataset = ln?.querySelector(`DataSet[name=${this.element.getAttribute('datSet')}]`); - this.dispatchEvent( - newGOOSESelectEvent( - this.element.closest('IED')?.getAttribute('name') ?? '', - this.element, - dataset! - ) + const dataset = ln?.querySelector( + `DataSet[name=${this.element.getAttribute('datSet')}]` ); + this.dispatchEvent(newGOOSESelectEvent(this.element, dataset!)); }; render(): TemplateResult { - return html` + return html` ${this.element.getAttribute('name')} ${gooseIcon} `; } - - static styles = css``; } diff --git a/src/editors/subscription/elements/ied-element.ts b/src/editors/subscription/elements/ied-element.ts new file mode 100644 index 0000000000..ff11cbe9c8 --- /dev/null +++ b/src/editors/subscription/elements/ied-element.ts @@ -0,0 +1,43 @@ +import { + customElement, + html, + LitElement, + property, + TemplateResult, +} from 'lit-element'; + +import '@material/mwc-icon'; +import '@material/mwc-list/mwc-list-item'; + +import { newIEDSubscriptionEvent, SubscribeStatus } from '../foundation.js'; + +@customElement('ied-element') +export class IEDElement extends LitElement { + /** Holding the IED element */ + @property({ attribute: false }) + element!: Element; + + @property({ attribute: false }) + status!: SubscribeStatus; + + private onIedSelect = () => { + this.dispatchEvent( + newIEDSubscriptionEvent(this.element, this.status ?? SubscribeStatus.None) + ); + }; + + render(): TemplateResult { + return html` + ${this.element.getAttribute('name')} + ${this.status == SubscribeStatus.Full + ? html`clear` + : html`add`} + `; + } +} diff --git a/src/editors/subscription/foundation.ts b/src/editors/subscription/foundation.ts new file mode 100644 index 0000000000..c1445a7115 --- /dev/null +++ b/src/editors/subscription/foundation.ts @@ -0,0 +1,115 @@ +import { css } from 'lit-element'; + +/** + * Enumeration stating the Subscribe status of a IED to a GOOSE. + */ +export enum SubscribeStatus { + Full, + Partial, + None, +} + +export interface GOOSESelectDetail { + gseControl: Element; + dataset: Element; +} +export type GOOSESelectEvent = CustomEvent; +export function newGOOSESelectEvent( + gseControl: Element, + dataset: Element, + eventInitDict?: CustomEventInit +): GOOSESelectEvent { + return new CustomEvent('goose-dataset', { + bubbles: true, + composed: true, + ...eventInitDict, + detail: { gseControl, dataset, ...eventInitDict?.detail }, + }); +} + +export interface IEDSubscriptionDetail { + element: Element; + subscribeStatus: SubscribeStatus; +} +export type IEDSubscriptionEvent = CustomEvent; +export function newIEDSubscriptionEvent( + element: Element, + subscribeStatus: SubscribeStatus +): IEDSubscriptionEvent { + return new CustomEvent('ied-subscription', { + bubbles: true, + composed: true, + detail: { element, subscribeStatus }, + }); +} + +/** Common `CSS` styles used by DataTypeTemplate subeditors */ +export const styles = css` + :host(.moving) section { + opacity: 0.3; + } + + section { + background-color: var(--mdc-theme-surface); + transition: all 200ms linear; + outline-color: var(--mdc-theme-primary); + outline-style: solid; + outline-width: 0px; + opacity: 1; + } + + section:focus { + box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + } + + section:focus-within { + outline-width: 2px; + transition: all 250ms linear; + } + + h1, + h2, + h3 { + color: var(--mdc-theme-on-surface); + font-family: 'Roboto', sans-serif; + font-weight: 300; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0px; + line-height: 48px; + padding-left: 0.3em; + transition: background-color 150ms linear; + } + + section:focus-within > h1, + section:focus-within > h2, + section:focus-within > h3 { + color: var(--mdc-theme-surface); + background-color: var(--mdc-theme-primary); + transition: background-color 200ms linear; + } + + h1 > nav, + h2 > nav, + h3 > nav, + h1 > abbr > mwc-icon-button, + h2 > abbr > mwc-icon-button, + h3 > abbr > mwc-icon-button { + float: right; + } + + abbr[title] { + border-bottom: none !important; + cursor: inherit !important; + text-decoration: none !important; + } +`; + +declare global { + interface ElementEventMap { + ['goose-dataset']: GOOSESelectEvent; + ['ied-subscription']: IEDSubscriptionEvent; + } +} diff --git a/src/editors/subscription/publisher-goose-list.ts b/src/editors/subscription/publisher-goose-list.ts index 1bf63fbf74..146229b78e 100644 --- a/src/editors/subscription/publisher-goose-list.ts +++ b/src/editors/subscription/publisher-goose-list.ts @@ -6,22 +6,27 @@ import { property, TemplateResult, } from 'lit-element'; +import { translate } from 'lit-translate'; -import './goose-message.js'; +import '@material/mwc-icon'; +import '@material/mwc-list'; +import '@material/mwc-list/mwc-list-item'; -import { translate } from 'lit-translate'; +import './elements/goose-message.js'; import { compareNames, getNameAttribute } from '../../foundation.js'; -import { styles } from '../templates/foundation.js'; +import { styles } from './foundation.js'; /** An sub element for showing all published GOOSE messages per IED. */ @customElement('publisher-goose-list') export class PublisherGOOSEList extends LitElement { - @property() + @property({ attribute: false }) doc!: XMLDocument; - private get ieds() : Element[] { - return (this.doc) - ? Array.from(this.doc.querySelectorAll(':root > IED')).sort((a,b) => compareNames(a,b)) + private get ieds(): Element[] { + return this.doc + ? Array.from(this.doc.querySelectorAll(':root > IED')).sort((a, b) => + compareNames(a, b) + ) : []; } @@ -30,37 +35,40 @@ export class PublisherGOOSEList extends LitElement { * @param ied - The IED to search through. * @returns All the published GOOSE messages of this specific IED. */ - private getGSEControls(ied: Element) : Element[] { - return Array.from(ied.querySelectorAll(':scope > AccessPoint > Server > LDevice > LN0[lnClass="LLN0"] > GSEControl')); + private getGSEControls(ied: Element): Element[] { + return Array.from( + ied.querySelectorAll( + ':scope > AccessPoint > Server > LDevice > LN0 > GSEControl' + ) + ); } render(): TemplateResult { - return html` -
+ return html`

${translate('subscription.publisherGoose.title')}

${this.ieds.map(ied => - ied.querySelector('GSEControl') ? - html` - - ${getNameAttribute(ied)} - developer_board - -
  • - ${this.getGSEControls(ied).map(control => - html``)} - ` : `` - ) - } -
    -
    `; + ied.querySelector('GSEControl') + ? html` + + ${getNameAttribute(ied)} + developer_board + +
  • + ${this.getGSEControls(ied).map( + control => + html`` + )} + ` + : `` + )} + +
    `; } static styles = css` ${styles} - + mwc-list { height: 100vh; overflow-y: scroll; diff --git a/src/editors/subscription/subscriber-ied-list.ts b/src/editors/subscription/subscriber-ied-list.ts index ac1c2cdbc5..bfd2bd8874 100644 --- a/src/editors/subscription/subscriber-ied-list.ts +++ b/src/editors/subscription/subscriber-ied-list.ts @@ -8,8 +8,27 @@ import { TemplateResult, } from 'lit-element'; import { translate } from 'lit-translate'; -import { GOOSESelectEvent } from '../../foundation.js'; -import { styles } from '../templates/foundation.js'; + +import '@material/mwc-icon'; +import '@material/mwc-list'; +import '@material/mwc-list/mwc-list-item'; + +import './elements/ied-element.js'; +import { + Create, + createElement, + Delete, + identity, + newActionEvent, + selector, +} from '../../foundation.js'; +import { + GOOSESelectEvent, + IEDSubscriptionEvent, + newGOOSESelectEvent, + styles, + SubscribeStatus, +} from './foundation.js'; /** * An IED within this IED list has 2 properties: @@ -25,40 +44,81 @@ interface IED { * All available FCDA references that are used to link ExtRefs. */ const fcdaReferences = [ - "ldInst", - "lnClass", - "lnInst", - "prefix", - "doName", - "daName", + 'ldInst', + 'lnClass', + 'lnInst', + 'prefix', + 'doName', + 'daName', ]; -/** An sub element for subscribing and unsubscribing IEDs to GOOSE messages. */ -@customElement('subscriber-ied-list') -export class SubscriberIEDList extends LitElement { - @property() - doc!: XMLDocument; +/** + * Get all the FCDA attributes containing values from a specific element. + * @param elementContainingFcdaReferences - The element to use + * @returns FCDA references + */ +function getFcdaReferences(elementContainingFcdaReferences: Element): string { + return fcdaReferences + .map(fcdaRef => + elementContainingFcdaReferences.getAttribute(fcdaRef) + ? `[${fcdaRef}="${elementContainingFcdaReferences.getAttribute(fcdaRef)}"]` + : '' + ) + .join(''); +} + +/** + * Internal persistent state, so it's not lost when + * subscribing / unsubscribing. + */ +interface State { + /** Current selected GSEControl element */ + currentGseControl: Element | undefined; + + /** The current selected dataset */ + currentDataset: Element | undefined; + + /** The name of the IED belonging to the current selected GOOSE */ + currentGooseIEDName: string | undefined | null; /** List holding all current subscribed IEDs. */ - subscribedIeds: IED[] = []; + subscribedIeds: IED[]; /** List holding all current avaialble IEDs which are not subscribed. */ - availableIeds: IED[] = []; + availableIeds: IED[]; +} - /** Current selected IED. */ - iedName!: string; +const localState: State = { + currentGseControl: undefined, + currentDataset: undefined, + currentGooseIEDName: undefined, + subscribedIeds: [], + availableIeds: [], +}; + +/** An sub element for subscribing and unsubscribing IEDs to GOOSE messages. */ +@customElement('subscriber-ied-list') +export class SubscriberIEDList extends LitElement { + @property({ attribute: false }) + doc!: XMLDocument; - /** Current selected GSEControl element. */ - gseControl!: Element; - @query('div') subscriberWrapper!: Element; constructor() { super(); this.onGOOSEDataSetEvent = this.onGOOSEDataSetEvent.bind(this); - const openScdElement = document.querySelector('open-scd'); - if (openScdElement) { - openScdElement.addEventListener('goose-dataset', this.onGOOSEDataSetEvent); + this.onIEDSubscriptionEvent = this.onIEDSubscriptionEvent.bind(this); + + const parentDiv = this.closest('div[id="containerTemplates"]'); + if (parentDiv) { + parentDiv.addEventListener( + 'goose-dataset', + this.onGOOSEDataSetEvent + ); + parentDiv.addEventListener( + 'ied-subscription', + this.onIEDSubscriptionEvent + ); } } @@ -68,139 +128,319 @@ export class SubscriberIEDList extends LitElement { * @param event - Incoming event. */ private async onGOOSEDataSetEvent(event: GOOSESelectEvent) { - this.iedName = event.detail.iedName; - this.gseControl = event.detail.gseControl; + localState.currentGseControl = event.detail.gseControl; + localState.currentDataset = event.detail.dataset; + localState.currentGooseIEDName = localState.currentGseControl + .closest('IED') + ?.getAttribute('name'); - const dataSet = event.detail.dataset; + localState.subscribedIeds = []; + localState.availableIeds = []; - this.clearIedLists(); + Array.from(this.doc.querySelectorAll(':root > IED')) + .filter(ied => ied.getAttribute('name') != localState.currentGooseIEDName) + .forEach(ied => { + const inputElements = ied.querySelectorAll(`LN0 > Inputs, LN > Inputs`); - Array.from(this.doc.querySelectorAll(':root > IED')).forEach(ied => { - const inputElements = ied.querySelectorAll(`LN0 > Inputs, LN > Inputs`); + let numberOfLinkedExtRefs = 0; - let numberOfLinkedExtRefs = 0; - - /** - * If no Inputs element is found, we can safely say it's not subscribed. - */ - if (!inputElements) { - this.availableIeds.push({element: ied}); - return; - } + /** + * If no Inputs element is found, we can safely say it's not subscribed. + */ + if (!inputElements) { + localState.availableIeds.push({ element: ied }); + return; + } - /** - * Count all the linked ExtRefs. - */ - dataSet.querySelectorAll('FCDA').forEach(fcda => { - inputElements.forEach(inputs => { - if(inputs.querySelector(`ExtRef[iedName=${event.detail.iedName}]` + - `${fcdaReferences.map(fcdaRef => - fcda.getAttribute(fcdaRef) - ? `[${fcdaRef}="${fcda.getAttribute(fcdaRef)}"]` - : '').join('') - }`)) { + /** + * Count all the linked ExtRefs. + */ + localState.currentDataset!.querySelectorAll('FCDA').forEach(fcda => { + inputElements.forEach(inputs => { + if ( + inputs.querySelector( + `ExtRef[iedName=${localState.currentGooseIEDName}]` + + `${getFcdaReferences(fcda)}` + ) + ) { numberOfLinkedExtRefs++; } - }) - }) + }); + }); + + /** + * Make a distinction between not subscribed at all, + * partially subscribed and fully subscribed. + */ + if (numberOfLinkedExtRefs == 0) { + localState.availableIeds.push({ element: ied }); + return; + } - /** - * Make a distinction between not subscribed at all, - * partially subscribed and fully subscribed. - */ - if (numberOfLinkedExtRefs == 0) { - this.availableIeds.push({element: ied}); - return; + if ( + numberOfLinkedExtRefs >= + localState.currentDataset!.querySelectorAll('FCDA').length + ) { + localState.subscribedIeds.push({ element: ied }); + } else { + localState.availableIeds.push({ element: ied, partial: true }); + } + }); + + this.requestUpdate(); + } + + /** + * When a IEDSubscriptionEvent is received, check if + * @param event - Incoming event. + */ + private async onIEDSubscriptionEvent(event: IEDSubscriptionEvent) { + switch (event.detail.subscribeStatus) { + case SubscribeStatus.Full: { + this.unsubscribe(event.detail.element); + break; + } + case SubscribeStatus.Partial: { + this.subscribe(event.detail.element); + break; + } + case SubscribeStatus.None: { + this.subscribe(event.detail.element); + break; } + } + } + + /** + * Full subscribe a given IED to the current dataset. + * @param ied - Given IED to subscribe. + */ + private async subscribe(ied: Element): Promise { + if (!ied.querySelector('LN0')) return; - if (numberOfLinkedExtRefs == dataSet.querySelectorAll('FCDA').length) { - this.subscribedIeds.push({element: ied}); - } else { - this.availableIeds.push({element: ied, partial: true}); + let inputsElement = ied.querySelector('LN0 > Inputs'); + if (!inputsElement) + inputsElement = createElement(ied.ownerDocument, 'Inputs', {}); + + const actions: Create[] = []; + localState.currentDataset!.querySelectorAll('FCDA').forEach(fcda => { + if ( + !inputsElement!.querySelector( + `ExtRef[iedName=${localState.currentGooseIEDName}]` + + `${getFcdaReferences(fcda)}` + ) + ) { + const extRef = createElement(ied.ownerDocument, 'ExtRef', { + iedName: localState.currentGooseIEDName!, + serviceType: 'GOOSE', + ldInst: fcda.getAttribute('ldInst') ?? '', + lnClass: fcda.getAttribute('lnClass') ?? '', + lnInst: fcda.getAttribute('lnInst') ?? '', + prefix: fcda.getAttribute('prefix') ?? '', + doName: fcda.getAttribute('doName') ?? '', + daName: fcda.getAttribute('daName') ?? '', + }); + + if (inputsElement?.parentElement) + actions.push({ new: { parent: inputsElement!, element: extRef } }); + else inputsElement?.appendChild(extRef); } + }); + + /** If the IED doesn't have a Inputs element, just append it to the first LN0 element. */ + const title = 'Connect'; + if (inputsElement.parentElement) { + this.dispatchEvent(newActionEvent({ title, actions })); + } else { + const inputAction: Create = { + new: { parent: ied.querySelector('LN0')!, element: inputsElement }, + }; + this.dispatchEvent(newActionEvent({ title, actions: [inputAction] })); + } + + this.dispatchEvent( + newGOOSESelectEvent( + localState.currentGseControl!, + localState.currentDataset! + ) + ); + } + + /** + * Unsubscribing a given IED to the current dataset. + * @param ied - Given IED to unsubscribe. + */ + private unsubscribe(ied: Element): void { + const actions: Delete[] = []; + ied.querySelectorAll('LN0 > Inputs, LN > Inputs').forEach(inputs => { + localState.currentDataset!.querySelectorAll('FCDA').forEach(fcda => { + const extRef = inputs.querySelector( + `ExtRef[iedName=${localState.currentGooseIEDName}]` + + `${getFcdaReferences(fcda)}` + ); + + if (extRef) actions.push({ old: { parent: inputs, element: extRef } }); + }); + - }) + }); - this.requestUpdate(); + this.dispatchEvent( + newActionEvent({ + title: 'Disconnect', + actions: this.extendDeleteActions(actions), + }) + ); + + this.dispatchEvent( + newGOOSESelectEvent( + localState.currentGseControl!, + localState.currentDataset! + ) + ); } /** - * Clear all the IED lists. + * Creating Delete actions in case Inputs elements are empty. + * @param extRefDeleteActions - All Delete actions for ExtRefs. + * @returns Possible delete actions for empty Inputs elements. */ - private clearIedLists() { - this.subscribedIeds = []; - this.availableIeds = []; + private extendDeleteActions(extRefDeleteActions: Delete[]): Delete[] { + if (!extRefDeleteActions.length) return []; + + // Initialize with the already existing ExtRef Delete actions. + const extendedDeleteActions: Delete[] = extRefDeleteActions; + const inputsMap: Record = {}; + + for (const extRefDeleteAction of extRefDeleteActions) { + const extRef = extRefDeleteAction.old.element; + const inputsElement = extRefDeleteAction.old.parent; + + const id = identity(inputsElement); + if (!inputsMap[id]) inputsMap[id] = (inputsElement.cloneNode(true)); + + const linkedExtRef = inputsMap[id].querySelector(`ExtRef[iedName=${extRef.getAttribute('iedName')}]` + + `${getFcdaReferences(extRef)}`); + + if (linkedExtRef) inputsMap[id].removeChild(linkedExtRef); + } + + // create delete action for each empty inputs + Object.entries(inputsMap).forEach(([key, value]) => { + if (value.children.length ! == 0) { + const doc = extRefDeleteActions[0].old.parent.ownerDocument!; + const inputs = doc.querySelector(selector('Inputs', key)); + + if (inputs && inputs.parentElement) { + extendedDeleteActions.push({ + old: { parent: inputs.parentElement, element: inputs }, + }); + } + } + }); + + return extendedDeleteActions; } protected updated(): void { if (this.subscriberWrapper) { - this.subscriberWrapper.scrollTo(0,0); + this.subscriberWrapper.scrollTo(0, 0); } } render(): TemplateResult { - const partialSubscribedIeds = this.availableIeds.filter(ied => ied.partial); - const gseControlName = this.gseControl?.getAttribute('name') ?? undefined; + const partialSubscribedIeds = localState.availableIeds.filter( + ied => ied.partial + ); + const availableIeds = localState.availableIeds.filter( + ied => !ied.partial + ); + const gseControlName = + localState.currentGseControl?.getAttribute('name') ?? undefined; return html`
    -

    ${translate('subscription.subscriberIed.title', { - selected: gseControlName ? this.iedName + ' > ' + gseControlName : 'IED' - })}

    - ${this.gseControl ? - html`
    - - - ${translate('subscription.subscriberIed.subscribed')} - -
  • - ${this.subscribedIeds.length > 0 ? - this.subscribedIeds.map(ied => html` - - ${ied.element.getAttribute('name')} - clear - `) - : html` - ${translate('subscription.none')} - `} -
    - +

    + ${translate('subscription.subscriberIed.title', { + selected: gseControlName + ? localState.currentGooseIEDName + ' > ' + gseControlName + : 'IED', + })} +

    + ${localState.currentGseControl + ? html`
    + + + ${translate('subscription.subscriberIed.subscribed')} + +
  • + ${localState.subscribedIeds.length > 0 + ? localState.subscribedIeds.map( + ied => + html`` + ) + : html` + ${translate('subscription.none')} + `} +
    + + + ${translate( + 'subscription.subscriberIed.partiallySubscribed' + )} + +
  • + ${partialSubscribedIeds.length > 0 + ? partialSubscribedIeds.map( + ied => + html`` + ) + : html` + ${translate('subscription.none')} + `} +
    + + + ${translate( + 'subscription.subscriberIed.availableToSubscribe' + )} + +
  • + ${availableIeds.length > 0 + ? availableIeds.map( + ied => + html`` + ) + : html` + ${translate('subscription.none')} + `} +
    +
    ` + : html` - ${translate('subscription.subscriberIed.partiallySubscribed')} - -
  • - ${partialSubscribedIeds.length > 0 ? - partialSubscribedIeds.map(ied => html` - - ${ied.element.getAttribute('name')} - add - `) - : html` - ${translate('subscription.none')} - `} -
    - - - ${translate('subscription.subscriberIed.availableToSubscribe')} - -
  • - ${this.availableIeds.length > 0 ? - this.availableIeds.map(ied => html` - - ${ied.element.getAttribute('name')} - add - `) - : html` - ${translate('subscription.none')} - `} -
    ` : html` - - ${translate('subscription.subscriberIed.noGooseMessageSelected')} + ${translate( + 'subscription.subscriberIed.noGooseMessageSelected' + )}
    `} -
    - `; + + `; } static styles = css` @@ -211,7 +451,7 @@ export class SubscriberIEDList extends LitElement { white-space: unset; text-overflow: unset; } - + .subscriberWrapper { height: 100vh; overflow-y: scroll; diff --git a/src/foundation.ts b/src/foundation.ts index 23d2d1b017..460a8048b2 100644 --- a/src/foundation.ts +++ b/src/foundation.ts @@ -280,26 +280,6 @@ export interface ResetDetail { kind: 'reset'; } -export interface GOOSESelectDetail { - iedName: string; - gseControl: Element; - dataset: Element; -} -export type GOOSESelectEvent = CustomEvent; -export function newGOOSESelectEvent( - iedName: string, - gseControl: Element, - dataset: Element, - eventInitDict?: CustomEventInit -): GOOSESelectEvent { - return new CustomEvent('goose-dataset', { - bubbles: true, - composed: true, - ...eventInitDict, - detail: { iedName, gseControl, dataset, ...eventInitDict?.detail }, - }); -} - export type LogDetail = InfoDetail | CommitDetail | ResetDetail; export type LogEvent = CustomEvent; export function newLogEvent( @@ -2657,7 +2637,6 @@ declare global { ['open-doc']: OpenDocEvent; ['wizard']: WizardEvent; ['validate']: ValidateEvent; - ['goose-dataset']: GOOSESelectEvent; ['log']: LogEvent; ['issue']: IssueEvent; } diff --git a/test/integration/editors/subscription/Subscription.test.ts b/test/integration/editors/subscription/Subscription.test.ts new file mode 100644 index 0000000000..49f6e12b43 --- /dev/null +++ b/test/integration/editors/subscription/Subscription.test.ts @@ -0,0 +1,78 @@ +import { html, fixture, expect } from '@open-wc/testing'; + +import '../../../mock-wizard.js'; + +import Subscription from '../../../../src/editors/Subscription.js'; +import { Editing } from '../../../../src/Editing.js'; +import { Wizarding } from '../../../../src/Wizarding.js'; + +describe('Subscription Plugin', () => { + customElements.define('subscription-plugin', Wizarding(Editing(Subscription))); + let element: Subscription; + let doc: XMLDocument; + + beforeEach(async () => { + doc = await fetch('/test/testfiles/valid2007B4ForSubscription.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + element = await fixture(html``); + }); + + describe('initially', () => { + it('the GOOSE list looks like the latest snapshot', async () => { + await expect(element.shadowRoot?.querySelector('publisher-goose-list')).shadowDom.to.equalSnapshot(); + }); + + it('the IED list looks like the latest snapshot', async () => { + await expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot(); + }); + }); + + describe('when selecting a GOOSE message', () => { + beforeEach(async () => { + const gseMsg = element.shadowRoot?.querySelector('publisher-goose-list') + ?.shadowRoot?.querySelectorAll('goose-message')[2].shadowRoot?.querySelector('mwc-list-item'); + + ((gseMsg)).click(); + }); + + it('the list on the right will initially show the subscribed / partially subscribed / not subscribed IEDs', async () => { + await element.updateComplete; + expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot(); + }); + + describe('and you subscribe a non-subscribed IED', () => { + it('it looks like the latest snapshot', async () => { + const ied = element.shadowRoot?.querySelector('subscriber-ied-list') + ?.shadowRoot?.querySelectorAll('ied-element')[2].shadowRoot?.querySelector('mwc-list-item'); + + ((ied)).click(); + await element.updateComplete; + expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot(); + }); + }); + + describe('and you unsubscribe a subscribed IED', () => { + it('it looks like the latest snapshot', async () => { + const ied = element.shadowRoot?.querySelector('subscriber-ied-list') + ?.shadowRoot?.querySelectorAll('ied-element')[0].shadowRoot?.querySelector('mwc-list-item'); + + ((ied)).click(); + await element.updateComplete; + expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot(); + }); + }); + + describe('and you subscribe a partially subscribed IED', () => { + it('it looks like the latest snapshot', async () => { + const ied = element.shadowRoot?.querySelector('subscriber-ied-list') + ?.shadowRoot?.querySelectorAll('ied-element')[1].shadowRoot?.querySelector('mwc-list-item'); + + ((ied)).click(); + await element.updateComplete; + expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot(); + }); + }); + }); +}); diff --git a/test/integration/editors/subscription/__snapshots__/Subscription.test.snap.js b/test/integration/editors/subscription/__snapshots__/Subscription.test.snap.js new file mode 100644 index 0000000000..9fdce293ca --- /dev/null +++ b/test/integration/editors/subscription/__snapshots__/Subscription.test.snap.js @@ -0,0 +1,388 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Subscription Plugin initially the GOOSE list looks like the latest snapshot"] = +`
    +

    + [subscription.publisherGoose.title] +

    + + + + IED1 + + + developer_board + + +
  • +
  • + + + + + + + IED2 + + + developer_board + + +
  • +
  • + + + + + IED4 + + + developer_board + + +
  • +
  • + + + + +
    +
    +`; +/* end snapshot Subscription Plugin initially the GOOSE list looks like the latest snapshot */ + +snapshots["Subscription Plugin initially the IED list looks like the latest snapshot"] = +`
    +

    + [subscription.subscriberIed.title] +

    + + + + [subscription.subscriberIed.noGooseMessageSelected] + + + +
    +`; +/* end snapshot Subscription Plugin initially the IED list looks like the latest snapshot */ + +snapshots["Subscription Plugin when selecting a GOOSE message the list on the right will initially show the subscribed / partially subscribed / not subscribed IEDs"] = +`
    +

    + [subscription.subscriberIed.title] +

    +
    + + + + [subscription.subscriberIed.subscribed] + + +
  • +
  • + + +
    + + + + [subscription.subscriberIed.partiallySubscribed] + + +
  • +
  • + + +
    + + + + [subscription.subscriberIed.availableToSubscribe] + + +
  • +
  • + + +
    +
    +
    +`; +/* end snapshot Subscription Plugin when selecting a GOOSE message the list on the right will initially show the subscribed / partially subscribed / not subscribed IEDs */ + +snapshots["Subscription Plugin when selecting a GOOSE message and you subscribe a non-subscribed IED it looks like the latest snapshot"] = +`
    +

    + [subscription.subscriberIed.title] +

    +
    + + + + [subscription.subscriberIed.subscribed] + + +
  • +
  • + + + + +
    + + + + [subscription.subscriberIed.partiallySubscribed] + + +
  • +
  • + + +
    + + + + [subscription.subscriberIed.availableToSubscribe] + + +
  • +
  • + + + [subscription.none] + + +
    +
    +
    +`; +/* end snapshot Subscription Plugin when selecting a GOOSE message and you subscribe a non-subscribed IED it looks like the latest snapshot */ + +snapshots["Subscription Plugin when selecting a GOOSE message and you unsubscribe a subscribed IED it looks like the latest snapshot"] = +`
    +

    + [subscription.subscriberIed.title] +

    +
    + + + + [subscription.subscriberIed.subscribed] + + +
  • +
  • + + + [subscription.none] + + +
    + + + + [subscription.subscriberIed.partiallySubscribed] + + +
  • +
  • + + +
    + + + + [subscription.subscriberIed.availableToSubscribe] + + +
  • +
  • + + + + +
    +
    +
    +`; +/* end snapshot Subscription Plugin when selecting a GOOSE message and you unsubscribe a subscribed IED it looks like the latest snapshot */ + +snapshots["Subscription Plugin when selecting a GOOSE message and you subscribe a partially subscribed IED it looks like the latest snapshot"] = +`
    +

    + [subscription.subscriberIed.title] +

    +
    + + + + [subscription.subscriberIed.subscribed] + + +
  • +
  • + + + + +
    + + + + [subscription.subscriberIed.partiallySubscribed] + + +
  • +
  • + + + [subscription.none] + + +
    + + + + [subscription.subscriberIed.availableToSubscribe] + + +
  • +
  • + + +
    +
    +
    +`; +/* end snapshot Subscription Plugin when selecting a GOOSE message and you subscribe a partially subscribed IED it looks like the latest snapshot */ + diff --git a/test/testfiles/valid2007B4ForSubscription.scd b/test/testfiles/valid2007B4ForSubscription.scd new file mode 100644 index 0000000000..b9ea199caa --- /dev/null +++ b/test/testfiles/valid2007B4ForSubscription.scd @@ -0,0 +1,787 @@ + + +
    + TrainingIEC61850 + + + +
    + + + 110.0 + + + + + + + + + + + + + + + + + + + + + + + + + 20 + + + + + + + 100.0 + +
    +

    192.168.210.111

    +

    255.255.255.0

    +

    192.168.210.1

    +

    1,3,9999,23

    +

    23

    +

    00000001

    +

    0001

    +

    0001

    +
    + +
    +

    01-0C-CD-01-00-10

    +

    005

    +

    4

    +

    0010

    +
    +
    + +

    RJ45

    +
    +
    +
    + + +
    +

    192.168.0.112

    +

    255.255.255.0

    +

    192.168.210.1

    +

    1,3,9999,23

    +

    23

    +

    00000001

    +

    0001

    +

    0001

    +
    +
    + +
    +

    192.168.0.113

    +

    255.255.255.0

    +

    192.168.210.1

    +

    1,3,9999,23

    +

    23

    +

    00000001

    +

    0001

    +

    0001

    +
    + +
    +

    01-0C-CD-04-00-20

    +

    007

    +

    4

    +

    4002

    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IED2 + + + + + + + status-only + + + + + + + sbo-with-enhanced-security + + + + + + + status-only + + + + + + + + 1 + + + + sbo-with-enhanced-security + + + + + + + + + + status-only + + + + + + + + + + + + + + + + status-only + + + + + + + + + direct-with-normal-security + + + + + + + sbo-with-normal-security + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + + + + + + + + + + + + status-only + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + status-only + + + + + + + + + + + status-only + + + + + + + direct-with-enhanced-security + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IED2 + + + + + + + status-only + + + + + + + sbo-with-enhanced-security + + + + + + + status-only + + + + + + + + 1 + + + + sbo-with-enhanced-security + + + + + + + + + + status-only + + + + + + + + + + + + + + status-only + + + + + + + + + direct-with-normal-security + + + + + + + sbo-with-normal-security + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sbo-with-enhanced-security + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + + + + + + + + + + + + + + + + + status-only + direct-with-normal-security + sbo-with-normal-security + direct-with-enhanced-security + sbo-with-enhanced-security + + + on + blocked + test + test/blocked + off + + + Ok + Warning + Alarm + + + not-supported + bay-control + station-control + remote-control + automatic-bay + automatic-station + automatic-remote + maintenance + process + + +
    \ No newline at end of file diff --git a/test/unit/editors/subscription/__snapshots__/publisher-goose-list.test.snap.js b/test/unit/editors/subscription/__snapshots__/publisher-goose-list.test.snap.js index ea177daf07..7bf81ed850 100644 --- a/test/unit/editors/subscription/__snapshots__/publisher-goose-list.test.snap.js +++ b/test/unit/editors/subscription/__snapshots__/publisher-goose-list.test.snap.js @@ -54,3 +54,14 @@ snapshots["publisher-goose-list looks like the latest snapshot"] = `; /* end snapshot publisher-goose-list looks like the latest snapshot */ +snapshots["publisher-goose-list looks like the latest snapshot without a doc loaded"] = +`
    +

    + [subscription.publisherGoose.title] +

    + + +
    +`; +/* end snapshot publisher-goose-list looks like the latest snapshot without a doc loaded */ + diff --git a/test/unit/editors/subscription/__snapshots__/subscriber-ied-list.test.snap.js b/test/unit/editors/subscription/__snapshots__/subscriber-ied-list.test.snap.js index 6fa76b0703..a3105d42a7 100644 --- a/test/unit/editors/subscription/__snapshots__/subscriber-ied-list.test.snap.js +++ b/test/unit/editors/subscription/__snapshots__/subscriber-ied-list.test.snap.js @@ -1,7 +1,7 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["subscriber-ied-list looks like the latest snapshot"] = +snapshots["subscriber-ied-list initially looks like the latest snapshot"] = `

    [subscription.subscriberIed.title] @@ -19,5 +19,5 @@ snapshots["subscriber-ied-list looks like the latest snapshot"] =

    `; -/* end snapshot subscriber-ied-list looks like the latest snapshot */ +/* end snapshot subscriber-ied-list initially looks like the latest snapshot */ diff --git a/test/unit/editors/subscription/elements/__snapshots__/goose-message.test.snap.js b/test/unit/editors/subscription/elements/__snapshots__/goose-message.test.snap.js new file mode 100644 index 0000000000..9e2e07de5d --- /dev/null +++ b/test/unit/editors/subscription/elements/__snapshots__/goose-message.test.snap.js @@ -0,0 +1,19 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["goose-message looks like the latest snapshot"] = +` + + GCB + + + + +`; +/* end snapshot goose-message looks like the latest snapshot */ + diff --git a/test/unit/editors/subscription/elements/__snapshots__/ied-element.test.snap.js b/test/unit/editors/subscription/elements/__snapshots__/ied-element.test.snap.js new file mode 100644 index 0000000000..f61bc9b17a --- /dev/null +++ b/test/unit/editors/subscription/elements/__snapshots__/ied-element.test.snap.js @@ -0,0 +1,21 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["ied-element looks like the latest snapshot"] = +` + + IED2 + + + add + + +`; +/* end snapshot ied-element looks like the latest snapshot */ + diff --git a/test/unit/editors/subscription/elements/goose-message.test.ts b/test/unit/editors/subscription/elements/goose-message.test.ts new file mode 100644 index 0000000000..2e4614149c --- /dev/null +++ b/test/unit/editors/subscription/elements/goose-message.test.ts @@ -0,0 +1,41 @@ +import { html, fixture, expect } from '@open-wc/testing'; +import { spy } from 'sinon'; + +import '../../../../../src/editors/subscription/elements/goose-message.js' +import { GOOSEMessage } from '../../../../../src/editors/subscription/elements/goose-message.js'; + +describe('goose-message', () => { + let element: GOOSEMessage; + let validSCL: XMLDocument; + + let gseControl: Element; + + beforeEach(async () => { + validSCL = await fetch('/test/testfiles/valid2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + gseControl = validSCL.querySelector('GSEControl[name="GCB"]')!; + + element = await fixture(html``); + }); + + it('a newGOOSESelectEvent is fired when clicking the goose message element', async () => { + const newGOOSESelectEvent = spy(); + window.addEventListener('goose-dataset', newGOOSESelectEvent); + + const listItem = (element.shadowRoot!.querySelector('mwc-list-item')); + listItem.click(); + + await element.requestUpdate(); + expect(newGOOSESelectEvent).to.have.been.called; + expect(newGOOSESelectEvent.args[0][0].detail['gseControl']).to.eql(gseControl); + expect(newGOOSESelectEvent.args[0][0].detail['dataset']).to.eql(gseControl.closest('IED')?.querySelector('DataSet[name="GooseDataSet1"]')); + }); + + it('looks like the latest snapshot', async () => { + await expect(element).shadowDom.to.equalSnapshot(); + }); +}); diff --git a/test/unit/editors/subscription/elements/ied-element.test.ts b/test/unit/editors/subscription/elements/ied-element.test.ts new file mode 100644 index 0000000000..06a5ba6a70 --- /dev/null +++ b/test/unit/editors/subscription/elements/ied-element.test.ts @@ -0,0 +1,42 @@ +import { html, fixture, expect } from '@open-wc/testing'; +import { spy } from 'sinon'; + +import '../../../../../src/editors/subscription/elements/ied-element.js' +import { IEDElement } from '../../../../../src/editors/subscription/elements/ied-element.js'; +import { SubscribeStatus } from '../../../../../src/editors/subscription/foundation.js'; + +describe('ied-element', () => { + let element: IEDElement; + let validSCL: XMLDocument; + + let iedElement: Element; + + beforeEach(async () => { + validSCL = await fetch('/test/testfiles/valid2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + iedElement = validSCL.querySelector('IED[name="IED2"]')!; + + element = await fixture(html``); + }); + + it('a newGOOSESelectEvent is fired when clicking the goose message element', async () => { + const newIEDSubscriptionEvent = spy(); + window.addEventListener('ied-subscription', newIEDSubscriptionEvent); + + const listItem = (element.shadowRoot!.querySelector('mwc-list-item')); + listItem.click(); + + await element.requestUpdate(); + expect(newIEDSubscriptionEvent).to.have.been.called; + expect(newIEDSubscriptionEvent.args[0][0].detail['element']).to.eql(iedElement); + expect(newIEDSubscriptionEvent.args[0][0].detail['subscribeStatus']).to.eql(SubscribeStatus.None); + }); + + it('looks like the latest snapshot', async () => { + await expect(element).shadowDom.to.equalSnapshot(); + }); +}); diff --git a/test/unit/editors/subscription/goose-message.test.ts b/test/unit/editors/subscription/goose-message.test.ts deleted file mode 100644 index fcd972e5d0..0000000000 --- a/test/unit/editors/subscription/goose-message.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { html, fixture, expect } from '@open-wc/testing'; - -import '../../../../src/editors/subscription/goose-message.js' -import { GOOSEMessage } from '../../../../src/editors/subscription/goose-message.js'; - -describe('goose-message', () => { - let element: GOOSEMessage; - let validSCL: XMLDocument; - - beforeEach(async () => { - validSCL = await fetch('/test/testfiles/valid2007B4.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element = await fixture(html``); - }); - - it('looks like the latest snapshot', async () => { - await expect(element).shadowDom.to.equalSnapshot(); - }); -}); diff --git a/test/unit/editors/subscription/publisher-goose-list.test.ts b/test/unit/editors/subscription/publisher-goose-list.test.ts index 0690448f07..19d7a87254 100644 --- a/test/unit/editors/subscription/publisher-goose-list.test.ts +++ b/test/unit/editors/subscription/publisher-goose-list.test.ts @@ -20,4 +20,10 @@ describe('publisher-goose-list', () => { it('looks like the latest snapshot', async () => { await expect(element).shadowDom.to.equalSnapshot(); }); + + it('looks like the latest snapshot without a doc loaded', async () => { + element = await fixture(html``); + + await expect(element).shadowDom.to.equalSnapshot(); + }); }); diff --git a/test/unit/editors/subscription/subscriber-ied-list.test.ts b/test/unit/editors/subscription/subscriber-ied-list.test.ts index 9bcc8aac86..4b3732dc13 100644 --- a/test/unit/editors/subscription/subscriber-ied-list.test.ts +++ b/test/unit/editors/subscription/subscriber-ied-list.test.ts @@ -17,7 +17,7 @@ describe('subscriber-ied-list', () => { >`); }); - it('looks like the latest snapshot', async () => { + it('initially looks like the latest snapshot', async () => { await expect(element).shadowDom.to.equalSnapshot(); }); });