diff --git a/public/js/plugins.js b/public/js/plugins.js index b4c9625373..47e7e8b3e1 100644 --- a/public/js/plugins.js +++ b/public/js/plugins.js @@ -62,6 +62,13 @@ export const officialPlugins = [ default: false, kind: 'editor', }, + { + name: 'Cleanup', + src: '/src/editors/Cleanup.js', + icon: 'cleaning_services', + default: false, + kind: 'editor', + }, { name: 'Open project', src: '/src/menu/OpenProject.js', @@ -119,7 +126,7 @@ export const officialPlugins = [ default: false, kind: 'menu', requireDoc: true, - position: 'middle' + position: 'middle', }, { name: 'Subscriber Update', @@ -164,11 +171,13 @@ export const officialPlugins = [ position: 'middle', }, { - name: 'Cleanup', - src: '/src/editors/Cleanup.js', - icon: 'cleaning_services', + name: 'Compare IED', + src: '/src/menu/CompareIED.js', + icon: 'compare_arrows', default: false, - kind: 'editor', + kind: 'menu', + requireDoc: true, + position: 'middle', }, { name: 'Help', diff --git a/src/foundation/compare.ts b/src/foundation/compare.ts new file mode 100644 index 0000000000..df052be9e2 --- /dev/null +++ b/src/foundation/compare.ts @@ -0,0 +1,257 @@ +import { html, TemplateResult } from 'lit-element'; +import { repeat } from 'lit-html/directives/repeat'; +import { get, translate } from 'lit-translate'; + +import '@material/mwc-list'; +import '@material/mwc-list/mwc-list-item'; +import '@material/mwc-icon'; + +import { identity } from '../foundation.js'; +import { nothing } from 'lit-html'; + +export type Diff = + | { oldValue: T; newValue: null } + | { oldValue: null; newValue: T } + | { oldValue: T; newValue: T }; + +/** + * Returns the description of the Element that differs. + * + * @param element - The Element to retrieve the identifier from. + */ +function describe(element: Element): string { + const id = identity(element); + return typeof id === 'string' ? id : get('unidentifiable'); +} + +/** + * Check if there are any attribute values changed between the two elements. + * + * @param elementToBeCompared - The element to check for differences. + * @param elementToCompareAgainst - The element used to check against. + */ +export function diffSclAttributes( + elementToBeCompared: Element, + elementToCompareAgainst: Element +): [string, Diff][] { + const attrDiffs: [string, Diff][] = []; + + // First check if there is any text inside the element and there should be no child elements. + const newText = elementToBeCompared.textContent?.trim() ?? ''; + const oldText = elementToCompareAgainst.textContent?.trim() ?? ''; + if ( + elementToBeCompared.childElementCount === 0 && + elementToCompareAgainst.childElementCount === 0 && + newText !== oldText + ) { + attrDiffs.push(['value', { newValue: newText, oldValue: oldText }]); + } + + // Next check if there are any difference between attributes. + const attributeNames = new Set( + elementToCompareAgainst + .getAttributeNames() + .concat(elementToBeCompared.getAttributeNames()) + ); + for (const name of attributeNames) { + if ( + elementToCompareAgainst.getAttribute(name) !== + elementToBeCompared.getAttribute(name) + ) { + attrDiffs.push([ + name, + >{ + newValue: elementToBeCompared.getAttribute(name), + oldValue: elementToCompareAgainst.getAttribute(name), + }, + ]); + } + } + return attrDiffs; +} + +/** + * Function to retrieve the identity to compare 2 children on the same level. + * This means we only need to last part of the Identity string to compare the children. + * + * @param element - The element to retrieve the identity from. + */ +export function identityForCompare(element: Element): string | number { + let identityOfElement = identity(element); + if (typeof identityOfElement === 'string') { + identityOfElement = identityOfElement.split('>').pop() ?? ''; + } + return identityOfElement; +} + +/** + * Custom method for comparing to check if 2 elements are the same. Because they are on the same level + * we don't need to compare the full identity, we just compare the part of the Element itself. + * + * RemarkPrivate elements are already filtered out, so we don't need to bother them. + * + * @param newValue - The new element to compare with the old element. + * @param oldValue - The old element to which the new element is compared. + */ +export function isSame(newValue: Element, oldValue: Element): boolean { + return ( + newValue.tagName === oldValue.tagName && + identityForCompare(newValue) === identityForCompare(oldValue) + ); +} + +/** + * List of all differences between children elements that both old and new element have. + * The list contains children both elements have and children that were added or removed + * from the new element. + * Remark: Private elements are ignored. + * + * @param elementToBeCompared - The element to check for differences. + * @param elementToCompareAgainst - The element used to check against. + */ +export function diffSclChilds( + elementToBeCompared: Element, + elementToCompareAgainst: Element +): Diff[] { + const childDiffs: Diff[] = []; + const childrenToBeCompared = Array.from(elementToBeCompared.children); + const childrenToCompareTo = Array.from(elementToCompareAgainst.children); + + childrenToBeCompared.forEach(newElement => { + if (!newElement.closest('Private')) { + const twinIndex = childrenToCompareTo.findIndex(ourChild => + isSame(newElement, ourChild) + ); + const oldElement = twinIndex > -1 ? childrenToCompareTo[twinIndex] : null; + + if (oldElement) { + childrenToCompareTo.splice(twinIndex, 1); + childDiffs.push({ newValue: newElement, oldValue: oldElement }); + } else { + childDiffs.push({ newValue: newElement, oldValue: null }); + } + } + }); + childrenToCompareTo.forEach(oldElement => { + if (!oldElement.closest('Private')) { + childDiffs.push({ newValue: null, oldValue: oldElement }); + } + }); + return childDiffs; +} + +/** + * Generate HTML (TemplateResult) containing all the differences between the two elements passed. + * If null is returned there are no differences between the two elements. + * + * @param elementToBeCompared - The element to check for differences. + * @param elementToCompareAgainst - The element used to check against. + */ +export function renderDiff( + elementToBeCompared: Element, + elementToCompareAgainst: Element +): TemplateResult | null { + // Determine the ID from the current tag. These can be numbers or strings. + let idTitle: string | undefined = identity(elementToBeCompared).toString(); + if (idTitle === 'NaN') { + idTitle = undefined; + } + + // First get all differences in attributes and text for the current 2 elements. + const attrDiffs: [string, Diff][] = diffSclAttributes( + elementToBeCompared, + elementToCompareAgainst + ); + // Next check which elements are added, deleted or in both elements. + const childDiffs: Diff[] = diffSclChilds( + elementToBeCompared, + elementToCompareAgainst + ); + + const childAddedOrDeleted: Diff[] = []; + const childToCompare: Diff[] = []; + childDiffs.forEach(diff => { + if (!diff.oldValue || !diff.newValue) { + childAddedOrDeleted.push(diff); + } else { + childToCompare.push(diff); + } + }); + + // These children exist in both old and new element, let's check if there are any difference in the children. + const childToCompareTemplates = childToCompare + .map(diff => renderDiff(diff.newValue!, diff.oldValue!)) + .filter(result => result !== null); + + // If there are difference generate the HTML otherwise just return null. + if ( + childToCompareTemplates.length > 0 || + attrDiffs.length > 0 || + childAddedOrDeleted.length > 0 + ) { + return html` ${attrDiffs.length > 0 || childAddedOrDeleted.length > 0 + ? html` + ${attrDiffs.length > 0 + ? html` + + ${translate('compare.attributes', { + elementName: elementToBeCompared.tagName, + })} + + ${idTitle + ? html`${idTitle}` + : nothing} + +
  • ` + : ''} + ${repeat( + attrDiffs, + e => e, + ([name, diff]) => + html` + ${name} + + ${diff.oldValue ?? ''} + ${diff.oldValue && diff.newValue ? html`↷` : ' '} + ${diff.newValue ?? ''} + + + ${diff.oldValue ? (diff.newValue ? 'edit' : 'delete') : 'add'} + + ` + )} + ${childAddedOrDeleted.length > 0 + ? html` + + ${translate('compare.children', { + elementName: elementToBeCompared.tagName, + })} + + ${idTitle + ? html`${idTitle}` + : nothing} + +
  • ` + : ''} + ${repeat( + childAddedOrDeleted, + e => e, + diff => + html` + ${diff.oldValue?.tagName ?? diff.newValue?.tagName} + + ${diff.oldValue + ? describe(diff.oldValue) + : describe(diff.newValue)} + + + ${diff.oldValue ? 'delete' : 'add'} + + ` + )} +
    ` + : ''} + ${childToCompareTemplates}`; + } + return null; +} diff --git a/src/menu/CompareIED.ts b/src/menu/CompareIED.ts new file mode 100644 index 0000000000..93218e1dad --- /dev/null +++ b/src/menu/CompareIED.ts @@ -0,0 +1,260 @@ +import { + css, + html, + LitElement, + property, + query, + TemplateResult, +} from 'lit-element'; +import { get, translate } from 'lit-translate'; + +import '@material/mwc-dialog'; +import '@material/mwc-list'; +import '@material/mwc-list/mwc-list-item'; + +import { Dialog } from '@material/mwc-dialog'; +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; +import { List } from '@material/mwc-list'; + +import { + compareNames, + getNameAttribute, + identity, + newPendingStateEvent, + selector, +} from '../foundation.js'; +import { renderDiff } from '../foundation/compare.js'; + +export default class CompareIEDPlugin extends LitElement { + @property({ attribute: false }) + doc!: XMLDocument; + + @property({ attribute: false }) + templateDoc: XMLDocument | undefined; + + @property({ attribute: false }) + selectedProjectIed: Element | undefined; + + @property({ attribute: false }) + selectedTemplateIed: Element | undefined; + + @query('mwc-dialog') + dialog!: Dialog; + + @query('#template-file') + private templateFileUI!: HTMLInputElement; + + get ieds(): Element[] { + if (this.doc) { + return Array.from(this.doc.querySelectorAll(`IED`)).sort(compareNames); + } + return []; + } + + get templateIeds(): Element[] { + if (this.templateDoc) { + return Array.from(this.templateDoc.querySelectorAll(`IED`)).sort( + compareNames + ); + } + return []; + } + + async run(): Promise { + this.dialog.open = true; + } + + private onClosed(): void { + this.templateDoc = undefined; + this.selectedProjectIed = undefined; + this.selectedTemplateIed = undefined; + } + + private getSelectedListItem( + doc: Document, + listId: string + ): Element | undefined { + const selectListItem = ( + (this.shadowRoot!.querySelector(`mwc-list[id='${listId}']`)!) + .selected + ); + const identity = selectListItem?.value; + if (identity) { + return doc.querySelector(selector('IED', identity)) ?? undefined; + } + return undefined; + } + + private async getTemplateFile(evt: Event): Promise { + const file = (evt.target)?.files?.item(0) ?? false; + if (!file) return; + + const templateText = await file.text(); + this.templateDoc = new DOMParser().parseFromString( + templateText, + 'application/xml' + ); + this.templateFileUI.onchange = null; + } + + private renderSelectIedButton(): TemplateResult { + return html` { + this.selectedProjectIed = undefined; + this.selectedTemplateIed = undefined; + }} + >`; + } + + private renderCompareButton(): TemplateResult { + return html` { + this.selectedProjectIed = this.getSelectedListItem( + this.doc, + 'currentDocument' + ); + this.selectedTemplateIed = this.getSelectedListItem( + this.templateDoc!, + 'currentTemplate' + ); + }} + >`; + } + + protected renderCloseButton(): TemplateResult { + return html``; + } + + protected renderCompare(): TemplateResult { + return html`${renderDiff( + this.selectedProjectIed!, + this.selectedTemplateIed! + ) ?? + html`${translate('compare-ied.noDiff', { + projectIedName: identity(this.selectedProjectIed!), + templateIedName: identity(this.selectedTemplateIed!), + })}`} + ${this.renderSelectIedButton()} ${this.renderCloseButton()}`; + } + + private renderIEDList(ieds: Element[], id: string): TemplateResult { + return html` + ${ieds.map(ied => { + const name = getNameAttribute(ied); + return html` + ${name} + `; + })} + `; + } + + protected renderIEDLists(): TemplateResult { + return html`
    +
    +
    ${translate('compare-ied.projectIedTitle')}
    +
    + ${this.renderIEDList(this.ieds, 'currentDocument')} +
    +
    +
    +
    ${translate('compare-ied.templateIedTitle')}
    +
    + ${this.renderIEDList(this.templateIeds, 'currentTemplate')} +
    +
    +
    + ${this.renderCompareButton()} ${this.renderCloseButton()}`; + } + + protected renderSelectTemplateFile(): TemplateResult { + return html`
    + + this.dispatchEvent(newPendingStateEvent(this.getTemplateFile(evt)))} + /> + + { + const input = ( + this.shadowRoot!.querySelector('#template-file') + ); + input?.click(); + }} + > +
    + ${this.renderCloseButton()}`; + } + + private renderDialog( + content: TemplateResult, + heading: string + ): TemplateResult { + return html` + ${content} + `; + } + + render(): TemplateResult { + if (!this.doc) return html``; + + if (this.selectedProjectIed && this.selectedTemplateIed) { + return this.renderDialog( + this.renderCompare(), + get('compare-ied.resultTitle') + ); + } else if (this.templateDoc) { + return this.renderDialog( + this.renderIEDLists(), + get('compare-ied.selectIedTitle') + ); + } else { + return this.renderDialog( + this.renderSelectTemplateFile(), + get('compare-ied.selectProjectTitle') + ); + } + } + + static styles = css` + mwc-dialog { + --mdc-dialog-max-width: 92vw; + } + + .splitContainer { + display: flex; + padding: 8px 6px 16px; + height: 64vh; + } + + .iedList { + flex: 50%; + margin: 0px 6px 0px; + min-width: 300px; + height: 100%; + overflow-y: auto; + } + + .resultTitle { + font-weight: bold; + } + `; +} diff --git a/src/translations/de.ts b/src/translations/de.ts index 2e1704bd02..7c24e1fac8 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -151,6 +151,11 @@ export const de: Translations = { noMultiplier: 'keiner', unique: 'Darf sich nicht wiederholen', }, + compare: { + compareButton: 'Starte Vergleich', + attributes: 'Attribute von {{ elementName }}', + children: 'Kindelemente von {{ elementName }}', + }, log: { name: 'Protokoll', placeholder: @@ -458,6 +463,17 @@ export const de: Translations = { scaleOffsetHelper: '???', }, }, + 'compare-ied': { + selectProjectTitle: 'Lade IEDs aus Vorlage', + selectIedTitle: 'IEDs zum Vergleich auswählen', + resultTitle: 'Vergleiche IED mit Vorlage', + projectIedTitle: 'IEDs im Projekt', + templateIedTitle: 'IEDs aus Vorlage', + selectIedButton: 'IED auswählen', + selectTemplateButton: 'Vorlage auswählen', + noDiff: + 'Keine Unterschiede zwischen IED Projekt "{{ projectIedName }}" und IED aus Vorlage "{{ templateIedName }}" gefunden', + }, 'enum-val': { wizard: { title: { diff --git a/src/translations/en.ts b/src/translations/en.ts index 0da9d77b13..35ef75f1d0 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -129,6 +129,11 @@ export const en = { noMultiplier: 'none', unique: 'Must be unique', }, + compare: { + compareButton: 'Compare', + attributes: 'Attributes from {{ elementName }}', + children: 'Child elements from {{ elementName }}', + }, log: { name: 'Log', placeholder: 'Edits, errors, and other notifications will show up here.', @@ -458,6 +463,17 @@ export const en = { scaleOffsetHelper: 'Scale Offset', }, }, + 'compare-ied': { + selectProjectTitle: 'Select template project to Compare IED with', + selectIedTitle: 'Select IED for comparison', + resultTitle: 'Compared IED with IED from template project', + projectIedTitle: 'IEDs in project', + templateIedTitle: 'IEDs in template project', + selectIedButton: 'Select IED', + selectTemplateButton: 'Select template project', + noDiff: + 'No differences between the project IED "{{ projectIedName }}" and template IED "{{ templateIedName }}"', + }, 'enum-val': { wizard: { title: { diff --git a/test/integration/__snapshots__/open-scd.test.snap.js b/test/integration/__snapshots__/open-scd.test.snap.js index 6c14c74b87..9b7b55c8d1 100644 --- a/test/integration/__snapshots__/open-scd.test.snap.js +++ b/test/integration/__snapshots__/open-scd.test.snap.js @@ -937,6 +937,21 @@ snapshots["open-scd looks like its snapshot"] = Update Substation + + + compare_arrows + + Compare IED +
  • + + CIM DLA + CID + +
    + + + + + + + + + + + +
    + + + 380.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 110.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 380.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 110.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/testfiles/foundation/compare-original.cid b/test/testfiles/foundation/compare-original.cid new file mode 100644 index 0000000000..516e208b2c --- /dev/null +++ b/test/testfiles/foundation/compare-original.cid @@ -0,0 +1,203 @@ + + + CIM DLA + CID + +
    + + + + + + + + + + +
    + + + 380.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + 110.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/test/testfiles/menu/compare-ied-changed.scd b/test/testfiles/menu/compare-ied-changed.scd new file mode 100644 index 0000000000..48ac51c281 --- /dev/null +++ b/test/testfiles/menu/compare-ied-changed.scd @@ -0,0 +1,360 @@ + +
    + TrainingIEC61850 + + + +
    + + + + + + + + + + + + + + + + + + + + + + Ok + + + + + Newest model + + + Other value + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ok + Warning + Alarm + + + not-supported + bay-control + station-control + remote-control + automatic-bay + automatic-station + automatic-remote + maintenance + process + + + status-only + direct-with-normal-security + sbo-with-normal-security + direct-with-enhanced-security + sbo-with-enhanced-security + + + on + blocked + test + test/blocked + off + + +
    diff --git a/test/testfiles/menu/compare-ied-original.scd b/test/testfiles/menu/compare-ied-original.scd new file mode 100644 index 0000000000..64db5ec999 --- /dev/null +++ b/test/testfiles/menu/compare-ied-original.scd @@ -0,0 +1,321 @@ + +
    + TrainingIEC61850 + + + +
    + + + + + + + + + + + + + + + + + + + + + + Newest model + + + Some value + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ok + Warning + Alarm + + + not-supported + bay-control + station-control + remote-control + automatic-bay + automatic-station + automatic-remote + maintenance + process + + + status-only + direct-with-normal-security + sbo-with-normal-security + direct-with-enhanced-security + sbo-with-enhanced-security + + + on + blocked + test + test/blocked + off + + +
    diff --git a/test/unit/foundation/__snapshots__/compare.test.snap.js b/test/unit/foundation/__snapshots__/compare.test.snap.js new file mode 100644 index 0000000000..cf3262475c --- /dev/null +++ b/test/unit/foundation/__snapshots__/compare.test.snap.js @@ -0,0 +1,210 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["compas-compare-dialog renderDiff child is added, so check latest snapshot"] = +`
    + + + + [compare.children] + + + Substation 1>S1 30kV + + +
  • +
  • + + + Bay + + + Substation 1>S1 30kV>BUSBAR12 + + + add + + + + +`; +/* end snapshot compas-compare-dialog renderDiff child is added, so check latest snapshot */ + +snapshots["compas-compare-dialog renderDiff child is removed and attribute added/removed/updated, so check latest snapshot"] = +`
    + + + + [compare.attributes] + + + Substation 1>S1 110kV + + +
  • +
  • + + + desc + + + Extra Voltage Level + + + add + + + + + [compare.children] + + + Substation 1>S1 110kV + + +
  • +
  • + + + Bay + + + Substation 1>S1 110kV>BAY_T3_1 + + + delete + + +
    + + + + [compare.attributes] + + + Substation 1>S1 110kV>BUSBAR6 + + +
  • +
  • + + + desc + + + Busbar 6 + + + delete + + +
    + + + + [compare.attributes] + + + Substation 1>S1 110kV>BAY_L1_0 + + +
  • +
  • + + + desc + + + First Bays + ↷ + First Bay + + + edit + + +
    +
    +`; +/* end snapshot compas-compare-dialog renderDiff child is removed and attribute added/removed/updated, so check latest snapshot */ + diff --git a/test/unit/foundation/compare.test.ts b/test/unit/foundation/compare.test.ts new file mode 100644 index 0000000000..6f9c7f99e1 --- /dev/null +++ b/test/unit/foundation/compare.test.ts @@ -0,0 +1,269 @@ +import { expect, fixtureSync } from '@open-wc/testing'; + +import { html } from 'lit-element'; + +import { + diffSclAttributes, + diffSclChilds, + identityForCompare, + isSame, + renderDiff, +} from '../../../src/foundation/compare.js'; + +describe('compas-compare-dialog', () => { + let oldSclElement: Element; + let newSclElement: Element; + + beforeEach(async () => { + oldSclElement = await fetch( + '/test/testfiles/foundation/compare-original.cid' + ) + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')) + .then(document => document.documentElement); + newSclElement = await fetch( + '/test/testfiles/foundation/compare-changed.cid' + ) + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')) + .then(document => document.documentElement); + }); + + describe('identityForCompare', () => { + it('will return the identity of the sub element, not the full identity', () => { + const voltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + + const result = identityForCompare(voltageLevel!); + expect(result).to.be.equal('S1 30kV'); + }); + + it('will return the identity of the main element, meaning the full identity', () => { + const substation = oldSclElement.querySelector( + 'Substation[name="Substation 1"]' + ); + + const result = identityForCompare(substation!); + expect(result).to.be.equal('Substation 1'); + }); + + it('will return the NaN of the root element', () => { + const substation = oldSclElement.querySelector('SCL'); + + const result = identityForCompare(substation!); + expect(result).to.be.NaN; + }); + }); + + describe('isSame', () => { + it('will return true when the same elements are passed', () => { + const voltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + + const same = isSame(voltageLevel!, voltageLevel!); + expect(same).to.be.true; + }); + + it('will return true when the same elements from different sources are passed', () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + + const same = isSame(newVoltageLevel!, oldVoltageLevel!); + expect(same).to.be.true; + }); + + it('will return false when the different type of elements are passed', () => { + const voltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + const substation = oldSclElement.querySelector( + 'Substation[name="Substation 1"]' + ); + + const same = isSame(voltageLevel!, substation!); + expect(same).to.be.false; + }); + + it('will return false when the different elements of the same type are passed', () => { + const voltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + const differentVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 380kV"]' + ); + + const same = isSame(differentVoltageLevel!, voltageLevel!); + expect(same).to.be.false; + }); + }); + + describe('diffSclAttributes', () => { + it('no attributes changed', () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + + const diffAttributes = diffSclAttributes( + newVoltageLevel!, + oldVoltageLevel! + ); + expect(diffAttributes).to.have.length(0); + }); + + it('one attribute has changed', () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 110kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 110kV"]' + ); + + const diffAttributes = diffSclAttributes( + newVoltageLevel!, + oldVoltageLevel! + ); + expect(diffAttributes).to.have.length(1); + expect(diffAttributes[0][0]).to.be.equal('desc'); + expect(diffAttributes[0][1].oldValue).to.be.null; + expect(diffAttributes[0][1].newValue).to.be.equal('Extra Voltage Level'); + }); + + it('only name changed on copied element', () => { + const newSubstation = newSclElement.querySelector( + 'Substation[name="Substation 1 (Copy)"]' + ); + const oldSubstation = oldSclElement.querySelector( + 'Substation[name="Substation 1"]' + ); + + const diffAttributes = diffSclAttributes(newSubstation!, oldSubstation!); + expect(diffAttributes).to.have.length(1); + expect(diffAttributes[0][0]).to.be.equal('name'); + expect(diffAttributes[0][1].oldValue).to.be.equal('Substation 1'); + expect(diffAttributes[0][1].newValue).to.be.equal('Substation 1 (Copy)'); + }); + }); + + describe('diffSclChilds', () => { + it('all children can be updated', () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 380kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 380kV"]' + ); + + const diffChilds = diffSclChilds(newVoltageLevel!, oldVoltageLevel!); + expect(diffChilds).to.have.length(5); + + const updatedChilds = diffChilds.filter( + diff => diff.newValue !== null && diff.oldValue !== null + ); + expect(updatedChilds).to.have.length(5); + }); + + it('all children can be updated of a copied element', () => { + const newSubstation = newSclElement.querySelector( + 'Substation[name="Substation 1 (Copy)"]' + ); + const oldSubstation = oldSclElement.querySelector( + 'Substation[name="Substation 1"]' + ); + + const diffChilds = diffSclChilds(newSubstation!, oldSubstation!); + expect(diffChilds).to.have.length(3); + + const updatedChilds = diffChilds.filter( + diff => diff.newValue !== null && diff.oldValue !== null + ); + expect(updatedChilds).to.have.length(3); + }); + + it('one child is added', () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + + const diffChilds = diffSclChilds(newVoltageLevel!, oldVoltageLevel!); + expect(diffChilds).to.have.length(5); + + const addedBay = diffChilds.filter(diff => diff.oldValue === null); + expect(addedBay).to.have.length(1); + expect(addedBay[0].newValue?.tagName).to.be.equal('Bay'); + }); + + it('one child is removed', () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 110kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 110kV"]' + ); + + const diffChilds = diffSclChilds(newVoltageLevel!, oldVoltageLevel!); + expect(diffChilds).to.have.length(7); + + const removedBay = diffChilds.filter(diff => diff.newValue === null); + expect(removedBay).to.have.length(1); + expect(removedBay[0].oldValue?.tagName).to.be.equal('Bay'); + }); + }); + + describe('renderDiff', () => { + it('no changes, so no template is returned', async () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 380kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 380kV"]' + ); + + const templateResult = renderDiff(newVoltageLevel!, oldVoltageLevel!); + expect(templateResult).to.be.null; + }); + + it('child is added, so check latest snapshot', async () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 30kV"]' + ); + + const templateResult = renderDiff(newVoltageLevel!, oldVoltageLevel!); + expect(templateResult).to.be.not.null; + + const element = fixtureSync(html`
    ${templateResult}
    `); + await element; + await expect(element).to.equalSnapshot(); + }); + + it('child is removed and attribute added/removed/updated, so check latest snapshot', async () => { + const newVoltageLevel = newSclElement.querySelector( + 'VoltageLevel[name="S1 110kV"]' + ); + const oldVoltageLevel = oldSclElement.querySelector( + 'VoltageLevel[name="S1 110kV"]' + ); + + const templateResult = renderDiff(newVoltageLevel!, oldVoltageLevel!); + expect(templateResult).to.be.not.null; + + const element = fixtureSync(html`
    ${templateResult}
    `); + await element; + await expect(element).to.equalSnapshot(); + }); + }); +}); diff --git a/test/unit/menu/CompareIED.test.ts b/test/unit/menu/CompareIED.test.ts new file mode 100644 index 0000000000..bd5b70b9ab --- /dev/null +++ b/test/unit/menu/CompareIED.test.ts @@ -0,0 +1,148 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import CompareIEDPlugin from '../../../src/menu/CompareIED.js'; + +describe('Compare IED Plugin', () => { + if (customElements.get('compare-ied') === undefined) + customElements.define('compare-ied', CompareIEDPlugin); + + let plugin: CompareIEDPlugin; + let doc: XMLDocument; + let template: XMLDocument; + + beforeEach(async () => { + plugin = await fixture(html``); + doc = await fetch('/test/testfiles/menu/compare-ied-changed.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + template = await fetch('/test/testfiles/menu/compare-ied-original.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + }); + + describe('show template project selection dialog', () => { + beforeEach(async () => { + plugin.doc = doc; + plugin.run(); + await plugin.requestUpdate(); + }); + + it('after closing the dialog everything set to undefined', async () => { + expect(plugin.templateDoc).to.be.undefined; + expect(plugin.selectedProjectIed).to.be.undefined; + expect(plugin.selectedTemplateIed).to.be.undefined; + + plugin['onClosed'](); + await plugin.requestUpdate(); + + expect(plugin.templateDoc).to.be.undefined; + expect(plugin.selectedProjectIed).to.be.undefined; + expect(plugin.selectedTemplateIed).to.be.undefined; + }); + + it('looks like its latest snapshot', async () => { + await expect(plugin.dialog).to.equalSnapshot(); + }); + }); + + describe('show ied selection lists dialog', () => { + beforeEach(async () => { + plugin.doc = doc; + plugin.templateDoc = template; + plugin.run(); + await plugin.requestUpdate(); + }); + + it('expect the correct number of IEDs from project', () => { + expect(plugin.ieds).to.have.length(4); + }); + + it('expect the correct number of IEDs from template project', () => { + expect(plugin.templateIeds).to.have.length(3); + }); + + it('after closing the dialog everything set to undefined', async () => { + expect(plugin.templateDoc).to.not.be.undefined; + expect(plugin.selectedProjectIed).to.be.undefined; + expect(plugin.selectedTemplateIed).to.be.undefined; + + plugin['onClosed'](); + await plugin.requestUpdate(); + + // expect(plugin.templateDoc).to.be.undefined; + expect(plugin.selectedProjectIed).to.be.undefined; + expect(plugin.selectedTemplateIed).to.be.undefined; + }); + + it('looks like its latest snapshot', async () => { + await expect(plugin.dialog).to.equalSnapshot(); + }); + }); + + describe('show compare dialog with no differences', () => { + beforeEach(async () => { + plugin.doc = doc; + plugin.templateDoc = template; + plugin.selectedProjectIed = + doc.querySelector('IED[name="FieldC_QA1_QB1_QB2_QC9"]') ?? undefined; + plugin.selectedTemplateIed = + template.querySelector('IED[name="FieldC_QA1_QB1_QB2_QC9"]') ?? + undefined; + plugin.run(); + await plugin.requestUpdate(); + }); + + it('looks like its latest snapshot', async () => { + await expect(plugin.dialog).to.equalSnapshot(); + }); + }); + + describe('show compare dialog with copied IED', () => { + beforeEach(async () => { + plugin.doc = doc; + plugin.templateDoc = template; + plugin.selectedProjectIed = + doc.querySelector('IED[name="FieldC_QA1_QB1_QB2_QCX"]') ?? undefined; + plugin.selectedTemplateIed = + template.querySelector('IED[name="FieldC_QA1_QB1_QB2_QC9"]') ?? + undefined; + plugin.run(); + await plugin.requestUpdate(); + }); + + it('looks like its latest snapshot', async () => { + await expect(plugin.dialog).to.equalSnapshot(); + }); + }); + + describe('show compare dialog with differences', () => { + beforeEach(async () => { + plugin.doc = doc; + plugin.templateDoc = template; + plugin.selectedProjectIed = + doc.querySelector('IED[name="FieldA_QA1_QB1_QB2_QC9"]') ?? undefined; + plugin.selectedTemplateIed = + template.querySelector('IED[name="FieldA_QA1_QB1_QB2_QC9"]') ?? + undefined; + plugin.run(); + await plugin.requestUpdate(); + }); + + it('after closing the dialog everything set to undefined', async () => { + expect(plugin.templateDoc).to.not.be.undefined; + expect(plugin.selectedProjectIed).to.not.be.undefined; + expect(plugin.selectedTemplateIed).to.not.be.undefined; + + plugin['onClosed'](); + await plugin.requestUpdate(); + + // expect(plugin.templateDoc).to.be.undefined; + expect(plugin.selectedProjectIed).to.be.undefined; + expect(plugin.selectedTemplateIed).to.be.undefined; + }); + + it('looks like its latest snapshot', async () => { + await expect(plugin.dialog).to.equalSnapshot(); + }); + }); +}); diff --git a/test/unit/menu/subscriberinfo.test.ts b/test/unit/menu/SubscriberInfo.test.ts similarity index 100% rename from test/unit/menu/subscriberinfo.test.ts rename to test/unit/menu/SubscriberInfo.test.ts diff --git a/test/unit/menu/__snapshots__/CompareIED.test.snap.js b/test/unit/menu/__snapshots__/CompareIED.test.snap.js new file mode 100644 index 0000000000..412e2fb903 --- /dev/null +++ b/test/unit/menu/__snapshots__/CompareIED.test.snap.js @@ -0,0 +1,332 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Compare IED Plugin show template project selection dialog looks like its latest snapshot"] = +` +
    + + + +
    + + +
    +`; +/* end snapshot Compare IED Plugin show template project selection dialog looks like its latest snapshot */ + +snapshots["Compare IED Plugin show ied selection lists dialog looks like its latest snapshot"] = +` +
    +
    +
    + [compare-ied.projectIedTitle] +
    +
    + + + + FieldA_QA1_QB1_QB2_QC9 + + + + + FieldB_QA1_QB1_QB2_QC9 + + + + + FieldC_QA1_QB1_QB2_QC9 + + + + + FieldC_QA1_QB1_QB2_QCX + + + +
    +
    +
    +
    + [compare-ied.templateIedTitle] +
    +
    + + + + FieldA_QA1_QB1_QB2_QC9 + + + + + FieldB_QA1_QB1_QB2_QC9 + + + + + FieldC_QA1_QB1_QB2_QC9 + + + +
    +
    +
    + + + + +
    +`; +/* end snapshot Compare IED Plugin show ied selection lists dialog looks like its latest snapshot */ + +snapshots["Compare IED Plugin show compare dialog with no differences looks like its latest snapshot"] = +` + [compare-ied.noDiff] + + + + + +`; +/* end snapshot Compare IED Plugin show compare dialog with no differences looks like its latest snapshot */ + +snapshots["Compare IED Plugin show compare dialog with copied IED looks like its latest snapshot"] = +` + + + + [compare.attributes] + + + FieldC_QA1_QB1_QB2_QCX + + +
  • +
  • + + + name + + + FieldC_QA1_QB1_QB2_QC9 + ↷ + FieldC_QA1_QB1_QB2_QCX + + + edit + + +
    + + + + +
    +`; +/* end snapshot Compare IED Plugin show compare dialog with copied IED looks like its latest snapshot */ + +snapshots["Compare IED Plugin show compare dialog with differences looks like its latest snapshot"] = +` + + + + [compare.children] + + + FieldA_QA1_QB1_QB2_QC9>>CBSW> LPHD 1 + + +
  • +
  • + + + DOI + + + FieldA_QA1_QB1_QB2_QC9>>CBSW> LPHD 1>PhyHealth + + + add + + +
    + + + + [compare.attributes] + + + FieldA_QA1_QB1_QB2_QC9>>CBSW> LPHD 1>PhyNam>vendor> 0 + + +
  • +
  • + + + value + + + Some value + ↷ + Other value + + + edit + + +
    + + + + +
    +`; +/* end snapshot Compare IED Plugin show compare dialog with differences looks like its latest snapshot */ +