diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 000000000..ef5f44a17 --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,44 @@ +# This workflow labels stale issues. +# +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues + +on: + workflow_dispatch: + schedule: + - cron: '0 19 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + days-before-stale: 60 + days-before-close: -1 + days-before-pr-stale: -1 + days-before-pr-close: -1 + + stale-issue-label: 'stale' + stale-issue-message: | + Hello there, + + Thank you for opening this issue! We appreciate your interest in our project. + However, it seems that this issue hasn't had any activity for a while. To ensure that our issue tracker remains organized and efficient, we occasionally review and address stale issues. + + If you believe this issue is still relevant and requires attention, please provide any additional context, updates, or details that might help us understand the problem better. + Feel free to continue the conversation here. + + If the issue is no longer relevant, you can simply close it. If you're uncertain, you can always reopen it later. + + Remember, our project thrives on community contributions, and your input matters. We're here to collaborate and improve. + Thank you for being part of this journey! + + + diff --git a/docs/decisions/0001-record-architecture-decisions.md b/docs/decisions/0001-record-architecture-decisions.md new file mode 100644 index 000000000..4cdab76ce --- /dev/null +++ b/docs/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,28 @@ +# 1. Record architecture decisions + +Date: 2023-07 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will follow the decisions recorded in the central organizational +repository ([.github](https://github.com/openscd/.github)), +and record new repo-specific decisions in this repository. + + +We write ADRs in the `docs/decisions` folder instead of a standard `doc/adr`: +- `docs` instead of `doc` because `doc` is used for the generated documentation. +- `decisions` instead of `adrs` because it is more explicit and a followed practice: + [↗ Markdown Any Decision Records - Applying MADR to your project ](https://adr.github.io/madr/#applying-madr-to-your-project) + +## Consequences + +- It will be harder to track which decisions have to be taken into consideration +- Local decisions will be easier to find diff --git a/package.json b/package.json index 9238aa93e..0b16a5abb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-scd", - "version": "0.33.0", + "version": "0.34.0", "repository": "https://github.com/openscd/open-scd.git", "description": "A bottom-up substation configuration designer for projects described using SCL `IEC 61850-6` Edition 2 or greater.", "keywords": [ diff --git a/src/editors/IED.ts b/src/editors/IED.ts index 2f239e9b9..196587751 100644 --- a/src/editors/IED.ts +++ b/src/editors/IED.ts @@ -96,7 +96,11 @@ export default class IedPlugin extends LitElement { super.updated(_changedProperties); // When the document is updated, we reset the selected IED. - if (_changedProperties.has('doc') || _changedProperties.has('nsdoc')) { + if ( + _changedProperties.has('doc') || + _changedProperties.has('editCount') || + _changedProperties.has('nsdoc') + ) { this.selectedIEDs = []; this.selectedLNClasses = []; diff --git a/src/editors/communication/subnetwork-editor.ts b/src/editors/communication/subnetwork-editor.ts index 57f35ae4c..398557ec8 100644 --- a/src/editors/communication/subnetwork-editor.ts +++ b/src/editors/communication/subnetwork-editor.ts @@ -142,9 +142,7 @@ export class SubNetworkEditor extends LitElement { private subNetworkSpecs(): string { if (!this.type && !this.bitrate) return ''; - return `(${this.type}${ - this.type && this.bitrate ? ` — ${this.bitrate}` : `` - })`; + return `(${[this.type, this.bitrate].filter(text => !!text).join(' — ')})`; } private header(): string { diff --git a/src/editors/protocol104/wizards/selectDo.ts b/src/editors/protocol104/wizards/selectDo.ts index c7b9394fe..37d861dca 100644 --- a/src/editors/protocol104/wizards/selectDo.ts +++ b/src/editors/protocol104/wizards/selectDo.ts @@ -13,7 +13,7 @@ import { getNameAttribute, identity, newSubWizardEvent, - selector, + find, Wizard, WizardActor, WizardInputElement, @@ -158,7 +158,7 @@ function checkAndGetLastElementFromPath( const [tagName, id] = path.pop()!.split(': '); if (!expectedTag.includes(tagName)) return null; - return doc.querySelector(selector(tagName, id)); + return find(doc, tagName, id); } /** diff --git a/src/editors/publisher/data-set-editor.ts b/src/editors/publisher/data-set-editor.ts index 3f7f48f30..27ffa2c70 100644 --- a/src/editors/publisher/data-set-editor.ts +++ b/src/editors/publisher/data-set-editor.ts @@ -19,7 +19,7 @@ import './data-set-element-editor.js'; import '../../filtered-list.js'; import { FilteredList } from '../../filtered-list.js'; -import { compareNames, identity, selector } from '../../foundation.js'; +import { compareNames, identity, find } from '../../foundation.js'; import { styles, updateElementReference } from './foundation.js'; @customElement('data-set-editor') @@ -50,7 +50,7 @@ export class DataSetEditor extends LitElement { private selectDataSet(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; - const dataSet = this.doc.querySelector(selector('DataSet', id)); + const dataSet = find(this.doc, 'DataSet', id); if (dataSet) { this.selectedDataSet = dataSet; diff --git a/src/editors/publisher/foundation.ts b/src/editors/publisher/foundation.ts index 71cdaaf2d..be524f0be 100644 --- a/src/editors/publisher/foundation.ts +++ b/src/editors/publisher/foundation.ts @@ -1,6 +1,6 @@ import { css } from 'lit-element'; -import { identity, selector } from '../../foundation.js'; +import { identity, find } from '../../foundation.js'; export function updateElementReference( newDoc: XMLDocument, @@ -9,7 +9,7 @@ export function updateElementReference( if (!oldElement || !oldElement.closest('SCL')) return null; const id = identity(oldElement); - const newElement = newDoc.querySelector(selector(oldElement.tagName, id)); + const newElement = find(newDoc, oldElement.tagName, id); return newElement; } diff --git a/src/editors/publisher/gse-control-editor.ts b/src/editors/publisher/gse-control-editor.ts index 19f71a450..39cdd388a 100644 --- a/src/editors/publisher/gse-control-editor.ts +++ b/src/editors/publisher/gse-control-editor.ts @@ -21,7 +21,7 @@ import '../../filtered-list.js'; import { FilteredList } from '../../filtered-list.js'; import { gooseIcon } from '../../icons/icons.js'; -import { compareNames, identity, selector } from '../../foundation.js'; +import { compareNames, identity, find } from '../../foundation.js'; import { styles, updateElementReference } from './foundation.js'; @customElement('gse-control-editor') @@ -62,7 +62,7 @@ export class GseControlEditor extends LitElement { private selectGSEControl(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; - const gseControl = this.doc.querySelector(selector('GSEControl', id)); + const gseControl = find(this.doc, 'GSEControl', id); if (!gseControl) return; this.selectedGseControl = gseControl; diff --git a/src/editors/publisher/report-control-editor.ts b/src/editors/publisher/report-control-editor.ts index 3aa580a75..2768c83e0 100644 --- a/src/editors/publisher/report-control-editor.ts +++ b/src/editors/publisher/report-control-editor.ts @@ -20,7 +20,7 @@ import './report-control-element-editor.js'; import '../../filtered-list.js'; import { FilteredList } from '../../filtered-list.js'; -import { compareNames, identity, selector } from '../../foundation.js'; +import { compareNames, identity, find } from '../../foundation.js'; import { reportIcon } from '../../icons/icons.js'; import { styles, updateElementReference } from './foundation.js'; @@ -66,7 +66,7 @@ export class ReportControlEditor extends LitElement { private selectReportControl(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; - const reportControl = this.doc.querySelector(selector('ReportControl', id)); + const reportControl = find(this.doc, 'ReportControl', id); if (!reportControl) return; this.selectedReportControl = reportControl; diff --git a/src/editors/publisher/sampled-value-control-editor.ts b/src/editors/publisher/sampled-value-control-editor.ts index ae68c94d9..c0f5f0f84 100644 --- a/src/editors/publisher/sampled-value-control-editor.ts +++ b/src/editors/publisher/sampled-value-control-editor.ts @@ -20,7 +20,7 @@ import '../../filtered-list.js'; import './sampled-value-control-element-editor.js'; import { FilteredList } from '../../filtered-list.js'; -import { compareNames, identity, selector } from '../../foundation.js'; +import { compareNames, identity, find } from '../../foundation.js'; import { smvIcon } from '../../icons/icons.js'; import { styles, updateElementReference } from './foundation.js'; @@ -29,7 +29,7 @@ export class SampledValueControlEditor extends LitElement { /** The document being edited as provided to plugins by [[`OpenSCD`]]. */ @property({ attribute: false }) doc!: XMLDocument; - @property({type: Number}) + @property({ type: Number }) editCount = -1; @state() @@ -66,9 +66,7 @@ export class SampledValueControlEditor extends LitElement { private selectSMVControl(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; - const smvControl = this.doc.querySelector( - selector('SampledValueControl', id) - ); + const smvControl = find(this.doc, 'SampledValueControl', id); if (!smvControl) return; this.selectedSampledValueControl = smvControl; diff --git a/src/editors/templates/datype-wizards.ts b/src/editors/templates/datype-wizards.ts index c19696f31..689728419 100644 --- a/src/editors/templates/datype-wizards.ts +++ b/src/editors/templates/datype-wizards.ts @@ -16,6 +16,7 @@ import { Create, createElement, EditorAction, + find, getValue, identity, newActionEvent, @@ -23,7 +24,6 @@ import { newWizardEvent, patterns, Replace, - selector, Wizard, WizardActor, WizardInputElement, @@ -89,7 +89,7 @@ export function editDaTypeWizard( dATypeIdentity: string, doc: XMLDocument ): Wizard | undefined { - const datype = doc.querySelector(selector('DAType', dATypeIdentity)); + const datype = find(doc, 'DAType', dATypeIdentity); if (!datype) return undefined; const id = datype.getAttribute('id'); @@ -138,7 +138,7 @@ export function editDaTypeWizard( style="margin-top: 0px;" @selected=${(e: SingleSelectedEvent) => { const bdaIdentity = ((e.target).selected).value; - const bda = doc.querySelector(selector('BDA', bdaIdentity)); + const bda = find(doc, 'BDA', bdaIdentity); if (bda) e.target!.dispatchEvent(newSubWizardEvent(editBDAWizard(bda))); diff --git a/src/editors/templates/dotype-wizards.ts b/src/editors/templates/dotype-wizards.ts index 5562f2c27..ab8af6977 100644 --- a/src/editors/templates/dotype-wizards.ts +++ b/src/editors/templates/dotype-wizards.ts @@ -16,6 +16,7 @@ import { Create, createElement, EditorAction, + find, getValue, identity, isPublic, @@ -23,7 +24,6 @@ import { newSubWizardEvent, newWizardEvent, Replace, - selector, Wizard, WizardActor, WizardInputElement, @@ -99,12 +99,7 @@ function sDOWizard(options: WizardOptions): Wizard | undefined { const doc = (options).doc ? (options).doc : (options).parent.ownerDocument; - const sdo = - Array.from( - doc.querySelectorAll( - selector('SDO', (options).identity ?? NaN) - ) - ).find(isPublic) ?? null; + const sdo = find(doc, 'SDO', (options).identity ?? NaN); const [title, action, type, menuActions, name, desc] = sdo ? [ @@ -351,7 +346,7 @@ export function dOTypeWizard( dOTypeIdentity: string, doc: XMLDocument ): Wizard | undefined { - const dotype = doc.querySelector(selector('DOType', dOTypeIdentity)); + const dotype = find(doc, 'DOType', dOTypeIdentity); if (!dotype) return undefined; return [ @@ -411,7 +406,7 @@ export function dOTypeWizard( const item = (e.target).selected; const daIdentity = ((e.target).selected).value; - const da = doc.querySelector(selector('DA', daIdentity)); + const da = find(doc, 'DA', daIdentity); const wizard = item.classList.contains('DA') ? da diff --git a/src/editors/templates/enumtype-wizard.ts b/src/editors/templates/enumtype-wizard.ts index 282debb33..c33d39b48 100644 --- a/src/editors/templates/enumtype-wizard.ts +++ b/src/editors/templates/enumtype-wizard.ts @@ -15,6 +15,7 @@ import { cloneElement, createElement, EditorAction, + find, getValue, identity, isPublic, @@ -23,7 +24,6 @@ import { newWizardEvent, patterns, Replace, - selector, Wizard, WizardActor, WizardInputElement, @@ -101,12 +101,11 @@ function eNumValWizard(options: WizardOptions): Wizard { const doc = (options).doc ? (options).doc : (options).parent.ownerDocument; - const enumval = - Array.from( - doc.querySelectorAll( - selector('EnumVal', (options).identity ?? NaN) - ) - ).find(isPublic) ?? null; + const enumval = find( + doc, + 'EnumVal', + (options).identity ?? NaN + ); const [title, action, ord, desc, value, menuActions] = enumval ? [ @@ -295,7 +294,7 @@ export function eNumTypeEditWizard( eNumTypeIdentity: string, doc: XMLDocument ): Wizard | undefined { - const enumtype = doc.querySelector(selector('EnumType', eNumTypeIdentity)); + const enumtype = find(doc, 'EnumType', eNumTypeIdentity); if (!enumtype) return undefined; return [ diff --git a/src/editors/templates/lnodetype-wizard.ts b/src/editors/templates/lnodetype-wizard.ts index 51fa41ee9..25990e054 100644 --- a/src/editors/templates/lnodetype-wizard.ts +++ b/src/editors/templates/lnodetype-wizard.ts @@ -18,6 +18,7 @@ import { Create, createElement, EditorAction, + find, getChildElementsByTagName, getValue, identity, @@ -27,7 +28,6 @@ import { newWizardEvent, patterns, Replace, - selector, Wizard, WizardActor, WizardInputElement, @@ -130,12 +130,7 @@ function dOWizard(options: WizardOptions): Wizard | undefined { const doc = (options).doc ? (options).doc : (options).parent.ownerDocument; - const DO = - Array.from( - doc.querySelectorAll( - selector('DO', (options).identity ?? NaN) - ) - ).find(isPublic) ?? null; + const DO = find(doc, 'DO', (options).identity ?? NaN); const [ title, @@ -412,7 +407,7 @@ function startLNodeTypeCreate( const value = (e.target).selected?.value; - const lnodetype = identity - ? templates.querySelector(selector('LNodeType', identity)) - : null; + const lnodetype = identity ? find(templates, 'LNodeType', identity) : null; const primaryAction = (e.target) @@ -606,7 +599,7 @@ export function lNodeTypeWizard( lNodeTypeIdentity: string, doc: XMLDocument ): Wizard | undefined { - const lnodetype = doc.querySelector(selector('LNodeType', lNodeTypeIdentity)); + const lnodetype = find(doc, 'LNodeType', lNodeTypeIdentity); if (!lnodetype) return undefined; return [ diff --git a/src/foundation.ts b/src/foundation.ts index 5665cf152..fa63bed47 100644 --- a/src/foundation.ts +++ b/src/foundation.ts @@ -2523,7 +2523,7 @@ export function getReference(parent: Element, tag: SCLTag): Element | null { return nextSibling ?? null; } -export function selector(tagName: string, identity: string | number): string { +function selector(tagName: string, identity: string | number): string { if (typeof identity !== 'string') return voidSelector; if (isSCLTag(tagName)) return tags[tagName].selector(tagName, identity); @@ -2531,6 +2531,24 @@ export function selector(tagName: string, identity: string | number): string { return tagName; } +export function find( + root: XMLDocument | Element | DocumentFragment, + tagName: string, + identity: string | number +): Element | null { + if (typeof identity !== 'string' || !isSCLTag(tagName)) return null; + + const element = root.querySelector(tags[tagName].selector(tagName, identity)); + + if (element === null || isPublic(element)) return element; + + return ( + Array.from( + root.querySelectorAll(tags[tagName].selector(tagName, identity)) + ).find(isPublic) ?? null + ); +} + /** @returns a string uniquely identifying `e` in its document, or NaN if `e` * is unidentifiable. */ export function identity(e: Element | null): string | number { diff --git a/src/foundation/ied.ts b/src/foundation/ied.ts index 980cc1288..ba735e411 100644 --- a/src/foundation/ied.ts +++ b/src/foundation/ied.ts @@ -1,4 +1,4 @@ -import { Delete, identity, selector } from '../foundation.js'; +import { Delete, find, identity } from '../foundation.js'; /** * All available FCDA references that are used to link ExtRefs. @@ -94,7 +94,7 @@ export function emptyInputsDeleteActions( 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)); + const inputs = find(doc, 'Inputs', key); if (inputs && inputs.parentElement) { inputDeleteActions.push({ diff --git a/src/menu/CompareIED.ts b/src/menu/CompareIED.ts index c932534d0..c4043a94d 100644 --- a/src/menu/CompareIED.ts +++ b/src/menu/CompareIED.ts @@ -22,11 +22,11 @@ import '../plain-compare-list.js'; import { compareNames, + find, getNameAttribute, identity, isPublic, newPendingStateEvent, - selector, } from '../foundation.js'; import { DiffFilter } from '../foundation/compare.js'; @@ -137,7 +137,7 @@ export default class CompareIEDPlugin extends LitElement { ); const identity = selectListItem?.value; if (identity) { - return doc.querySelector(selector('IED', identity)) ?? undefined; + return find(doc, 'IED', identity) ?? undefined; } return undefined; } diff --git a/src/menu/ImportIEDs.ts b/src/menu/ImportIEDs.ts index a1bb1edcc..e488245eb 100644 --- a/src/menu/ImportIEDs.ts +++ b/src/menu/ImportIEDs.ts @@ -24,7 +24,7 @@ import { newActionEvent, newLogEvent, newPendingStateEvent, - selector, + find, SimpleAction, } from '../foundation.js'; @@ -454,7 +454,7 @@ export default class ImportingIedPlugin extends LitElement { const ieds = selectedItems .map(item => { - return this.importDoc!.querySelector(selector('IED', item.value)); + return find(this.importDoc!, 'IED', item.value); }) .filter(ied => ied) as Element[]; diff --git a/src/menu/UpdateDescriptionABB.ts b/src/menu/UpdateDescriptionABB.ts index 57b70b65b..3a2db629b 100644 --- a/src/menu/UpdateDescriptionABB.ts +++ b/src/menu/UpdateDescriptionABB.ts @@ -8,11 +8,11 @@ import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../filtered-list.js'; import { cloneElement, + find, identity, isPublic, newWizardEvent, SCLTag, - selector, Wizard, WizardAction, WizardActor, @@ -37,7 +37,7 @@ function addDescriptionAction(doc: XMLDocument): WizardActor { const desc = (item.querySelector('span')).textContent; const [tag, identity] = item.value.split(' | '); - const oldElement = doc.querySelector(selector(tag, identity))!; + const oldElement = find(doc, tag, identity)!; const newElement = cloneElement(oldElement, { desc }); return { old: { element: oldElement }, new: { element: newElement } }; }); diff --git a/src/menu/UpdateDescriptionSEL.ts b/src/menu/UpdateDescriptionSEL.ts index 9848665af..1918a5337 100644 --- a/src/menu/UpdateDescriptionSEL.ts +++ b/src/menu/UpdateDescriptionSEL.ts @@ -8,11 +8,11 @@ import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../filtered-list.js'; import { cloneElement, + find, identity, isPublic, newWizardEvent, SCLTag, - selector, Wizard, WizardAction, WizardActor, @@ -71,7 +71,7 @@ function addDescriptionAction(doc: XMLDocument): WizardActor { const desc = (item.querySelector('span')).textContent; const [tag, identity] = item.value.split(' | '); - const oldElement = doc.querySelector(selector(tag, identity))!; + const oldElement = find(doc, tag, identity)!; const newElement = cloneElement(oldElement, { desc }); return { old: { element: oldElement }, new: { element: newElement } }; }); diff --git a/src/menu/UpdateSubstation.ts b/src/menu/UpdateSubstation.ts index 149933b2d..71d00e475 100644 --- a/src/menu/UpdateSubstation.ts +++ b/src/menu/UpdateSubstation.ts @@ -3,10 +3,10 @@ import { get } from 'lit-translate'; import { crossProduct, + find, identity, newWizardEvent, SCLTag, - selector, tags, } from '../foundation.js'; import { Diff, mergeWizard } from '../wizards.js'; @@ -79,56 +79,46 @@ export function isValidReference( ); } -export function mergeSubstation(element: Element, currentDoc: Document, docWithSubstation: Document): void { - element.dispatchEvent( - newWizardEvent( - mergeWizard( - // FIXME: doesn't work with multiple Substations! - currentDoc.documentElement, - docWithSubstation.documentElement, - { - title: get('updatesubstation.title'), - selected: (diff: Diff): boolean => - diff.theirs instanceof Element - ? diff.theirs.tagName === 'LNode' - ? currentDoc.querySelector( - selector('LNode', identity(diff.theirs)) - ) === null && - isValidReference(docWithSubstation, identity(diff.theirs)) - : diff.theirs.tagName === 'Substation' || - !tags['SCL'].children.includes( - diff.theirs.tagName - ) - : diff.theirs !== null, - disabled: (diff: Diff): boolean => - diff.theirs instanceof Element && - diff.theirs.tagName === 'LNode' && - (currentDoc.querySelector( - selector('LNode', identity(diff.theirs)) - ) !== null || - !isValidReference(docWithSubstation, identity(diff.theirs))), - auto: (): boolean => true, - } - ) - ) - ); -} - export default class UpdateSubstationPlugin extends LitElement { doc!: XMLDocument; - @query('#update-substation-plugin-input') - pluginFileUI!: HTMLInputElement; - - async updateSubstation(event: Event): Promise { - const file = (event.target)?.files?.item(0) ?? false; - if (!file) { - return; - } + @query('#update-substation-plugin-input') pluginFileUI!: HTMLInputElement; - const text = await file.text() - const doc = new DOMParser().parseFromString(text, 'application/xml'); - mergeSubstation(this, this.doc, doc); + updateSubstation(event: Event): void { + const file = + (event.target)?.files?.item(0) ?? false; + if (file) + file.text().then(text => { + const doc = new DOMParser().parseFromString(text, 'application/xml'); + this.dispatchEvent( + newWizardEvent( + mergeWizard( + // FIXME: doesn't work with multiple Substations! + this.doc.documentElement, + doc.documentElement, + { + title: get('updatesubstation.title'), + selected: (diff: Diff): boolean => + diff.theirs instanceof Element + ? diff.theirs.tagName === 'LNode' + ? find(this.doc, 'LNode', identity(diff.theirs)) === + null && isValidReference(doc, identity(diff.theirs)) + : diff.theirs.tagName === 'Substation' || + !tags['SCL'].children.includes( + diff.theirs.tagName + ) + : diff.theirs !== null, + disabled: (diff: Diff): boolean => + diff.theirs instanceof Element && + diff.theirs.tagName === 'LNode' && + (find(this.doc, 'LNode', identity(diff.theirs)) !== null || + !isValidReference(doc, identity(diff.theirs))), + auto: (): boolean => true, + } + ) + ) + ); + }); this.pluginFileUI.onchange = null; } @@ -137,9 +127,11 @@ export default class UpdateSubstationPlugin extends LitElement { } render(): TemplateResult { - return html` ((event.target).value = '')} - @change=${this.updateSubstation} - id="update-substation-plugin-input" accept=".sed,.scd,.ssd,.iid,.cid" type="file">`; + return html` + ((event.target).value = '')} @change=${(e: Event) => + this.updateSubstation( + e + )} id="update-substation-plugin-input" accept=".sed,.scd,.ssd,.iid,.cid" type="file">`; } static styles = css` diff --git a/src/menu/VirtualTemplateIED.ts b/src/menu/VirtualTemplateIED.ts index a45a11491..dd2501924 100644 --- a/src/menu/VirtualTemplateIED.ts +++ b/src/menu/VirtualTemplateIED.ts @@ -21,10 +21,10 @@ import { Select } from '@material/mwc-select'; import '../filtered-list.js'; import { + find, getChildElementsByTagName, identity, newActionEvent, - selector, } from '../foundation.js'; import { WizardTextField } from '../wizard-textfield.js'; import { @@ -164,10 +164,7 @@ export default class VirtualTemplateIED extends LitElement { this.dialog.querySelectorAll( 'mwc-check-list-item[selected]:not([disabled])' ) ?? [] - ).map( - selectedItem => - this.doc.querySelector(selector('LNode', selectedItem.value))! - ); + ).map(selectedItem => find(this.doc, 'LNode', selectedItem.value)!); if (!selectedLNode.length) return; const selectedLLN0s = Array.from( diff --git a/src/open-scd.ts b/src/open-scd.ts index c65c3e7aa..92350462d 100644 --- a/src/open-scd.ts +++ b/src/open-scd.ts @@ -122,17 +122,13 @@ export class OpenSCD extends Compasing( mwc-linear-progress { position: fixed; - --mdc-theme-primary: var(--mdc-theme-secondary); + --mdc-linear-progress-buffer-color: var(--primary); + --mdc-theme-primary: var(--secondary); left: 0px; - top: 112px; + top: 0px; width: 100%; pointer-events: none; - } - - @media (max-width: 599px) { - mwc-linear-progress { - top: 104px; - } + z-index: 1000; } tt { diff --git a/src/wizards/clientln.ts b/src/wizards/clientln.ts index 8b7c8c482..4da19dbcd 100644 --- a/src/wizards/clientln.ts +++ b/src/wizards/clientln.ts @@ -9,9 +9,9 @@ import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../filtered-list.js'; import { createElement, + find, identity, pathParts, - selector, Wizard, WizardAction, WizardActor, @@ -34,16 +34,16 @@ function getElement(identity: string | number): string { function getLogicalNode(doc: XMLDocument, identity: string): Element | null { if (identity.split('>').length === 4) { - return doc.querySelector(selector('LN', identity)); + return find(doc, 'LN', identity); } if (identity.split('>').length === 3) { if (getElement(identity).split(' ').length > 1) { - return doc.querySelector(selector('LN', identity)); + return find(doc, 'LN', identity); } if (getElement(identity).split(' ').length === 1) { - return doc.querySelector(selector('LN0', identity)); + return find(doc, 'LN0', identity); } } @@ -128,7 +128,7 @@ function addClientLnAction(doc: XMLDocument): WizardActor { const reportCbs = cbSelected .map(cb => cb.value) - .map(cbValue => doc.querySelector(selector('ReportControl', cbValue))) + .map(cbValue => find(doc, 'ReportControl', cbValue)) .filter(cb => cb !== null); reportCbs.forEach(cb => { diff --git a/src/wizards/commmap-wizards.ts b/src/wizards/commmap-wizards.ts index ca9a96589..36437ee88 100644 --- a/src/wizards/commmap-wizards.ts +++ b/src/wizards/commmap-wizards.ts @@ -7,11 +7,11 @@ import { SingleSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; import '../filtered-list.js'; import { + find, findControlBlocks, identity, isPublic, newWizardEvent, - selector, Wizard, WizardActor, } from '../foundation.js'; @@ -90,9 +90,7 @@ export function communicationMappingWizard( >${Array.from(connections.keys()).map(key => { const elements = connections.get(key)!; const [cbId, cbTag, sinkIED] = key.split(' | '); - const cbElement = ownerDocument.querySelector( - selector(cbTag, cbId) - ); + const cbElement = find(ownerDocument, cbTag, cbId); const [_, sourceIED, controlBlock] = cbId.match(/^(.+)>>(.*)$/)!; return html` mwc-check-list-item:not([selected])' ) ) - .map(listItem => - element.querySelector(selector('FCDA', (listItem).value)) - ) + .map(listItem => find(element, 'FCDA', (listItem).value)) .filter(fcda => fcda) .map(fcda => { return { diff --git a/src/wizards/fcda.ts b/src/wizards/fcda.ts index ee9343789..939a7feb0 100644 --- a/src/wizards/fcda.ts +++ b/src/wizards/fcda.ts @@ -3,7 +3,7 @@ import { get } from 'lit-translate'; import { createElement, - selector, + find, Wizard, WizardAction, WizardActor, @@ -17,7 +17,7 @@ import { export function newFCDA(parent: Element, path: string[]): Element | undefined { const [leafTag, leafId] = path[path.length - 1].split(': '); - const leaf = parent.ownerDocument.querySelector(selector(leafTag, leafId)); + const leaf = find(parent.ownerDocument, leafTag, leafId); if (!leaf || getDataModelChildren(leaf).length > 0) return; const lnSegment = path.find(segment => segment.startsWith('LN')); @@ -25,7 +25,7 @@ export function newFCDA(parent: Element, path: string[]): Element | undefined { const [lnTag, lnId] = lnSegment.split(': '); - const ln = parent.ownerDocument.querySelector(selector(lnTag, lnId)); + const ln = find(parent.ownerDocument, lnTag, lnId); if (!ln) return; const ldInst = ln.closest('LDevice')?.getAttribute('inst'); @@ -44,7 +44,7 @@ export function newFCDA(parent: Element, path: string[]): Element | undefined { const [tagName, id] = segment.split(': '); if (!['DO', 'DA', 'SDO', 'BDA'].includes(tagName)) continue; - const element = parent.ownerDocument.querySelector(selector(tagName, id)); + const element = find(parent.ownerDocument, tagName, id); if (!element) return; diff --git a/src/wizards/foundation/finder.ts b/src/wizards/foundation/finder.ts index 67832295a..62553e29c 100644 --- a/src/wizards/foundation/finder.ts +++ b/src/wizards/foundation/finder.ts @@ -3,7 +3,7 @@ import { translate } from 'lit-translate'; import '../../finder-list.js'; import { Directory } from '../../finder-list.js'; -import { identity, isPublic, selector } from '../../foundation.js'; +import { find, identity, isPublic } from '../../foundation.js'; export function getDisplayString(entry: string): string { if (entry.startsWith('IED:')) return entry.replace(/^.*:/, '').trim(); @@ -17,7 +17,7 @@ export function getReader( ): (path: string[]) => Promise { return async (path: string[]) => { const [tagName, id] = path[path.length - 1]?.split(': ', 2); - const element = doc.querySelector(selector(tagName, id)); + const element = find(doc, tagName, id); if (!element) return { path, header: html`

${translate('error')}

`, entries: [] }; diff --git a/src/wizards/foundation/limits.ts b/src/wizards/foundation/limits.ts index 8096b637a..b7e66518c 100644 --- a/src/wizards/foundation/limits.ts +++ b/src/wizards/foundation/limits.ts @@ -3,7 +3,7 @@ const nameStartChar = '|[\u037F-\u1FFF]|[\u200C-\u200D]|[\u2070-\u218F]|[\u2C00-\u2FEF]' + '|[\u3001-\uD7FF]|[\uF900-\uFDCF]|[\uFDF0-\uFFFD]'; const nameChar = - nameStartChar + '|[.0-9-]|\u00B7|[\u0300-\u036F]|[\u203F-\u2040]'; + nameStartChar + '|[.0-9\\-]|\u00B7|[\u0300-\u036F]|[\u203F-\u2040]'; const name = nameStartChar + '(' + nameChar + ')*'; const nmToken = '(' + nameChar + ')+'; @@ -17,7 +17,7 @@ export const patterns = { nmToken, names: name + '( ' + name + ')*', nmTokens: nmToken + '( ' + nmToken + ')*', - decimal: '[+-]?[0-9]+(([.][0-9]*)?|([.][0-9]+))', + decimal: '[+\\-]?[0-9]+(([.][0-9]*)?|([.][0-9]+))', unsigned: '[+]?[0-9]+(([.][0-9]*)?|([.][0-9]+))', alphanumericFirstUpperCase: '[A-Z][0-9,A-Z,a-z]*', asciName: '[A-Za-z][0-9,A-Z,a-z_]*', diff --git a/src/wizards/gsecontrol.ts b/src/wizards/gsecontrol.ts index ab7a91115..7f02cb626 100644 --- a/src/wizards/gsecontrol.ts +++ b/src/wizards/gsecontrol.ts @@ -18,6 +18,7 @@ import { createElement, Delete, EditorAction, + find, getUniqueElementName, getValue, identity, @@ -26,7 +27,6 @@ import { newActionEvent, newSubWizardEvent, newWizardEvent, - selector, SimpleAction, Wizard, WizardActor, @@ -335,7 +335,7 @@ function openGseControlCreateWizard(doc: XMLDocument): WizardActor { const [tagName, id] = path.pop()!.split(': '); if (tagName !== 'IED') return []; - const ied = doc.querySelector(selector(tagName, id)); + const ied = find(doc, tagName, id); if (!ied) return []; const ln0 = ied.querySelector('LN0'); @@ -562,9 +562,7 @@ export function selectGseControlWizard(element: Element): Wizard { @selected=${(e: SingleSelectedEvent) => { const gseControlIndentity = ((e.target).selected) .value; - const gseControl = element.querySelector( - selector('GSEControl', gseControlIndentity) - ); + const gseControl = find(element, 'GSEControl', gseControlIndentity); if (gseControl) { e.target!.dispatchEvent( newSubWizardEvent(() => editGseControlWizard(gseControl)) diff --git a/src/wizards/lnode.ts b/src/wizards/lnode.ts index 109519799..d4978e378 100644 --- a/src/wizards/lnode.ts +++ b/src/wizards/lnode.ts @@ -15,6 +15,7 @@ import { cloneElement, createElement, EditorAction, + find, getChildElementsByTagName, getValue, identity, @@ -22,7 +23,6 @@ import { newLogEvent, newWizardEvent, referencePath, - selector, Wizard, WizardActor, WizardInputElement, @@ -40,11 +40,7 @@ function createLNodeAction(parent: Element): WizardActor { const selectedLNodeTypes = list!.items .filter(item => item.selected) .map(item => item.value) - .map(identity => { - return parent.ownerDocument.querySelector( - selector('LNodeType', identity) - ); - }) + .map(identity => find(parent.ownerDocument, 'LNodeType', identity)) .filter(item => item !== null); const lnInstGenerator = newLnInstGenerator(parent); @@ -279,9 +275,9 @@ export function lNodeWizardAction(parent: Element): WizardActor { .filter(item => item.selected) .map(item => item.value) .map(identity => { - return parent.ownerDocument.querySelector(selector('LN0', identity)) - ? parent.ownerDocument.querySelector(selector('LN0', identity)) - : parent.ownerDocument.querySelector(selector('LN', identity)); + const ln0 = find(parent.ownerDocument, 'LN0', identity); + if (ln0) return ln0; + return find(parent.ownerDocument, 'LN', identity); }) .filter(item => item !== null); diff --git a/src/wizards/reportcontrol.ts b/src/wizards/reportcontrol.ts index 0e8d7d2a3..4577259d9 100644 --- a/src/wizards/reportcontrol.ts +++ b/src/wizards/reportcontrol.ts @@ -16,12 +16,12 @@ import { cloneElement, createElement, EditorAction, + find, getReference, getValue, identity, isPublic, newSubWizardEvent, - selector, SimpleAction, Wizard, WizardActor, @@ -307,7 +307,7 @@ function openReportControlCreateWizard(doc: XMLDocument): WizardActor { const [tagName, id] = path.pop()!.split(': '); if (tagName !== 'IED') return []; - const ied = doc.querySelector(selector(tagName, id)); + const ied = find(doc, tagName, id); if (!ied) return []; const ln0 = ied.querySelector('LN0'); @@ -421,7 +421,7 @@ function copyReportControlActions(element: Element): WizardActor { const complexActions: ComplexAction[] = []; iedItems.forEach(iedItem => { - const ied = doc.querySelector(selector('IED', iedItem.value)); + const ied = find(doc, 'IED', iedItem.value); if (!ied) return; const sinkLn0 = ied.querySelector('LN0'); @@ -738,9 +738,7 @@ export function selectReportControlWizard(element: Element): Wizard { html` { const identity = ((e.target).selected).value; - const reportControl = element.querySelector( - selector('ReportControl', identity) - ); + const reportControl = find(element, 'ReportControl', identity); if (!reportControl) return; e.target?.dispatchEvent( diff --git a/src/wizards/sampledvaluecontrol.ts b/src/wizards/sampledvaluecontrol.ts index 396dd3491..c3dc41aea 100644 --- a/src/wizards/sampledvaluecontrol.ts +++ b/src/wizards/sampledvaluecontrol.ts @@ -17,6 +17,7 @@ import { createElement, Delete, EditorAction, + find, getUniqueElementName, getValue, identity, @@ -25,7 +26,6 @@ import { newActionEvent, newSubWizardEvent, newWizardEvent, - selector, Wizard, WizardActor, WizardInputElement, @@ -455,7 +455,7 @@ function openSampledValueControlCreateWizard(doc: XMLDocument): WizardActor { const [tagName, id] = path.pop()!.split(': '); if (tagName !== 'IED') return []; - const ied = doc.querySelector(selector(tagName, id)); + const ied = find(doc, tagName, id); if (!ied) return []; const ln0 = ied.querySelector('LN0'); @@ -652,8 +652,10 @@ export function selectSampledValueControlWizard(element: Element): Wizard { html` { const identity = ((e.target).selected).value; - const sampledValueControl = element.querySelector( - selector('SampledValueControl', identity) + const sampledValueControl = find( + element, + 'SampledValueControl', + identity ); if (!sampledValueControl) return; diff --git a/src/wizards/subnetwork.ts b/src/wizards/subnetwork.ts index 411b7361d..a2bba6d7f 100644 --- a/src/wizards/subnetwork.ts +++ b/src/wizards/subnetwork.ts @@ -127,19 +127,22 @@ function getBitRateAction( multiplier: string | null, SubNetwork: Element ): EditorAction { - if (oldBitRate === null) + if (oldBitRate === null) { + const bitRateElement = createElement(SubNetwork.ownerDocument, 'BitRate', { + unit: 'b/s', + }); + + if (multiplier) bitRateElement.setAttribute('multiplier', multiplier); + if (BitRate) bitRateElement.textContent = BitRate; + return { new: { parent: SubNetwork, - element: new DOMParser().parseFromString( - `${BitRate === null ? '' : BitRate}`, - 'application/xml' - ).documentElement, + element: bitRateElement, reference: SubNetwork.firstElementChild, }, }; + } if (BitRate === null) return { diff --git a/test/integration/editors/communication/subnetwork-editor-wizarding-editing.test.ts b/test/integration/editors/communication/subnetwork-editor-wizarding-editing.test.ts index afcdc8a1e..cbe111534 100644 --- a/test/integration/editors/communication/subnetwork-editor-wizarding-editing.test.ts +++ b/test/integration/editors/communication/subnetwork-editor-wizarding-editing.test.ts @@ -59,6 +59,7 @@ describe('subnetwork-editor wizarding editing integration', () => { ) ); }); + it('closes on secondary action', async () => { await (( parent.wizardUI.dialog?.querySelector( @@ -153,6 +154,7 @@ describe('subnetwork-editor wizarding editing integration', () => { .null; }); }); + describe('remove action', () => { let doc: XMLDocument; let parent: MockWizardEditor; @@ -211,13 +213,16 @@ describe('subnetwork-editor wizarding editing integration', () => { ) ); element = parent.querySelector('subnetwork-editor'); + }); + it('adds ConnectedAP on primary action', async () => { (( element?.shadowRoot?.querySelector( 'mwc-icon-button[icon="playlist_add"]' ) )).click(); await parent.updateComplete; + await element?.updateComplete; newConnectedAPItem = ( parent.wizardUI.dialog!.querySelector( @@ -230,23 +235,192 @@ describe('subnetwork-editor wizarding editing integration', () => { 'mwc-button[slot="primaryAction"]' ) ); - }); - it('add ConnectedAP on primary action', async () => { expect( doc.querySelector( ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED3"][apName="P2"]' ) ).to.not.exist; + newConnectedAPItem.click(); - await parent.updateComplete; primaryAction.click(); await parent.updateComplete; + expect( doc.querySelector( ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED3"][apName="P2"]' ) ).to.exist; }); + + it('add ConnectedAP with GSE and generates correct addresses', async () => { + doc = await fetch('/test/testfiles/editors/MessageBindingGOOSE2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html`` + ) + ); + element = parent.querySelector('subnetwork-editor'); + await parent.updateComplete; + await element?.updateComplete; + + (( + element?.shadowRoot?.querySelector( + 'mwc-icon-button[icon="playlist_add"]' + ) + )).click(); + await parent.updateComplete; + await element?.updateComplete; + + expect( + doc.querySelector( + ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED4"][apName="P1"]' + ) + ).to.not.exist; + + newConnectedAPItem = ( + parent.wizardUI.dialog!.querySelector( + 'mwc-check-list-item:nth-child(2)' + ) + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + newConnectedAPItem.click(); + primaryAction.click(); + await parent.updateComplete; + + const connectedAp = doc.querySelector( + ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED4"][apName="P1"]' + ); + + expect(connectedAp).to.exist; + const gse1 = connectedAp!.querySelector('GSE[cbName="GCB2"]'); + const gse2 = connectedAp!.querySelector('GSE[cbName="GCB2"]'); + expect(gse1).to.exist; + expect(gse2).to.exist; + + expect(gse1?.getAttribute('ldInst')).to.equal('CircuitBreaker_CB1'); + + const address1 = gse1?.querySelector('Address'); + expect(address1).to.exist; + + const vlanPriority = address1?.querySelector('P[type="VLAN-PRIORITY"]'); + expect(vlanPriority).to.exist; + expect(vlanPriority?.textContent).to.equal('4'); + + const vlanId = address1?.querySelector('P[type="VLAN-ID"]'); + expect(vlanId).to.exist; + expect(vlanId?.textContent).to.equal('000'); + + const appId = address1?.querySelector('P[type="APPID"]'); + expect(appId).to.exist; + expect(appId?.textContent).to.equal('0001'); + + const mac = address1?.querySelector('P[type="MAC-Address"]'); + expect(mac).to.exist; + expect(mac?.textContent).to.equal('01-0C-CD-01-00-01'); + + const minTime = gse1?.querySelector('MinTime'); + expect(minTime).to.exist; + expect(minTime?.getAttribute('unit')).to.equal('s'); + expect(minTime?.getAttribute('multiplier')).to.equal('m'); + expect(minTime?.textContent).to.equal('10'); + + const maxTime = gse1?.querySelector('MaxTime'); + expect(maxTime).to.exist; + expect(maxTime?.getAttribute('unit')).to.equal('s'); + expect(maxTime?.getAttribute('multiplier')).to.equal('m'); + expect(maxTime?.textContent).to.equal('10000'); + }); + + it('add ConnectedAP with SMV and generates correct addresses', async () => { + doc = await fetch('/test/testfiles/editors/MessageBindingSMV2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html`` + ) + ); + element = parent.querySelector('subnetwork-editor'); + await parent.updateComplete; + await element?.updateComplete; + + (( + element?.shadowRoot?.querySelector( + 'mwc-icon-button[icon="playlist_add"]' + ) + )).click(); + await parent.updateComplete; + await element?.updateComplete; + + expect( + doc.querySelector( + ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED4"][apName="P1"]' + ) + ).to.not.exist; + + newConnectedAPItem = ( + parent.wizardUI.dialog!.querySelector( + 'mwc-check-list-item:nth-child(2)' + ) + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + newConnectedAPItem.click(); + primaryAction.click(); + await parent.updateComplete; + + const connectedAp = doc.querySelector( + ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED4"][apName="P1"]' + ); + + expect(connectedAp).to.exist; + const smv1 = connectedAp!.querySelector('SMV[cbName="MSVCB02"]'); + expect(smv1).to.exist; + + expect(smv1?.getAttribute('ldInst')).to.equal('CircuitBreaker_CB1'); + + const address1 = smv1?.querySelector('Address'); + expect(address1).to.exist; + + const vlanPriority = address1?.querySelector('P[type="VLAN-PRIORITY"]'); + expect(vlanPriority).to.exist; + expect(vlanPriority?.textContent).to.equal('4'); + + const vlanId = address1?.querySelector('P[type="VLAN-ID"]'); + expect(vlanId).to.exist; + expect(vlanId?.textContent).to.equal('000'); + + const appId = address1?.querySelector('P[type="APPID"]'); + expect(appId).to.exist; + expect(appId?.textContent).to.equal('4000'); + + const mac = address1?.querySelector('P[type="MAC-Address"]'); + expect(mac).to.exist; + expect(mac?.textContent).to.equal('01-0C-CD-04-00-00'); + }); }); }); diff --git a/test/integration/editors/templates/__snapshots__/dotype-wizarding.test.snap.js b/test/integration/editors/templates/__snapshots__/dotype-wizarding.test.snap.js index c87158ebc..d05a9d404 100644 --- a/test/integration/editors/templates/__snapshots__/dotype-wizarding.test.snap.js +++ b/test/integration/editors/templates/__snapshots__/dotype-wizarding.test.snap.js @@ -679,7 +679,6 @@ snapshots["DOType wizards defines a createDOTypeWizard looks like the latest sna label="id" maxlength="127" minlength="1" - pattern="([:_A-Za-z]|[À-Ö]|[Ø-ö]|[ø-˿]|[Ͱ-ͽ]|[Ϳ-῿]|[‌-‍]|[⁰-↏]|[Ⰰ-⿯]|[、-퟿]|[豈-﷏]|[ﷰ-�]|[.0-9-]|·|[̀-ͯ]|[‿-⁀])+" required="" > @@ -688,13 +687,11 @@ snapshots["DOType wizards defines a createDOTypeWizard looks like the latest sna helper="[scl.desc]" label="desc" nullable="" - pattern="([ -~]|[…]|[ -퟿]|[-�])*" > @@ -783,7 +780,6 @@ snapshots["DOType wizards defines a dOTypeWizard looks like the latest snapshot" label="id" maxlength="127" minlength="1" - pattern="([:_A-Za-z]|[À-Ö]|[Ø-ö]|[ø-˿]|[Ͱ-ͽ]|[Ϳ-῿]|[‌-‍]|[⁰-↏]|[Ⰰ-⿯]|[、-퟿]|[豈-﷏]|[ﷰ-�]|[.0-9-]|·|[̀-ͯ]|[‿-⁀])+" required="" > @@ -792,13 +788,11 @@ snapshots["DOType wizards defines a dOTypeWizard looks like the latest snapshot" helper="[scl.desc]" label="desc" nullable="" - pattern="([ -~]|[…]|[ -퟿]|[-�])*" > @@ -1068,7 +1062,6 @@ snapshots["DOType wizards defines a sDOWizard to edit an existing SDO looks like dialoginitialfocus="" helper="[scl.name]" label="name" - pattern="[a-z][0-9A-Za-z]*" required="" > > @@ -1078,7 +1071,6 @@ snapshots["DOType wizards defines a sDOWizard to edit an existing SDO looks like helper="[scl.desc]" label="desc" nullable="" - pattern="([ -~]|[…]|[ -퟿]|[-�])*" > > @@ -1266,7 +1257,6 @@ snapshots["DOType wizards defines a sDOWizard to create a new SDO element looks helper="[scl.desc]" label="desc" nullable="" - pattern="([ -~]|[…]|[ -퟿]|[-�])*" > { if (customElements.get('templates-editor') === undefined) customElements.define('templates-editor', TemplatesPlugin); @@ -69,7 +71,41 @@ describe('DOType wizards', () => { }); it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); + // prettier does not support escaping in regexes of the /v flag + await expect(parent.wizardUI.dialog).dom.to.equalSnapshot({ + ignoreAttributes: [ + { + tags: ['wizard-textfield'], + attributes: ['pattern'], + }, + ], + }); + }); + + // work around, because the escapes get removed in snapshot by prettier + it('should have correct pattern', async () => { + expect( + parent.wizardUI.dialog!.querySelectorAll('wizard-textfield[pattern]')! + .length + ).to.equal(3); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[0] + .getAttribute('pattern') + ).to.equal(patterns.nmToken); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[1] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[2] + .getAttribute('pattern') + ).to.equal(patterns.cdc); }); it('allows to add empty DOTypes to the project', async () => { @@ -171,7 +207,41 @@ describe('DOType wizards', () => { }); it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); + // prettier does not support escaping in regexes of the /v flag + await expect(parent.wizardUI.dialog).dom.to.equalSnapshot({ + ignoreAttributes: [ + { + tags: ['wizard-textfield'], + attributes: ['pattern'], + }, + ], + }); + }); + + // work around, because the escapes get removed in snapshot by prettier + it('should have correct pattern', async () => { + expect( + parent.wizardUI.dialog!.querySelectorAll('wizard-textfield[pattern]')! + .length + ).to.equal(3); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[0] + .getAttribute('pattern') + ).to.equal(patterns.nmToken); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[1] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[2] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); }); it('edits DOType attributes id', async () => { @@ -242,8 +312,37 @@ describe('DOType wizards', () => { }); it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); + // prettier does not support escaping in regexes of the /v flag + await expect(parent.wizardUI.dialog).dom.to.equalSnapshot({ + ignoreAttributes: [ + { + tags: ['wizard-textfield'], + attributes: ['pattern'], + }, + ], + }); + }); + + // work around, because the escapes get removed in snapshot by prettier + it('should have correct pattern', async () => { + expect( + parent.wizardUI.dialog!.querySelectorAll('wizard-textfield[pattern]')! + .length + ).to.equal(2); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[0] + .getAttribute('pattern') + ).to.equal(patterns.tRestrName1stL); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[1] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); }); + it('edits SDO attributes name', async () => { expect(doc.querySelector('DOType[id="Dummy.WYE"] > SDO[name="phsA"]')).to .exist; @@ -330,8 +429,37 @@ describe('DOType wizards', () => { }); it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); + // prettier does not support escaping in regexes of the /v flag + await expect(parent.wizardUI.dialog).dom.to.equalSnapshot({ + ignoreAttributes: [ + { + tags: ['wizard-textfield'], + attributes: ['pattern'], + }, + ], + }); }); + + // work around, because the escapes get removed in snapshot by prettier + it('should have correct pattern', async () => { + expect( + parent.wizardUI.dialog!.querySelectorAll('wizard-textfield[pattern]')! + .length + ).to.equal(2); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[0] + .getAttribute('pattern') + ).to.equal(patterns.tRestrName1stL); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[1] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); + }); + it('creates a new SDO element', async () => { expect( doc.querySelector( @@ -349,6 +477,7 @@ describe('DOType wizards', () => { ) ).to.exist; }); + it('creates yet another new SDO element', async () => { const name = 'newSDOElement2'; const desc = 'newSDOdesc'; diff --git a/test/integration/editors/triggered/ImportIedsPlugin.test.ts b/test/integration/editors/triggered/ImportIedsPlugin.test.ts index 55eda073c..a5d889f2b 100644 --- a/test/integration/editors/triggered/ImportIedsPlugin.test.ts +++ b/test/integration/editors/triggered/ImportIedsPlugin.test.ts @@ -41,7 +41,6 @@ describe('ImportIedsPlugin', () => { importDoc = await fetch('/test/testfiles/importieds/valid.iid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = importDoc; await element.updateComplete; }); @@ -49,7 +48,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc?.querySelector(':root > IED[name="TestImportIED"]')).to .not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc?.querySelector(':root > IED[name="TestImportIED"]')).to @@ -57,7 +56,7 @@ describe('ImportIedsPlugin', () => { }); it('adds the connectedap of the imported ied', async () => { - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -71,7 +70,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc.querySelector('SubNetwork[name="NewSubNetwork"]')).to .not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc.querySelector('SubNetwork[name="NewSubNetwork"]')).to @@ -87,10 +86,9 @@ describe('ImportIedsPlugin', () => { ied.setAttribute('manufacturer', 'Fancy-Vendy'); ied.setAttribute('type', 'Z#Mega$Y'); - element.importDoc = importDoc; await element.updateComplete; - element.prepareImport(); + element.prepareImport(importDoc, 'template.icd'); await parent.updateComplete; console.log( @@ -108,11 +106,7 @@ describe('ImportIedsPlugin', () => { ) .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = templateIED1; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(templateIED1, 'template.icd'); const templateIED2 = await fetch( '/test/testfiles/importieds/template.icd' @@ -120,10 +114,9 @@ describe('ImportIedsPlugin', () => { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = templateIED2; await element.updateComplete; - element.prepareImport(); + element.prepareImport(templateIED2, 'template.icd'); await parent.updateComplete; expect(element.doc.querySelector('IED[name="FancyVendy_ZMegaY_001"]')).to @@ -138,10 +131,7 @@ describe('ImportIedsPlugin', () => { ) .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = templateIED1; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(templateIED1, 'template.icd'); await parent.updateComplete; expect( @@ -157,7 +147,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(0); - element.prepareImport(); + element.prepareImport(importDoc, 'template.icd'); await parent.updateComplete; expect( @@ -189,7 +179,6 @@ describe('ImportIedsPlugin', () => { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = importDoc; await element.updateComplete; }); @@ -197,7 +186,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc?.querySelector(':root > IED[name="TestImportIED"]')).to .not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc?.querySelector(':root > IED[name="TestImportIED"]')).to @@ -210,7 +199,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(11); - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -224,7 +213,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(16); - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -239,7 +228,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(7); - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -254,7 +243,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(4); - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -267,7 +256,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc.querySelector('ConnectedAP[iedName="TestImportIED"]')) .to.not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc.querySelector('ConnectedAP[iedName="TestImportIED"]')) @@ -282,7 +271,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc.querySelector('SubNetwork[name="NewSubNetwork"]')).to .not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc.querySelector('SubNetwork[name="NewSubNetwork"]')).to @@ -290,7 +279,7 @@ describe('ImportIedsPlugin', () => { }); it('correctly transfers document element namespaces', async () => { - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -327,22 +316,14 @@ describe('ImportIedsPlugin', () => { ) .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = templateIED1; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(templateIED1, 'template.icd'); const templateIED2 = await fetch( '/test/testfiles/importieds/template.icd' ) .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = templateIED2; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(templateIED2, 'template.icd'); await parent.updateComplete; expect(element.doc.querySelector('IED[name="FancyVendy_ZMegaY_001"]')).to @@ -358,10 +339,7 @@ describe('ImportIedsPlugin', () => { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = multipleIedDoc; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(multipleIedDoc, 'multipleied.scd'); await element.updateComplete; expect(element.dialog).to.exist; @@ -378,10 +356,7 @@ describe('ImportIedsPlugin', () => { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = multipleIedDoc; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(multipleIedDoc, 'multipleied.scd'); await element.updateComplete; (( @@ -435,25 +410,17 @@ describe('ImportIedsPlugin', () => { importDoc = await fetch('/test/testfiles/importieds/invalid.iid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = importDoc; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(importDoc, 'invalid.iid'); expect(parent.history[0].kind).to.equal('error'); expect(parent.history[0].title).to.equal('[import.log.missingied]'); }); it('throws duplicate ied name error', async () => { - importDoc = await fetch('/test/testfiles/importieds/dublicate.iid') + importDoc = await fetch('/test/testfiles/importieds/duplicate.iid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = importDoc; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(importDoc, 'duplicate.iid'); expect(parent.history[0].kind).to.equal('error'); expect(parent.history[0].title).to.equal('[import.log.nouniqueied]'); @@ -463,11 +430,9 @@ describe('ImportIedsPlugin', () => { importDoc = await fetch('/test/testfiles/importieds/parsererror.iid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = importDoc; await element.updateComplete; - element.prepareImport(); + element.prepareImport(importDoc, 'parsererror.iid'); expect(parent.history[0].kind).to.equal('error'); expect(parent.history[0].title).to.equal('[import.log.parsererror]'); diff --git a/test/testfiles/importieds/dublicate.iid b/test/testfiles/importieds/duplicate.iid similarity index 100% rename from test/testfiles/importieds/dublicate.iid rename to test/testfiles/importieds/duplicate.iid diff --git a/test/unit/editors/subscription/fcda-binding-list.test.ts b/test/unit/editors/subscription/fcda-binding-list.test.ts index 33ecc45e3..594821b6f 100644 --- a/test/unit/editors/subscription/fcda-binding-list.test.ts +++ b/test/unit/editors/subscription/fcda-binding-list.test.ts @@ -174,7 +174,7 @@ describe('fcda-binding-list', () => { (( element.actionsMenu!.querySelector('.filter-subscribed') ))!.click(); - await new Promise(resolve => setTimeout(resolve, 200)); // await animation + await new Promise(resolve => setTimeout(resolve, 300)); // await animation await element.updateComplete; element.actionsMenuIcon.click(); @@ -182,7 +182,7 @@ describe('fcda-binding-list', () => { (( element.actionsMenu!.querySelector('.filter-not-subscribed') ))!.click(); - await new Promise(resolve => setTimeout(resolve, 200)); // await animation + await new Promise(resolve => setTimeout(resolve, 300)); // await animation await element.updateComplete; const fcdaList = element.shadowRoot?.querySelector('filtered-list'); diff --git a/test/unit/foundation.test.ts b/test/unit/foundation.test.ts index 60b8b5da6..5178f00bc 100644 --- a/test/unit/foundation.test.ts +++ b/test/unit/foundation.test.ts @@ -5,6 +5,7 @@ import { ComplexAction, depth, EditorAction, + find, findControlBlocks, findFCDAs, getChildElementsByTagName, @@ -26,7 +27,6 @@ import { newPendingStateEvent, newWizardEvent, SCLTag, - selector, tags, minAvailableLogicalNodeInstance, } from '../../src/foundation.js'; @@ -320,23 +320,23 @@ describe('foundation', () => { }); }); - describe('selector', () => { - it('returns negation pseudo-class for identity of type NaN', () => { + describe('find', () => { + it('returns null for the identity NaN', () => { const element = scl1.querySelector('Assotiation'); const ident = identity(element!); - expect(selector('Assotiation', ident)).to.equal(':not(*)'); + expect(find(scl1, 'Assotiation', ident)).to.equal(null); }); - it('returns correct selector for all tags except IEDName and ProtNs', () => { + it('returns correct element for all tags except IEDName and ProtNs', () => { Object.keys(tags).forEach(tag => { const element = Array.from(scl1.querySelectorAll(tag)).filter( item => !item.closest('Private') )[0]; if (element && tag !== 'IEDName' && tag !== 'ProtNs') - expect(element).to.satisfy((element: Element) => - element.isEqualNode( - scl1.querySelector(selector(tag, identity(element))) + expect(element) + .to.satisfy((element: Element) => + element.isEqualNode(find(scl1, tag, identity(element))) ) - ); + .and.to.equal(find(scl1, tag, identity(element))); }); }); });