diff --git a/public/js/plugins.js b/public/js/plugins.js
index c342d8e314..a1e4e11c67 100644
--- a/public/js/plugins.js
+++ b/public/js/plugins.js
@@ -20,6 +20,13 @@ export const officialPlugins = [
default: false,
kind: 'editor',
},
+ {
+ name: 'Subscription',
+ src: '/src/editors/Subscription.js',
+ icon: 'link',
+ default: true,
+ kind: 'editor',
+ },
{
name: 'Sampled Values Subscriber',
src: '/src/editors/SampledValues.js',
diff --git a/src/editors/SampledValues.ts b/src/editors/SampledValues.ts
index e71f101013..54edfeca4d 100644
--- a/src/editors/SampledValues.ts
+++ b/src/editors/SampledValues.ts
@@ -2,7 +2,7 @@ import { LitElement, html, TemplateResult, property, css } from 'lit-element';
import '@material/mwc-fab';
-import './sampledvalues/subscriber-ied-list.js';
+import './sampledvalues/subscriber-ied-list-smv.js';
import './sampledvalues/sampled-values-list.js';
/** An editor [[`plugin`]] for subscribing IEDs to Sampled Values. */
@@ -18,7 +18,7 @@ export default class SampledValuesPlugin extends LitElement {
`;
}
diff --git a/src/editors/Subscription.ts b/src/editors/Subscription.ts
new file mode 100644
index 0000000000..4c2b7baef8
--- /dev/null
+++ b/src/editors/Subscription.ts
@@ -0,0 +1,49 @@
+import { LitElement, html, TemplateResult, property, css } from 'lit-element';
+
+import '@material/mwc-fab';
+
+import './subscription/subscriber-ied-list-goose.js';
+import './subscription/publisher-goose-list.js';
+
+/** An editor [[`plugin`]] for subscribing IEDs to GOOSE messages using the ABB subscription method. */
+export default class SubscriptionABBPlugin extends LitElement {
+ /** The document being edited as provided to plugins by [[`OpenSCD`]]. */
+ @property()
+ doc!: XMLDocument;
+
+ render(): TemplateResult {
+ return html`
+
`;
+ }
+
+ static styles = css`
+ :host {
+ width: 100vw;
+ }
+
+ section {
+ width: 49vw;
+ }
+
+ #containerTemplates {
+ display: grid;
+ grid-gap: 12px;
+ padding: 8px 12px 16px;
+ box-sizing: border-box;
+ grid-template-columns: repeat(auto-fit, minmax(316px, auto));
+ }
+
+ @media (max-width: 387px) {
+ #containerTemplates {
+ grid-template-columns: repeat(auto-fit, minmax(196px, auto));
+ }
+ }
+ `;
+}
diff --git a/src/editors/sampledvalues/elements/ied-element.ts b/src/editors/sampledvalues/elements/ied-element-smv.ts
similarity index 92%
rename from src/editors/sampledvalues/elements/ied-element.ts
rename to src/editors/sampledvalues/elements/ied-element-smv.ts
index a940bdc799..c282443ae3 100644
--- a/src/editors/sampledvalues/elements/ied-element.ts
+++ b/src/editors/sampledvalues/elements/ied-element-smv.ts
@@ -11,8 +11,8 @@ import '@material/mwc-list/mwc-list-item';
import { newIEDSampledValuesSubscriptionEvent, SubscribeStatus } from '../foundation.js';
-@customElement('ied-element')
-export class IEDElement extends LitElement {
+@customElement('ied-element-smv')
+export class IEDElementSmv extends LitElement {
/** Holding the IED element */
@property({ attribute: false })
element!: Element;
diff --git a/src/editors/sampledvalues/subscriber-ied-list.ts b/src/editors/sampledvalues/subscriber-ied-list-smv.ts
similarity index 96%
rename from src/editors/sampledvalues/subscriber-ied-list.ts
rename to src/editors/sampledvalues/subscriber-ied-list-smv.ts
index 5ed0f258d2..159b4cb1a2 100644
--- a/src/editors/sampledvalues/subscriber-ied-list.ts
+++ b/src/editors/sampledvalues/subscriber-ied-list-smv.ts
@@ -13,7 +13,7 @@ import '@material/mwc-icon';
import '@material/mwc-list';
import '@material/mwc-list/mwc-list-item';
-import './elements/ied-element.js';
+import './elements/ied-element-smv.js';
import {
Create,
createElement,
@@ -97,8 +97,8 @@ const localState: State = {
};
/** An sub element for subscribing and unsubscribing IEDs to Sampled Values messages. */
-@customElement('subscriber-ied-list')
-export class SubscriberIEDList extends LitElement {
+@customElement('subscriber-ied-list-smv')
+export class SubscriberIEDListSmv extends LitElement {
@property({ attribute: false })
doc!: XMLDocument;
@@ -128,6 +128,7 @@ export class SubscriberIEDList extends LitElement {
* @param event - Incoming event.
*/
private async onSampledValuesDataSetEvent(event: SampledValuesSelectEvent) {
+ console.log('onSMVSelect')
localState.currentSampledValuesControl = event.detail.sampledValuesControl;
localState.currentDataset = event.detail.dataset;
localState.currentSampledValuesIEDName = localState.currentSampledValuesControl
@@ -195,6 +196,7 @@ export class SubscriberIEDList extends LitElement {
* @param event - Incoming event.
*/
private async onIEDSubscriptionEvent(event: IEDSampledValuesSubscriptionEvent) {
+ console.log('onSMVIEDSub')
switch (event.detail.subscribeStatus) {
case SubscribeStatus.Full: {
this.unsubscribe(event.detail.ied);
@@ -377,10 +379,10 @@ export class SubscriberIEDList extends LitElement {
${localState.subscribedIeds.length > 0
? localState.subscribedIeds.map(
ied =>
- html``
+ >`
)
: html`
${translate('sampledvalues.none')}
@@ -398,10 +400,10 @@ export class SubscriberIEDList extends LitElement {
${partialSubscribedIeds.length > 0
? partialSubscribedIeds.map(
ied =>
- html``
+ >`
)
: html`
${translate('sampledvalues.none')}
@@ -419,10 +421,10 @@ export class SubscriberIEDList extends LitElement {
${availableIeds.length > 0
? availableIeds.map(
ied =>
- html``
+ >`
)
: html`
${translate('sampledvalues.none')}
diff --git a/src/editors/subscription/elements/goose-message.ts b/src/editors/subscription/elements/goose-message.ts
new file mode 100644
index 0000000000..3a693a3864
--- /dev/null
+++ b/src/editors/subscription/elements/goose-message.ts
@@ -0,0 +1,35 @@
+import {
+ customElement,
+ html,
+ LitElement,
+ property,
+ TemplateResult,
+} from 'lit-element';
+
+import '@material/mwc-icon';
+import '@material/mwc-list/mwc-list-item';
+
+import { newGOOSESelectEvent } from '../foundation.js';
+import { gooseIcon } from '../../../icons/icons.js';
+
+@customElement('goose-message')
+export class GOOSEMessage extends LitElement {
+ /** Holding the GSEControl element */
+ @property({ attribute: false })
+ element!: Element;
+
+ private onGooseSelect = () => {
+ const ln = this.element.parentElement;
+ const dataset = ln?.querySelector(
+ `DataSet[name=${this.element.getAttribute('datSet')}]`
+ );
+ this.dispatchEvent(newGOOSESelectEvent(this.element, dataset!));
+ };
+
+ render(): TemplateResult {
+ return html`
+ ${this.element.getAttribute('name')}
+ ${gooseIcon}
+ `;
+ }
+}
diff --git a/src/editors/subscription/elements/ied-element-goose.ts b/src/editors/subscription/elements/ied-element-goose.ts
new file mode 100644
index 0000000000..3771727c5a
--- /dev/null
+++ b/src/editors/subscription/elements/ied-element-goose.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-goose')
+export class IEDElementGoose 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
new file mode 100644
index 0000000000..146229b78e
--- /dev/null
+++ b/src/editors/subscription/publisher-goose-list.ts
@@ -0,0 +1,81 @@
+import {
+ css,
+ customElement,
+ html,
+ LitElement,
+ property,
+ TemplateResult,
+} from 'lit-element';
+import { translate } from 'lit-translate';
+
+import '@material/mwc-icon';
+import '@material/mwc-list';
+import '@material/mwc-list/mwc-list-item';
+
+import './elements/goose-message.js';
+import { compareNames, getNameAttribute } from '../../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({ attribute: false })
+ doc!: XMLDocument;
+
+ private get ieds(): Element[] {
+ return this.doc
+ ? Array.from(this.doc.querySelectorAll(':root > IED')).sort((a, b) =>
+ compareNames(a, b)
+ )
+ : [];
+ }
+
+ /**
+ * Get all the published GOOSE messages.
+ * @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 > GSEControl'
+ )
+ );
+ }
+
+ render(): TemplateResult {
+ return html`
+ ${translate('subscription.publisherGoose.title')}
+
+ ${this.ieds.map(ied =>
+ 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;
+ }
+
+ .iedListTitle {
+ font-weight: bold;
+ }
+ `;
+}
diff --git a/src/editors/subscription/subscriber-ied-list-goose.ts b/src/editors/subscription/subscriber-ied-list-goose.ts
new file mode 100644
index 0000000000..ef837f1e0e
--- /dev/null
+++ b/src/editors/subscription/subscriber-ied-list-goose.ts
@@ -0,0 +1,466 @@
+import {
+ css,
+ customElement,
+ html,
+ LitElement,
+ property,
+ query,
+ TemplateResult,
+} from 'lit-element';
+import { translate } from 'lit-translate';
+
+import '@material/mwc-icon';
+import '@material/mwc-list';
+import '@material/mwc-list/mwc-list-item';
+
+import './elements/ied-element-goose.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:
+ * - The IED element itself.
+ * - A 'partial' property indicating if the GOOSE is fully initialized or partially.
+ */
+interface IED {
+ element: Element;
+ partial?: boolean;
+}
+
+/**
+ * All available FCDA references that are used to link ExtRefs.
+ */
+const fcdaReferences = [
+ 'ldInst',
+ 'lnClass',
+ 'lnInst',
+ 'prefix',
+ 'doName',
+ 'daName',
+];
+
+/**
+ * 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[];
+
+ /** List holding all current avaialble IEDs which are not subscribed. */
+ availableIeds: IED[];
+}
+
+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-goose')
+export class SubscriberIEDListGoose extends LitElement {
+ @property({ attribute: false })
+ doc!: XMLDocument;
+
+ @query('div') subscriberWrapper!: Element;
+
+ constructor() {
+ super();
+ this.onGOOSEDataSetEvent = this.onGOOSEDataSetEvent.bind(this);
+ 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
+ );
+ }
+ }
+
+ /**
+ * When a GOOSEDataSetEvent is received, check for all IEDs if
+ * all FCDAs are covered, or partly FCDAs are covered.
+ * @param event - Incoming event.
+ */
+ private async onGOOSEDataSetEvent(event: GOOSESelectEvent) {
+ console.log('onGOOSESelect')
+ localState.currentGseControl = event.detail.gseControl;
+ localState.currentDataset = event.detail.dataset;
+ localState.currentGooseIEDName = localState.currentGseControl
+ .closest('IED')
+ ?.getAttribute('name');
+
+ localState.subscribedIeds = [];
+ localState.availableIeds = [];
+
+ Array.from(this.doc.querySelectorAll(':root > IED'))
+ .filter(ied => ied.getAttribute('name') != localState.currentGooseIEDName)
+ .forEach(ied => {
+ const inputElements = ied.querySelectorAll(`LN0 > Inputs, LN > Inputs`);
+
+ let numberOfLinkedExtRefs = 0;
+
+ /**
+ * 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.
+ */
+ 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;
+ }
+
+ 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) {
+ console.log('onGOOSEIEDSub')
+ 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;
+
+ 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.dispatchEvent(
+ newActionEvent({
+ title: 'Disconnect',
+ actions: this.extendDeleteActions(actions),
+ })
+ );
+
+ this.dispatchEvent(
+ newGOOSESelectEvent(
+ localState.currentGseControl!,
+ localState.currentDataset!
+ )
+ );
+ }
+
+ /**
+ * 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 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);
+ }
+ }
+
+ render(): TemplateResult {
+ 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
+ ? 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.noGooseMessageSelected'
+ )}
+
+
+ `}
+
+ `;
+ }
+
+ static styles = css`
+ ${styles}
+
+ h1 {
+ overflow: unset;
+ white-space: unset;
+ text-overflow: unset;
+ }
+
+ .subscriberWrapper {
+ height: 100vh;
+ overflow-y: scroll;
+ }
+
+ .iedListTitle {
+ font-weight: bold;
+ }
+ `;
+}
diff --git a/src/translations/de.ts b/src/translations/de.ts
index b05573fed6..3310787e61 100644
--- a/src/translations/de.ts
+++ b/src/translations/de.ts
@@ -290,6 +290,19 @@ export const de: Translations = {
missing: 'DataTypeTemplates fehlen',
add: 'DataTypeTemplates hinzufügen',
},
+ subscription: {
+ none: 'Keine Verbindung vorhanden',
+ publisherGoose: {
+ title: 'GOOSE-Publizierer'
+ },
+ subscriberIed: {
+ title: 'Verbunden mit {{ selected }}',
+ subscribed: 'Verbunden',
+ availableToSubscribe: 'Kann verbunden werden',
+ partiallySubscribed: 'Teilweise verbunden',
+ noGooseMessageSelected: 'Keine GOOSE ausgewählt'
+ }
+ },
sampledvalues: {
none: 'Keine Verbindung vorhanden',
sampledValuesList: {
diff --git a/src/translations/en.ts b/src/translations/en.ts
index 5e6d8cab82..3182a72387 100644
--- a/src/translations/en.ts
+++ b/src/translations/en.ts
@@ -286,6 +286,19 @@ export const en = {
missing: 'DataTypeTemplates missing',
add: 'Add DataTypeTemplates',
},
+ subscription: {
+ none: 'None',
+ publisherGoose: {
+ title: 'GOOSE publisher'
+ },
+ subscriberIed: {
+ title: 'Subscriber of {{ selected }}',
+ subscribed: 'Subscribed',
+ availableToSubscribe: 'Available to subscribe',
+ partiallySubscribed: 'Partially subscribed',
+ noGooseMessageSelected: 'No GOOSE message selected'
+ }
+ },
sampledvalues: {
none: 'none',
sampledValuesList: {
diff --git a/test/integration/__snapshots__/open-scd.test.snap.js b/test/integration/__snapshots__/open-scd.test.snap.js
index a576dac150..1729df09d5 100644
--- a/test/integration/__snapshots__/open-scd.test.snap.js
+++ b/test/integration/__snapshots__/open-scd.test.snap.js
@@ -604,6 +604,22 @@ snapshots["open-scd looks like its snapshot"] =
Single Line Diagram
+
+
+ link
+
+ Subscription
+
{
});
it('the IED list looks like the latest snapshot', async () => {
- await expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot();
+ await expect(element.shadowRoot?.querySelector('subscriber-ied-list-smv')).shadowDom.to.equalSnapshot();
});
});
@@ -39,39 +39,39 @@ describe('Sampled Values Plugin', () => {
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();
+ expect(element.shadowRoot?.querySelector('subscriber-ied-list-smv')).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');
+ const ied = element.shadowRoot?.querySelector('subscriber-ied-list-smv')
+ ?.shadowRoot?.querySelectorAll('ied-element-smv')[2].shadowRoot?.querySelector('mwc-list-item');
((ied)).click();
await element.updateComplete;
- expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot();
+ expect(element.shadowRoot?.querySelector('subscriber-ied-list-smv')).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');
+ const ied = element.shadowRoot?.querySelector('subscriber-ied-list-smv')
+ ?.shadowRoot?.querySelectorAll('ied-element-smv')[0].shadowRoot?.querySelector('mwc-list-item');
((ied)).click();
await element.updateComplete;
- expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot();
+ expect(element.shadowRoot?.querySelector('subscriber-ied-list-smv')).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');
+ const ied = element.shadowRoot?.querySelector('subscriber-ied-list-smv')
+ ?.shadowRoot?.querySelectorAll('ied-element-smv')[1].shadowRoot?.querySelector('mwc-list-item');
((ied)).click();
await element.updateComplete;
- expect(element.shadowRoot?.querySelector('subscriber-ied-list')).shadowDom.to.equalSnapshot();
+ expect(element.shadowRoot?.querySelector('subscriber-ied-list-smv')).shadowDom.to.equalSnapshot();
});
});
});
diff --git a/test/integration/editors/subscription/Subscription.test.ts b/test/integration/editors/subscription/Subscription.test.ts
new file mode 100644
index 0000000000..f64df1ece0
--- /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-goose')).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-goose')).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-goose')
+ ?.shadowRoot?.querySelectorAll('ied-element-goose')[2].shadowRoot?.querySelector('mwc-list-item');
+
+ ((ied)).click();
+ await element.updateComplete;
+ expect(element.shadowRoot?.querySelector('subscriber-ied-list-goose')).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-goose')
+ ?.shadowRoot?.querySelectorAll('ied-element-goose')[0].shadowRoot?.querySelector('mwc-list-item');
+
+ ((ied)).click();
+ await element.updateComplete;
+ expect(element.shadowRoot?.querySelector('subscriber-ied-list-goose')).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-goose')
+ ?.shadowRoot?.querySelectorAll('ied-element-goose')[1].shadowRoot?.querySelector('mwc-list-item');
+
+ ((ied)).click();
+ await element.updateComplete;
+ expect(element.shadowRoot?.querySelector('subscriber-ied-list-goose')).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/Plugging.test.ts b/test/unit/Plugging.test.ts
index f95ff03fb8..2f0f276f5f 100644
--- a/test/unit/Plugging.test.ts
+++ b/test/unit/Plugging.test.ts
@@ -22,7 +22,7 @@ describe('PluggingElement', () => {
});
it('stores default plugins on load', () =>
- expect(element).property('editors').to.have.lengthOf(4));
+ expect(element).property('editors').to.have.lengthOf(5));
describe('plugin manager dialog', () => {
let firstEditorPlugin: HTMLElement;
@@ -49,7 +49,7 @@ describe('PluggingElement', () => {
it('disables deselected plugins', async () => {
firstEditorPlugin.click();
await element.updateComplete;
- expect(element).property('editors').to.have.lengthOf(3);
+ expect(element).property('editors').to.have.lengthOf(4);
});
it('enables selected plugins', async () => {
@@ -57,7 +57,7 @@ describe('PluggingElement', () => {
await element.updateComplete;
(element.pluginList.firstElementChild).click();
await element.updateComplete;
- expect(element).property('editors').to.have.lengthOf(4);
+ expect(element).property('editors').to.have.lengthOf(5);
});
it('resets plugins to default on reset button click', async () => {
@@ -65,7 +65,7 @@ describe('PluggingElement', () => {
await element.updateComplete;
resetAction.click();
await element.updateComplete;
- expect(element).property('editors').to.have.lengthOf(4);
+ expect(element).property('editors').to.have.lengthOf(5);
});
it('opens the custom plugin dialog on add button click', async () => {
@@ -139,7 +139,7 @@ describe('PluggingElement', () => {
await name.updateComplete;
primaryAction.click();
await element.updateComplete;
- expect(element.editors).to.have.lengthOf(5);
+ expect(element.editors).to.have.lengthOf(6);
});
it('adds a new menu kind plugin on add button click', async () => {
const lengthMenuKindPlugins = element.menuEntries.length;
diff --git a/test/unit/editors/subscription/__snapshots__/goose-message.test.snap.js b/test/unit/editors/subscription/__snapshots__/goose-message.test.snap.js
new file mode 100644
index 0000000000..9e2e07de5d
--- /dev/null
+++ b/test/unit/editors/subscription/__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/__snapshots__/publisher-goose-list.test.snap.js b/test/unit/editors/subscription/__snapshots__/publisher-goose-list.test.snap.js
new file mode 100644
index 0000000000..7bf81ed850
--- /dev/null
+++ b/test/unit/editors/subscription/__snapshots__/publisher-goose-list.test.snap.js
@@ -0,0 +1,67 @@
+/* @web/test-runner snapshot v1 */
+export const snapshots = {};
+
+snapshots["publisher-goose-list looks like the latest snapshot"] =
+`
+
+ [subscription.publisherGoose.title]
+
+
+
+
+ IED1
+
+
+ developer_board
+
+
+
+
+
+
+
+
+
+
+ IED2
+
+
+ developer_board
+
+
+
+
+
+
+
+
+`;
+/* 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
new file mode 100644
index 0000000000..a3105d42a7
--- /dev/null
+++ b/test/unit/editors/subscription/__snapshots__/subscriber-ied-list.test.snap.js
@@ -0,0 +1,23 @@
+/* @web/test-runner snapshot v1 */
+export const snapshots = {};
+
+snapshots["subscriber-ied-list initially looks like the latest snapshot"] =
+`
+
+ [subscription.subscriberIed.title]
+
+
+
+
+ [subscription.subscriberIed.noGooseMessageSelected]
+
+
+
+
+`;
+/* 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..185d857855
--- /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-goose.js'
+import { IEDElementGoose } from '../../../../../src/editors/subscription/elements/ied-element-goose.js';
+import { SubscribeStatus } from '../../../../../src/editors/subscription/foundation.js';
+
+describe('ied-element', () => {
+ let element: IEDElementGoose;
+ 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/publisher-goose-list.test.ts b/test/unit/editors/subscription/publisher-goose-list.test.ts
new file mode 100644
index 0000000000..19d7a87254
--- /dev/null
+++ b/test/unit/editors/subscription/publisher-goose-list.test.ts
@@ -0,0 +1,29 @@
+import { html, fixture, expect } from '@open-wc/testing';
+
+import '../../../../src/editors/subscription/publisher-goose-list.js'
+import { PublisherGOOSEList } from '../../../../src/editors/subscription/publisher-goose-list.js';
+
+describe('publisher-goose-list', () => {
+ let element: PublisherGOOSEList;
+ 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();
+ });
+
+ 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
new file mode 100644
index 0000000000..f5b8dca64f
--- /dev/null
+++ b/test/unit/editors/subscription/subscriber-ied-list.test.ts
@@ -0,0 +1,23 @@
+import { html, fixture, expect } from '@open-wc/testing';
+
+import '../../../../src/editors/subscription/subscriber-ied-list-goose.js'
+import { SubscriberIEDListGoose } from '../../../../src/editors/subscription/subscriber-ied-list-goose.js';
+
+describe('subscriber-ied-list', () => {
+ let element: SubscriberIEDListGoose;
+ 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('initially looks like the latest snapshot', async () => {
+ await expect(element).shadowDom.to.equalSnapshot();
+ });
+});