diff --git a/packages/compas-open-scd/src/Editing.ts b/packages/compas-open-scd/src/Editing.ts deleted file mode 100644 index 27b105e77..000000000 --- a/packages/compas-open-scd/src/Editing.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { property } from 'lit-element'; -import { get } from 'lit-translate'; - -import { - Create, - Delete, - EditorAction, - EditorActionEvent, - getReference, - isCreate, - isDelete, - isMove, - isSimple, - isReplace, - LitElementConstructor, - Mixin, - Move, - newLogEvent, - newValidateEvent, - OpenDocEvent, - SCLTag, - SimpleAction, - Replace, - Update, - isUpdate, -} from 'open-scd/src/foundation.js'; - -/** Mixin that edits an `XML` `doc`, listening to [[`EditorActionEvent`]]s */ -export type EditingElement = Mixin; - -/** @typeParam TBase - a type extending `LitElement` - * @returns `Base` with an `XMLDocument` property "`doc`" and an event listener - * applying [[`EditorActionEvent`]]s and dispatching [[`LogEvent`]]s. */ -export function Editing(Base: TBase) { - class EditingElement extends Base { - /** The `XMLDocument` to be edited */ - @property({ attribute: false }) - doc: XMLDocument | null = null; - /** The name of the current [[`doc`]] */ - @property({ type: String }) docName = ''; - /** The ID of the current [[`doc`]] */ - @property({ type: String }) docId = ''; - - private checkCreateValidity(create: Create): boolean { - if (create.checkValidity !== undefined) return create.checkValidity(); - - if ( - !(create.new.element instanceof Element) || - !(create.new.parent instanceof Element) - ) - return true; - - const invalidNaming = - create.new.element.hasAttribute('name') && - Array.from(create.new.parent.children).some( - elm => - elm.tagName === (create.new.element).tagName && - elm.getAttribute('name') === - (create.new.element).getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: - create.new.parent instanceof HTMLElement - ? create.new.parent.tagName - : 'Document', - child: create.new.element.tagName, - name: create.new.element.getAttribute('name')!, - }), - }) - ); - - return false; - } - - const invalidId = - create.new.element.hasAttribute('id') && - Array.from( - create.new.parent.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (create.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: create.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onCreate(action: Create) { - if (!this.checkCreateValidity(action)) return false; - - if ( - action.new.reference === undefined && - action.new.element instanceof Element && - action.new.parent instanceof Element - ) - action.new.reference = getReference( - action.new.parent, - action.new.element.tagName - ); - else action.new.reference = action.new.reference ?? null; - - action.new.parent.insertBefore(action.new.element, action.new.reference); - return true; - } - - private logCreate(action: Create) { - const name = - action.new.element instanceof Element - ? action.new.element.tagName - : get('editing.node'); - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.created', { name }), - action, - }) - ); - } - - private onDelete(action: Delete) { - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.old.element.parentNode !== action.old.parent) return false; - - action.old.parent.removeChild(action.old.element); - return true; - } - - private logDelete(action: Delete) { - const name = - action.old.element instanceof Element - ? action.old.element.tagName - : get('editing.node'); - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.deleted', { name }), - action, - }) - ); - } - - private checkMoveValidity(move: Move): boolean { - if (move.checkValidity !== undefined) return move.checkValidity(); - - const invalid = - move.old.element.hasAttribute('name') && - move.new.parent !== move.old.parent && - Array.from(move.new.parent.children).some( - elm => - elm.tagName === move.old.element.tagName && - elm.getAttribute('name') === move.old.element.getAttribute('name') - ); - - if (invalid) - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.move', { - name: move.old.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: move.new.parent.tagName, - child: move.old.element.tagName, - name: move.old.element.getAttribute('name')!, - }), - }) - ); - - return !invalid; - } - - private onMove(action: Move) { - if (!this.checkMoveValidity(action)) return false; - - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.new.reference === undefined) - action.new.reference = getReference( - action.new.parent, - action.old.element.tagName - ); - - action.new.parent.insertBefore(action.old.element, action.new.reference); - return true; - } - - private logMove(action: Move) { - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.moved', { - name: action.old.element.tagName, - }), - action: action, - }) - ); - } - - private checkReplaceValidity(replace: Replace): boolean { - if (replace.checkValidity !== undefined) return replace.checkValidity(); - - const invalidNaming = - replace.new.element.hasAttribute('name') && - replace.new.element.getAttribute('name') !== - replace.old.element.getAttribute('name') && - Array.from(replace.old.element.parentElement?.children ?? []).some( - elm => - elm.tagName === replace.new.element.tagName && - elm.getAttribute('name') === - replace.new.element.getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: replace.old.element.parentElement!.tagName, - child: replace.new.element.tagName, - name: replace.new.element.getAttribute('name')!, - }), - }) - ); - - return false; - } - - const invalidId = - replace.new.element.hasAttribute('id') && - replace.new.element.getAttribute('id') !== - replace.old.element.getAttribute('id') && - Array.from( - replace.new.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (replace.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: replace.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onReplace(action: Replace) { - if (!this.checkReplaceValidity(action)) return false; - - action.new.element.append(...Array.from(action.old.element.children)); - action.old.element.replaceWith(action.new.element); - return true; - } - - private logUpdate(action: Replace | Update) { - const name = isReplace(action) - ? action.new.element.tagName - : (action as Update).element.tagName; - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.updated', { - name, - }), - action: action, - }) - ); - } - - private checkUpdateValidity(update: Update): boolean { - if (update.checkValidity !== undefined) return update.checkValidity(); - - if (update.oldAttributes['name'] !== update.newAttributes['name']) { - const invalidNaming = Array.from( - update.element.parentElement?.children ?? [] - ).some( - elm => - elm.tagName === update.element.tagName && - elm.getAttribute('name') === update.newAttributes['name'] - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: update.element.parentElement!.tagName, - child: update.element.tagName, - name: update.newAttributes['name']!, - }), - }) - ); - - return false; - } - } - - const invalidId = - update.newAttributes['id'] && - Array.from( - update.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some(elm => elm.getAttribute('id') === update.newAttributes['id']); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.idClash', { - id: update.newAttributes['id']!, - }), - }) - ); - - return false; - } - - return true; - } - - private onUpdate(action: Update) { - if (!this.checkUpdateValidity(action)) return false; - - Array.from(action.element.attributes).forEach(attr => - action.element.removeAttributeNode(attr) - ); - - Object.entries(action.newAttributes).forEach(([key, value]) => { - if (value !== null && value !== undefined) - action.element.setAttribute(key, value); - }); - - return true; - } - - private onSimpleAction(action: SimpleAction) { - if (isMove(action)) return this.onMove(action as Move); - else if (isCreate(action)) return this.onCreate(action as Create); - else if (isDelete(action)) return this.onDelete(action as Delete); - else if (isReplace(action)) return this.onReplace(action as Replace); - else if (isUpdate(action)) return this.onUpdate(action as Update); - } - - private logSimpleAction(action: SimpleAction) { - if (isMove(action)) this.logMove(action as Move); - else if (isCreate(action)) this.logCreate(action as Create); - else if (isDelete(action)) this.logDelete(action as Delete); - else if (isReplace(action)) this.logUpdate(action as Replace); - else if (isUpdate(action)) this.logUpdate(action as Update); - } - - private async onAction(event: EditorActionEvent) { - if (isSimple(event.detail.action)) { - if (this.onSimpleAction(event.detail.action)) - this.logSimpleAction(event.detail.action); - } else if (event.detail.action.actions.length > 0) { - event.detail.action.actions.forEach(element => - this.onSimpleAction(element) - ); - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: event.detail.action.title, - action: event.detail.action, - }) - ); - } else return; - - if (!this.doc) return; - - await this.updateComplete; - this.dispatchEvent(newValidateEvent()); - } - - private async onOpenDoc(event: OpenDocEvent) { - this.doc = event.detail.doc; - this.docName = event.detail.docName; - this.docId = event.detail.docId ?? ''; - - await this.updateComplete; - - this.dispatchEvent(newValidateEvent()); - - this.dispatchEvent( - newLogEvent({ - kind: 'info', - title: get('openSCD.loaded', { name: this.docName }), - }) - ); - } - - constructor(...args: any[]) { - super(...args); - - this.addEventListener('editor-action', this.onAction); - this.addEventListener('open-doc', this.onOpenDoc); - } - } - - return EditingElement; -} diff --git a/packages/compas-open-scd/src/Plugging.ts b/packages/compas-open-scd/src/Plugging.ts index 51432b20c..b89a1161c 100644 --- a/packages/compas-open-scd/src/Plugging.ts +++ b/packages/compas-open-scd/src/Plugging.ts @@ -22,7 +22,7 @@ import { Switch } from '@material/mwc-switch'; import { TextField } from '@material/mwc-textfield'; import { ifImplemented, Mixin } from 'open-scd/src/foundation.js'; -import { EditingElement } from './Editing.js'; +import { EditingElement } from 'open-scd/src/Editing.js'; import { officialPlugins } from '../public/js/plugins.js'; import { Nsdoc } from 'open-scd/src/foundation/nsdoc.js'; import { HistoringElement } from './Historing.js'; @@ -99,7 +99,7 @@ function staticTagHtml( type PluginKind = 'editor' | 'menu' | 'validator'; const menuPosition = ['top', 'middle', 'bottom'] as const; -type MenuPosition = typeof menuPosition[number]; +type MenuPosition = (typeof menuPosition)[number]; export type Plugin = { name: string; diff --git a/packages/compas-open-scd/src/Setting.ts b/packages/compas-open-scd/src/Setting.ts index 2d24b8b9a..205d0466b 100644 --- a/packages/compas-open-scd/src/Setting.ts +++ b/packages/compas-open-scd/src/Setting.ts @@ -21,7 +21,7 @@ import { import { Language, languages, loader } from './translations/loader.js'; import 'open-scd/src/WizardDivider.js'; -import { WizardDialog } from './wizard-dialog.js'; +import { WizardDialog } from 'open-scd/src/wizard-dialog.js'; import { iec6185072, diff --git a/packages/compas-open-scd/src/compas/Compasing.ts b/packages/compas-open-scd/src/compas/Compasing.ts index cbc1b338d..8e639cfca 100644 --- a/packages/compas-open-scd/src/compas/Compasing.ts +++ b/packages/compas-open-scd/src/compas/Compasing.ts @@ -1,7 +1,7 @@ import { html, query, state, property, TemplateResult } from 'lit-element'; import { Mixin } from 'open-scd/src/foundation.js'; -import { EditingElement } from '../Editing.js'; +import { EditingElement } from 'open-scd/src/Editing.js'; import { CompasUserInfoService } from '../compas-services/CompasUserInfoService.js'; diff --git a/packages/compas-open-scd/src/menu/CompasUpdateSubstation.ts b/packages/compas-open-scd/src/menu/CompasUpdateSubstation.ts index 62268199e..5dd195eee 100644 --- a/packages/compas-open-scd/src/menu/CompasUpdateSubstation.ts +++ b/packages/compas-open-scd/src/menu/CompasUpdateSubstation.ts @@ -4,7 +4,7 @@ import { get } from 'lit-translate'; import { newWizardEvent, Wizard } from 'open-scd/src/foundation.js'; import { DocRetrievedEvent } from '../compas/CompasOpen.js'; -import { mergeSubstation } from './UpdateSubstation.js'; +import { mergeSubstation } from 'open-scd/src/menu/UpdateSubstation.js'; import '../compas/CompasOpen.js'; diff --git a/packages/compas-open-scd/src/menu/UpdateSubstation.ts b/packages/compas-open-scd/src/menu/UpdateSubstation.ts deleted file mode 100644 index cacfdf44b..000000000 --- a/packages/compas-open-scd/src/menu/UpdateSubstation.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { css, html, LitElement, query, TemplateResult } from 'lit-element'; -import { get } from 'lit-translate'; - -import { - crossProduct, - find, - identity, - newWizardEvent, - SCLTag, - tags, -} from 'open-scd/src/foundation.js'; -import { Diff, mergeWizard } from 'open-scd/src/wizards.js'; - -export function isValidReference( - doc: XMLDocument, - identity: string | number -): boolean { - if (typeof identity !== 'string') return false; - const [iedName, ldInst, prefix, lnClass, lnInst] = identity.split(/[ /]/); - - if (!iedName || !lnClass) return false; - - if (ldInst === '(Client)') { - const [ - iedNameSelectors, - prefixSelectors, - lnClassSelectors, - lnInstSelectors, - ] = [ - [`IED[name="${iedName}"]`], - prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'], - [`LN[lnClass="${lnClass}"]`], - lnInst ? [`[inst="${lnInst}"]`] : [':not([inst])', '[inst=""]'], - ]; - - return ( - doc.querySelector( - crossProduct( - iedNameSelectors, - ['>AccessPoint>'], - lnClassSelectors, - prefixSelectors, - lnInstSelectors - ) - .map(strings => strings.join('')) - .join(',') - ) !== null - ); - } - - const [ - iedNameSelectors, - ldInstSelectors, - prefixSelectors, - lnClassSelectors, - lnInstSelectors, - ] = [ - [`IED[name="${iedName}"]`], - [`LDevice[inst="${ldInst}"]`], - prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'], - lnClass === 'LLN0' ? [`LN0`] : [`LN[lnClass="${lnClass}"]`], - lnInst ? [`[inst="${lnInst}"]`] : [':not([inst])', '[inst=""]'], - ]; - - return ( - doc.querySelector( - crossProduct( - iedNameSelectors, - [' '], - ldInstSelectors, - ['>'], - lnClassSelectors, - prefixSelectors, - lnInstSelectors - ) - .map(strings => strings.join('')) - .join(',') - ) !== null - ); -} - -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' - ? find(currentDoc, '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' && - (find(currentDoc, '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; - } - const text = await file.text(); - const doc = new DOMParser().parseFromString(text, 'application/xml'); - - mergeSubstation(this, this.doc, doc); - this.pluginFileUI.onchange = null; - } - - async run(): Promise { - this.pluginFileUI.click(); - } - - render(): TemplateResult { - return html` - ((event.target).value = '')} - @change=${this.updateSubstation} - id="update-substation-plugin-input" accept=".sed,.scd,.ssd,.iid,.cid" type="file">`; - } - - static styles = css` - input { - width: 0; - height: 0; - opacity: 0; - } - `; -} diff --git a/packages/compas-open-scd/src/open-scd.ts b/packages/compas-open-scd/src/open-scd.ts index 52258e076..3cbe252de 100644 --- a/packages/compas-open-scd/src/open-scd.ts +++ b/packages/compas-open-scd/src/open-scd.ts @@ -9,10 +9,13 @@ import { import { ListItem } from '@material/mwc-list/mwc-list-item'; -import { newOpenDocEvent, newPendingStateEvent } from 'open-scd/src/foundation.js'; +import { + newOpenDocEvent, + newPendingStateEvent, +} from 'open-scd/src/foundation.js'; import { getTheme } from 'open-scd/src/themes.js'; -import { Editing } from './Editing.js'; +import { Editing } from 'open-scd/src/Editing.js'; import { Hosting } from './Hosting.js'; import { Historing } from './Historing.js'; import { Plugging } from './Plugging.js'; diff --git a/packages/compas-open-scd/src/wizard-dialog.ts b/packages/compas-open-scd/src/wizard-dialog.ts deleted file mode 100644 index 1a5f3938b..000000000 --- a/packages/compas-open-scd/src/wizard-dialog.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { - customElement, - css, - queryAll, - LitElement, - property, - internalProperty, - TemplateResult, - html, - query, -} from 'lit-element'; -import { ifDefined } from 'lit-html/directives/if-defined'; -import { get, translate } from 'lit-translate'; - -import '@material/mwc-button'; -import '@material/mwc-dialog'; -import '@material/mwc-icon-button'; -import '@material/mwc-icon-button-toggle'; -import '@material/mwc-menu'; -import { Dialog } from '@material/mwc-dialog'; -import { IconButton } from '@material/mwc-icon-button'; -import { List } from '@material/mwc-list'; -import { Menu } from '@material/mwc-menu'; - -import 'ace-custom-element'; -import 'open-scd/src/wizard-checkbox.js'; -import 'open-scd/src/wizard-textfield.js'; -import 'open-scd/src/wizard-select.js'; -import { - newActionEvent, - Wizard, - WizardInputElement, - WizardPage, - newWizardEvent, - WizardActor, - wizardInputSelector, - isWizardFactory, - checkValidity, - reportValidity, - Delete, - Create, - identity, - WizardInput, - WizardMenuActor, - formatXml, -} from 'open-scd/src/foundation.js'; - -function renderWizardInput( - input: TemplateResult | WizardInput -): TemplateResult { - if (input instanceof TemplateResult) return input; - - if (input.kind === 'Checkbox') - return html``; - - if (input.kind === 'Select') - return html`${input.values.map( - value => html`${value}` - )}`; - - return html``; -} - -function dialogInputs(dialog?: Dialog): WizardInputElement[] { - return Array.from(dialog?.querySelectorAll(wizardInputSelector) ?? []); -} - -function dialogValid(dialog?: Dialog): boolean { - return dialogInputs(dialog).every(checkValidity); -} - -function codeAction(element: Element): WizardActor { - return inputs => { - const text = inputs[0].value!; - if (!text || !element.parentElement) return []; - const desc = { - parent: element.parentElement!, - reference: element.nextSibling, - element, - }; - const del: Delete = { - old: desc, - checkValidity: () => true, - }; - const cre: Create = { - new: { - ...desc, - element: new DOMParser().parseFromString(text, 'application/xml') - .documentElement, - }, - checkValidity: () => true, - }; - return [ - { - actions: [del, cre], - title: get('code.log', { - id: identity(element), - }), - }, - ]; - }; -} - -/** A wizard style dialog consisting of several pages commiting some - * [[`EditorAction`]] on completion and aborting on dismissal. */ -@customElement('wizard-dialog') -export class WizardDialog extends LitElement { - /** The [[`Wizard`]] implemented by this dialog. */ - @property({ type: Array }) - wizard: Wizard = []; - /** Index of the currently active [[`WizardPage`]] */ - @internalProperty() - pageIndex = 0; - - @queryAll('mwc-dialog') dialogs!: NodeListOf; - @queryAll(wizardInputSelector) inputs!: NodeListOf; - @query('.actions-menu') actionsMenu!: Menu; - @query('mwc-icon-button[icon="more_vert"]') menuButton!: IconButton; - - /** The `Dialog` showing the active [[`WizardPage`]]. */ - get dialog(): Dialog | undefined { - return this.dialogs[this.pageIndex]; - } - - get code(): boolean { - return ( - (this.dialog?.querySelector('mwc-icon-button-toggle')?.on ?? false) && - localStorage.getItem('mode') === 'pro' - ); - } - - /** Checks the inputs of all [[`WizardPage`]]s for validity. */ - checkValidity(): boolean { - return Array.from(this.inputs).every(checkValidity); - } - - private get firstInvalidPage(): number { - return Array.from(this.dialogs).findIndex(dialog => !dialogValid(dialog)); - } - - prev(): void { - if (this.pageIndex <= 0) return; - this.pageIndex--; - this.dialog?.show(); - } - - async next(): Promise { - if (dialogValid(this.dialog)) { - if (this.wizard.length > this.pageIndex + 1) this.pageIndex++; - this.dialog?.show(); - } else { - this.dialog?.show(); - await this.dialog?.updateComplete; - dialogInputs(this.dialog).map(reportValidity); - } - } - - /** Commits `action` if all inputs are valid, reports validity otherwise. */ - async act(action?: WizardActor, primary = true): Promise { - if (action === undefined) return false; - const wizardInputs = Array.from(this.inputs); - const wizardList = ( - this.dialog?.querySelector('filtered-list,mwc-list') - ); - if (!this.checkValidity()) { - this.pageIndex = this.firstInvalidPage; - wizardInputs.map(reportValidity); - return false; - } - - const wizardActions = action(wizardInputs, this, wizardList); - if (wizardActions.length > 0) { - if (primary) this.wizard[this.pageIndex].primary = undefined; - else this.wizard[this.pageIndex].secondary = undefined; - this.dispatchEvent(newWizardEvent()); - } - wizardActions.forEach(wa => - isWizardFactory(wa) - ? this.dispatchEvent(newWizardEvent(wa)) - : this.dispatchEvent(newActionEvent(wa)) - ); - return true; - } - - /** Triggers menu action callback */ - async menuAct(action?: WizardMenuActor): Promise { - if (!action) return; - - action(this); - } - - private onClosed(ae: CustomEvent<{ action: string } | null>): void { - if (!(ae.target instanceof Dialog && ae.detail?.action)) return; - if (ae.detail.action === 'close') this.dispatchEvent(newWizardEvent()); - else if (ae.detail.action === 'prev') this.prev(); - else if (ae.detail.action === 'next') this.next(); - } - - constructor() { - super(); - - this.act = this.act.bind(this); - this.renderPage = this.renderPage.bind(this); - } - - updated(changedProperties: Map): void { - if (changedProperties.has('wizard')) { - this.pageIndex = 0; - while ( - this.wizard.findIndex(page => page.initial) > this.pageIndex && - dialogValid(this.dialog) - ) { - this.dialog?.close(); - this.next(); - } - this.dialog?.show(); - } - if (this.wizard[this.pageIndex]?.primary?.auto) { - this.updateComplete.then(() => - this.act(this.wizard[this.pageIndex].primary!.action) - ); - } - - if (this.actionsMenu) - this.actionsMenu.anchor = this.menuButton; - } - - renderMenu(page: WizardPage): TemplateResult { - const someIconsDefined = page.menuActions?.some( - menuAction => menuAction.icon - ); - - return html` { - if (!this.actionsMenu.open) this.actionsMenu.show(); - else this.actionsMenu.close(); - }} - > - - ${page.menuActions!.map( - menuAction => - html` this.menuAct(menuAction.action)} - > - ${menuAction.label} - ${menuAction.icon - ? html`${menuAction.icon}` - : html``} - ` - )} - `; - } - - renderPage(page: WizardPage, index: number): TemplateResult { - const showCodeToggleButton = - page.element && localStorage.getItem('mode') === 'pro'; - const extraWidth = - showCodeToggleButton && page.menuActions - ? 96 - : showCodeToggleButton || page.menuActions - ? 48 - : 0; - - return html` - ${showCodeToggleButton || page.menuActions - ? html`` - : ''} -
- ${this.code && page.element - ? html`` - : page.content?.map(renderWizardInput)} -
- ${index > 0 - ? html`` - : html``} - ${page.secondary - ? html` this.act(page.secondary?.action, false)} - icon="${page.secondary.icon}" - label="${page.secondary.label}" - >` - : html``} - ${this.code && page.element - ? (page.element.parentElement) - ? html` this.act(codeAction(page.element!))} - icon="code" - label="${translate('save')}" - trailingIcon - >` - : html `` - : page.primary - ? html` this.act(page.primary?.action)} - icon="${page.primary.icon}" - label="${page.primary.label}" - trailingIcon - >` - : index + 1 < (this.wizard?.length ?? 0) - ? html`` - : html``} -
`; - } - - render(): TemplateResult { - return html`${this.wizard.map(this.renderPage)}`; - } - - static styles = css` - mwc-dialog { - --mdc-dialog-max-width: 92vw; - } - - mwc-dialog > nav { - position: absolute; - top: 8px; - right: 14px; - color: var(--base00); - } - - mwc-dialog > nav > mwc-icon-button-toggle[on] { - color: var(--mdc-theme-primary); - } - - #wizard-content { - display: flex; - flex-direction: column; - } - - #wizard-content > * { - display: block; - margin-top: 16px; - } - - *[iconTrailing='search'] { - --mdc-shape-small: 28px; - } - `; -} diff --git a/packages/compas-open-scd/src/wizard-textfield.ts b/packages/compas-open-scd/src/wizard-textfield.ts deleted file mode 100644 index 6fc9fce44..000000000 --- a/packages/compas-open-scd/src/wizard-textfield.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { - customElement, - html, - internalProperty, - property, - query, - TemplateResult, -} from 'lit-element'; -import { get, translate } from 'lit-translate'; - -import '@material/mwc-icon-button'; -import '@material/mwc-list/mwc-list-item'; -import '@material/mwc-menu'; -import '@material/mwc-switch'; -import { IconButton } from '@material/mwc-icon-button'; -import { Menu } from '@material/mwc-menu'; -import { SingleSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; -import { Switch } from '@material/mwc-switch'; -import { TextField } from '@material/mwc-textfield'; - -/** A potentially `nullable` `TextField` that allows for selection of an SI - * `multiplier` if an SI `unit` is given. - * - * NB: Use `maybeValue: string | null` instead of `value` if `nullable`!*/ -@customElement('wizard-textfield') -export class WizardTextField extends TextField { - /** Whether [[`maybeValue`]] may be `null` */ - @property({ type: Boolean }) - nullable = false; - /** Selectable SI multipliers for a non-empty [[`unit`]]. */ - @property({ type: Array }) - multipliers = [null, '']; - private multiplierIndex = 0; - @property({ type: String }) - get multiplier(): string | null { - if (this.unit == '') return null; - return ( - this.multipliers[this.multiplierIndex] ?? this.multipliers[0] ?? null - ); - } - set multiplier(value: string | null) { - const index = this.multipliers.indexOf(value); - if (index >= 0) this.multiplierIndex = index; - this.suffix = (this.multiplier ?? '') + this.unit; - } - /** SI Unit, must be non-empty to allow for selecting a [[`multiplier`]]. - * Overrides `suffix`. */ - @property({ type: String }) - unit = ''; - private isNull = false; - @internalProperty() - private get null(): boolean { - return this.nullable && this.isNull; - } - private set null(value: boolean) { - if (!this.nullable || value === this.isNull) return; - this.isNull = value; - if (this.null) this.disable(); - else this.enable(); - } - /** Replacement for `value`, can only be `null` if [[`nullable`]]. */ - @property({ type: String }) - get maybeValue(): string | null { - return this.null ? null : this.value; - } - set maybeValue(value: string | null) { - if (value === null) { - this.null = true; - this.value = ''; - } else { - this.null = false; - this.value = value; - } - } - /** The default `value` displayed if [[`maybeValue`]] is `null`. */ - @property({ type: String }) - defaultValue = ''; - /** Additional values that cause validation to fail. */ - @property({ type: Array }) - reservedValues: string[] = []; - - // FIXME: workaround to allow disable of the whole component - need basic refactor - private disabledSwitch = false; - - @query('mwc-switch') nullSwitch?: Switch; - @query('mwc-menu') multiplierMenu?: Menu; - @query('mwc-icon-button') multiplierButton?: IconButton; - - private nulled: string | null = null; - - private selectMultiplier(se: SingleSelectedEvent): void { - this.multiplier = this.multipliers[se.detail.index]; - } - - private enable(): void { - if (this.nulled === null) return; - this.value = this.nulled; - this.nulled = null; - this.helperPersistent = false; - this.disabled = false; - } - - private disable(): void { - if (this.nulled !== null) return; - this.nulled = this.value; - this.value = this.defaultValue; - this.helperPersistent = true; - this.disabled = true; - } - - async firstUpdated(): Promise { - await super.firstUpdated(); - if (this.multiplierMenu) - this.multiplierMenu.anchor = this.multiplierButton ?? null; - } - - checkValidity(): boolean { - if ( - this.reservedValues && - this.reservedValues.some(array => array === this.value) - ) { - this.setCustomValidity(get('textfield.unique')); - return false; - } - // Reset to prevent super.checkValidity to always return false - this.setCustomValidity(''); - return super.checkValidity(); - } - - constructor() { - super(); - - this.disabledSwitch = this.hasAttribute('disabled'); - } - - renderUnitSelector(): TemplateResult { - if (this.multipliers.length && this.unit) - return html`
- this.multiplierMenu?.show()} - > - ${this.renderMulplierList()} -
`; - else return html``; - } - - renderMulplierList(): TemplateResult { - return html`${this.multipliers.map( - multiplier => - html`${multiplier === null - ? translate('textfield.noMultiplier') - : multiplier}` - )}`; - } - - renderSwitch(): TemplateResult { - if (this.nullable) { - return html` { - this.null = !this.nullSwitch!.checked; - }} - >`; - } - return html``; - } - - render(): TemplateResult { - return html` -
-
${super.render()}
- ${this.renderUnitSelector()} -
- ${this.renderSwitch()} -
-
- `; - } -} diff --git a/packages/compas-open-scd/test/foundation.ts b/packages/compas-open-scd/test/foundation.ts deleted file mode 100644 index e947dec6c..000000000 --- a/packages/compas-open-scd/test/foundation.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable no-control-regex */ - -import fc, { Arbitrary, array, hexaString, integer, tuple } from 'fast-check'; -import { patterns } from 'open-scd/src/foundation.js'; - -export function invertedRegex( - re: RegExp, - minLength = 0, - maxLength?: number -): Arbitrary { - return fc - .string({ minLength, maxLength: maxLength ?? 2 * minLength + 10 }) - .filter(char => !re.test(char)); -} - -export function regexString( - re: RegExp, - minLength = 0, - maxLength?: number -): Arbitrary { - return fc - .string({ minLength, maxLength: maxLength ?? 2 * minLength + 10 }) - .filter(char => re.test(char)); -} - -export function ipV6(): Arbitrary { - const h16Arb = hexaString({ minLength: 1, maxLength: 4 }); - const ls32Arb = tuple(h16Arb, h16Arb).map(([a, b]) => `${a}:${b}`); - return tuple(array(h16Arb, { minLength: 6, maxLength: 6 }), ls32Arb).map( - ([eh, l]) => `${eh.join(':')}:${l}` - ); -} - -export function MAC(): Arbitrary { - const h16Arb = hexaString({ minLength: 2, maxLength: 2 }); - const ls32Arb = tuple(h16Arb, h16Arb).map(([a, b]) => `${a}-${b}`); - return tuple(array(h16Arb, { minLength: 4, maxLength: 4 }), ls32Arb).map( - ([eh, l]) => `${eh.join('-')}-${l}` - ); -} - -export function ipV6SubNet(): Arbitrary { - return integer({ min: 1, max: 127 }).map(num => `/${num}`); -} - -export const regExp = { - tIEDName: /^[A-Za-z][0-9A-Za-z_]*$/, - tLDInst: /^[A-Za-z][0-9A-Za-z_]*$/, - tPrefix: /^[A-Za-z][0-9A-Za-z_]*$/, - tLNClass: /^[A-Z]{1,4}$/, - tLNInst: /^[0-9]{0,12}$/, - decimal: new RegExp(`^${patterns.decimal}$`), - unsigned: new RegExp(`^${patterns.unsigned}$`), - tName: new RegExp(`^${patterns.normalizedString}$`), - desc: new RegExp(`^${patterns.normalizedString}$`), - IPv4: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/, - IPv6: /^([0-9A-F]{2}-){5}[0-9A-F]{2}$/, - MAC: /^([0-9A-F]{2}-){5}[0-9A-F]{2}$/, - OSI: /^[0-9A-F]+$/, - OSIAPi: /^[0-9\u002C]+$/, - OSIid: /^[0-9]+$/, - token: new RegExp('^' + patterns.nmToken + '$'), - tAsciName: /^[A-Za-z][0-9A-Za-z_]$/, - tRestrName1stL: /^[a-z][0-9A-Za-z]*$/, - abstractDataAttributeName: - /^((T)|(Test)|(Check)|(SIUnit)|(Open)|(SBO)|(SBOw)|(Cancel)|[a-z][0-9A-Za-z]*)$/, - lnClass: /^(LLN0)|[A-Z]{4,4}$/, -}; - -export const inverseRegExp = { - unsigned: /[^0-9.+]|.[^0-9.]/, - decimal: /[^0-9.+-]|.[^0-9.]/, - integer: /[^0-9+-]/, - uint: /[^0-9+]/, -}; diff --git a/packages/compas-open-scd/test/integration/compas-editors/CompasVersions.test.ts b/packages/compas-open-scd/test/integration/compas-editors/CompasVersions.test.ts index ad3993983..bec039bb4 100644 --- a/packages/compas-open-scd/test/integration/compas-editors/CompasVersions.test.ts +++ b/packages/compas-open-scd/test/integration/compas-editors/CompasVersions.test.ts @@ -1,15 +1,15 @@ -import {expect, fixtureSync, html, waitUntil} from '@open-wc/testing'; -import sinon, {SinonSpy, spy, SinonStub} from "sinon"; +import { expect, fixtureSync, html, waitUntil } from '@open-wc/testing'; +import sinon, { SinonSpy, spy, SinonStub } from 'sinon'; -import {Editing} from '../../../src/Editing.js'; -import {Wizarding} from 'open-scd/src/Wizarding.js'; +import { Editing } from 'open-scd/src/Editing.js'; +import { Wizarding } from 'open-scd/src/Wizarding.js'; import { BASIC_VERSIONS_LIST_RESPONSE, stubFetchResponseFunction, - VERSION_ENTRY_ELEMENT_NAME -} from "../../unit/compas/CompasSclDataServiceResponses.js"; -import CompasVersionsPlugin from "../../../src/compas-editors/CompasVersions.js"; + VERSION_ENTRY_ELEMENT_NAME, +} from '../../unit/compas/CompasSclDataServiceResponses.js'; +import CompasVersionsPlugin from '../../../src/compas-editors/CompasVersions.js'; import { IconButton } from '@material/mwc-icon-button'; describe('compas-versions-plugin', () => { @@ -28,18 +28,22 @@ describe('compas-versions-plugin', () => { doc = await fetch('/test/testfiles/compas/test-scd.cid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - }) + }); describe('no-compas-document', () => { beforeEach(async () => { - element = fixtureSync(html` - - `); - - stub = stubFetchResponseFunction(element, FETCH_FUNCTION, undefined, VERSION_ENTRY_ELEMENT_NAME, + element = fixtureSync(html` + `); + + stub = stubFetchResponseFunction( + element, + FETCH_FUNCTION, + undefined, + VERSION_ENTRY_ELEMENT_NAME, () => { // Should not be called. - }); + } + ); await element.updateComplete; await waitUntil(() => element.historyItem !== undefined); @@ -57,14 +61,21 @@ describe('compas-versions-plugin', () => { describe('show-loading', () => { beforeEach(async () => { - element = fixtureSync(html` - - `); - - stub = stubFetchResponseFunction(element, FETCH_FUNCTION, undefined, VERSION_ENTRY_ELEMENT_NAME, + element = fixtureSync(html` + `); + + stub = stubFetchResponseFunction( + element, + FETCH_FUNCTION, + undefined, + VERSION_ENTRY_ELEMENT_NAME, () => { // Do nothing, so loading... will be displayed. - }); + } + ); await element.updateComplete; }); @@ -80,14 +91,21 @@ describe('compas-versions-plugin', () => { describe('no-items-in-list', () => { beforeEach(async () => { - element = fixtureSync(html` - - `); - - stub = stubFetchResponseFunction(element, FETCH_FUNCTION, undefined, VERSION_ENTRY_ELEMENT_NAME, + element = fixtureSync(html` + `); + + stub = stubFetchResponseFunction( + element, + FETCH_FUNCTION, + undefined, + VERSION_ENTRY_ELEMENT_NAME, (result: Element[]) => { element.historyItem = result; - }); + } + ); await element.updateComplete; await waitUntil(() => element.historyItem !== undefined); @@ -106,14 +124,21 @@ describe('compas-versions-plugin', () => { describe('items-in-list', () => { let wizardEvent: SinonSpy; beforeEach(async () => { - element = fixtureSync(html` - - `); - - stub = stubFetchResponseFunction(element, FETCH_FUNCTION, BASIC_VERSIONS_LIST_RESPONSE, VERSION_ENTRY_ELEMENT_NAME, + element = fixtureSync(html` + `); + + stub = stubFetchResponseFunction( + element, + FETCH_FUNCTION, + BASIC_VERSIONS_LIST_RESPONSE, + VERSION_ENTRY_ELEMENT_NAME, (result: Element[]) => { element.historyItem = result; - }); + } + ); wizardEvent = spy(); window.addEventListener('wizard', wizardEvent); await element.updateComplete; @@ -125,8 +150,9 @@ describe('compas-versions-plugin', () => { }); it('has 3 item entries', () => { - expect(element.shadowRoot!.querySelectorAll('mwc-list > mwc-check-list-item')) - .to.have.length(3); + expect( + element.shadowRoot!.querySelectorAll('mwc-list > mwc-check-list-item') + ).to.have.length(3); }); it('dispatches a wizard event when edit button is clicked', () => { @@ -138,24 +164,37 @@ describe('compas-versions-plugin', () => { }); it('first entry has correct buttons', () => { - expect(element.shadowRoot!.querySelectorAll('mwc-list > mwc-check-list-item').length) - .to.be.greaterThan(1); + expect( + element.shadowRoot!.querySelectorAll('mwc-list > mwc-check-list-item') + .length + ).to.be.greaterThan(1); // Retrieve the first item after checking that there are items. - const item = element.shadowRoot!.querySelectorAll('mwc-list > mwc-check-list-item')[0]; + const item = element.shadowRoot!.querySelectorAll( + 'mwc-list > mwc-check-list-item' + )[0]; // There should be 2 buttons, first the restore, second the delete. expect(item.querySelectorAll('span > mwc-icon')).to.have.length(2); - expect(item.querySelectorAll('span > mwc-icon')[0].textContent).to.be.equal('restore'); - expect(item.querySelectorAll('span > mwc-icon')[1].textContent).to.be.equal('delete'); + expect( + item.querySelectorAll('span > mwc-icon')[0].textContent + ).to.be.equal('restore'); + expect( + item.querySelectorAll('span > mwc-icon')[1].textContent + ).to.be.equal('delete'); }); it('last entry has one buttons', () => { - expect(element.shadowRoot!.querySelectorAll('mwc-list > mwc-check-list-item')) - .to.have.length(3); + expect( + element.shadowRoot!.querySelectorAll('mwc-list > mwc-check-list-item') + ).to.have.length(3); // Retrieve the last item after checking that there are 3 items. - const item = element.shadowRoot!.querySelectorAll('mwc-list > mwc-check-list-item')[2]; + const item = element.shadowRoot!.querySelectorAll( + 'mwc-list > mwc-check-list-item' + )[2]; // There should be 1 buttons, the restore button. expect(item.querySelectorAll('span > mwc-icon')).to.have.length(1); - expect(item.querySelectorAll('span > mwc-icon')[0].textContent).to.be.equal('restore'); + expect( + item.querySelectorAll('span > mwc-icon')[0].textContent + ).to.be.equal('restore'); }); it('looks like the latest snapshot', async () => { diff --git a/packages/compas-open-scd/test/integration/compas-editors/autogen-substation.test.ts b/packages/compas-open-scd/test/integration/compas-editors/autogen-substation.test.ts index 1d78ed4a5..e07561e5d 100644 --- a/packages/compas-open-scd/test/integration/compas-editors/autogen-substation.test.ts +++ b/packages/compas-open-scd/test/integration/compas-editors/autogen-substation.test.ts @@ -1,7 +1,7 @@ import { expect, fixture, html } from '@open-wc/testing'; -import '../../unit/mock-editor.js'; -import { MockEditor } from '../../unit/mock-editor.js'; +import 'open-scd/test/unit/mock-editor.js'; +import { MockEditor } from 'open-scd/test/unit/mock-editor.js'; import '../../../src/compas-editors/autogen-substation.js'; import CompasAutogenerateSubstation from '../../../src/compas-editors/autogen-substation.js'; diff --git a/packages/compas-open-scd/test/mock-editor-logger.ts b/packages/compas-open-scd/test/mock-editor-logger.ts deleted file mode 100644 index 63f1e9aae..000000000 --- a/packages/compas-open-scd/test/mock-editor-logger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { LitElement, customElement } from 'lit-element'; - -import { Editing } from '../src/Editing.js'; -import { Historing } from '../src/Historing.js'; - -@customElement('mock-editor-logger') -export class MockEditorLogger extends Editing(Historing(LitElement)) {} diff --git a/packages/compas-open-scd/test/mock-setter-logger.ts b/packages/compas-open-scd/test/mock-setter-logger.ts deleted file mode 100644 index a7bfc2231..000000000 --- a/packages/compas-open-scd/test/mock-setter-logger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { LitElement, customElement } from 'lit-element'; -import { Setting } from '../src/Setting.js'; -import { Editing } from '../src/Editing.js'; -import { Historing } from '../src/Historing.js'; - -@customElement('mock-setter-logger') -export class MockSetterLogger extends Setting(Editing(Historing(LitElement))) {} diff --git a/packages/compas-open-scd/test/unit/CompasHistoring.test.ts b/packages/compas-open-scd/test/unit/CompasHistoring.test.ts new file mode 100644 index 000000000..b3a2c7e2a --- /dev/null +++ b/packages/compas-open-scd/test/unit/CompasHistoring.test.ts @@ -0,0 +1,51 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import 'open-scd/test/unit/mock-logger.js'; +import { MockLogger } from 'open-scd/test/unit/mock-logger.js'; + +import { newIssueEvent } from 'open-scd/src/foundation.js'; + +describe('HistoringElement', () => { + let element: MockLogger; + beforeEach(async () => { + element = await fixture(html``); + }); + + describe('with a CoMPAS issue coming in - CoMPAS validator', () => { + let substation: Element; + beforeEach(async () => { + const doc = await fetch('/test/testfiles/valid2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + substation = doc.querySelector('Substation')!; + + element.dispatchEvent( + newIssueEvent({ + validatorId: '/src/validators/CompasValidateSchema.js', + title: 'CoMPAS Run', + element: substation, + }) + ); + }); + + it('in parallel saves the issues of the CoMPAS validator', () => { + expect(element.diagnoses.get('/src/validators/CompasValidateSchema.js')) + .to.exist; + expect( + element.diagnoses.get('/src/validators/CompasValidateSchema.js')!.length + ).to.equal(1); + const issue = element.diagnoses.get( + '/src/validators/CompasValidateSchema.js' + )![0]; + expect(issue.title).to.equal('CoMPAS Run'); + expect(issue.element).to.equal(substation); + }); + + it('diagnostic dialog looks like the latest snapshot', async () => { + await element.issueUI.querySelector('mwc-button')!.click(); + await element.updateComplete; + + await expect(element.diagnosticUI).to.equalSnapshot(); + }); + }); +}); diff --git a/packages/compas-open-scd/test/unit/Plugging.test.ts b/packages/compas-open-scd/test/unit/CompasPlugging.test.ts similarity index 95% rename from packages/compas-open-scd/test/unit/Plugging.test.ts rename to packages/compas-open-scd/test/unit/CompasPlugging.test.ts index 3bdc035b5..9ad63c492 100644 --- a/packages/compas-open-scd/test/unit/Plugging.test.ts +++ b/packages/compas-open-scd/test/unit/CompasPlugging.test.ts @@ -1,12 +1,12 @@ import { expect, fixture, html } from '@open-wc/testing'; -import './mock-plugger.js'; -import { MockPlugger } from './mock-plugger.js'; +import './mock-compas-plugger.js'; +import { MockCompasPlugger } from './mock-compas-plugger.js'; import { TextField } from '@material/mwc-textfield'; describe('PluggingElement', () => { - let element: MockPlugger; + let element: MockCompasPlugger; let doc: XMLDocument; afterEach(async () => { @@ -17,9 +17,12 @@ describe('PluggingElement', () => { doc = await fetch('/test/testfiles/valid2007B4.scd') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element = ( + element = ( await fixture( - html`` + html`` ) ); await element.updateComplete; diff --git a/packages/compas-open-scd/test/unit/Editing.test.ts b/packages/compas-open-scd/test/unit/Editing.test.ts deleted file mode 100644 index 148f56c3e..000000000 --- a/packages/compas-open-scd/test/unit/Editing.test.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { html, fixture, expect } from '@open-wc/testing'; -import { SinonSpy, spy } from 'sinon'; - -import './mock-editor.js'; -import { MockEditor } from './mock-editor.js'; - -import { createUpdateAction, newActionEvent } from 'open-scd/src/foundation.js'; - -describe('EditingElement', () => { - let elm: MockEditor; - let doc: XMLDocument; - let parent: Element; - let element: Element; - let reference: Node | null; - - let validateEvent: SinonSpy; - - beforeEach(async () => { - doc = await fetch('/test/testfiles/Editing.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - elm = ( - await fixture(html``) - ); - - parent = elm.doc!.querySelector('VoltageLevel[name="E1"]')!; - element = parent.querySelector('Bay[name="Q01"]')!; - reference = element.nextSibling; - - validateEvent = spy(); - window.addEventListener('validate', validateEvent); - }); - - it('creates an element on receiving a Create Action', () => { - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: elm.doc!.createElement('newBay'), - reference: null, - }, - }) - ); - expect(elm.doc!.querySelector('newBay')).to.not.be.null; - }); - - it('creates an Node on receiving a Create Action', () => { - const testNode = document.createTextNode('myTestNode'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: testNode, - }, - }) - ); - expect(parent.lastChild).to.equal(testNode); - }); - - it('creates the Node based on the reference definition', () => { - const testNode = document.createTextNode('myTestNode'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: testNode, - reference: parent.firstChild, - }, - }) - ); - expect(parent.firstChild).to.equal(testNode); - }); - - it('triggers getReference with missing reference on Create Action', () => { - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: elm.doc!.createElement('Bay'), - }, - }) - ); - expect(parent.querySelector('Bay')?.nextElementSibling).to.equal( - parent.querySelector('Bay[name="Q01"]') - ); - }); - - it('ignores getReference with existing reference on Create Action', () => { - const newElement = elm.doc!.createElement('Bay'); - newElement?.setAttribute('name', 'Q03'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: newElement, - reference: parent.querySelector('Bay[name="Q02"]'), - }, - }) - ); - expect( - parent.querySelector('Bay[name="Q03"]')?.nextElementSibling - ).to.equal(parent.querySelector('Bay[name="Q02"]')); - }); - - it('does not creates an element on name attribute conflict', () => { - const newElement = elm.doc!.createElement('Bay'); - newElement?.setAttribute('name', 'Q01'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: newElement, - reference: null, - }, - }) - ); - expect(parent.querySelectorAll('Bay[name="Q01"]').length).to.be.equal(1); - }); - - it('does not creates an element on id attribute conflict', () => { - const newElement = elm.doc!.createElement('DOType'); - newElement?.setAttribute('id', 'testId'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent: doc.querySelector('DataTypeTemplates')!, - element: newElement, - reference: null, - }, - }) - ); - expect(doc.querySelector('DOType')).to.be.null; - }); - - it('deletes an element on receiving a Delete action', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element, - reference, - }, - }) - ); - expect(elm.doc!.querySelector('VoltageLevel[name="E1"] > Bay[name="Q01"]')) - .to.be.null; - }); - - it('deletes a Node on receiving a Delete action', () => { - const testNode = document.createTextNode('myTestNode'); - parent.appendChild(testNode); - expect(testNode.parentNode).to.be.equal(parent); - - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element: testNode, - }, - }) - ); - - expect(parent.lastChild).to.not.equal(testNode); - expect(testNode.parentNode).to.be.null; - }); - - it('correctly handles incorrect delete action definition', () => { - const testNode = document.createTextNode('myTestNode'); - expect(testNode.parentNode).to.null; - - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element: testNode, - }, - }) - ); - - expect(parent.lastChild).to.not.equal(testNode); - expect(testNode.parentNode).to.null; - }); - - it('replaces an element on receiving an Replace action', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - element, - }, - new: { - element: elm.doc!.createElement('newBay'), - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(parent.querySelector('newBay')).to.not.be.null; - expect(parent.querySelector('newBay')?.nextElementSibling).to.equal( - parent.querySelector('Bay[name="Q02"]') - ); - }); - - it('does not replace an element in case of name conflict', () => { - const newElement = elm.doc!.createElement('Bay'); - newElement?.setAttribute('name', 'Q02'); - - elm.dispatchEvent( - newActionEvent({ - old: { - element, - }, - new: { - element: newElement, - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.not.be.null; - expect( - parent.querySelector('Bay[name="Q01"]')?.nextElementSibling - ).to.equal(parent.querySelector('Bay[name="Q02"]')); - }); - - it('replaces id defined element on receiving Replace action', () => { - expect(doc.querySelector('LNodeType[id="testId"]')).to.not.be.null; - - const newElement = doc.createElement('LNodeType'); - newElement?.setAttribute('id', 'testId3'); - - elm.dispatchEvent( - newActionEvent({ - old: { - element: doc.querySelector('LNodeType[id="testId"]')!, - }, - new: { - element: newElement, - }, - }) - ); - expect(elm.doc!.querySelector('LNodeType[id="testId"]')).to.be.null; - expect(elm.doc!.querySelector('LNodeType[id="testId3"]')).to.not.be.null; - }); - - it('does not replace an element in case of id conflict', () => { - expect(doc.querySelector('LNodeType[id="testId"]')).to.not.be.null; - - const newElement = elm.doc!.createElement('LNodeType'); - newElement?.setAttribute('id', 'testId1'); - - elm.dispatchEvent( - newActionEvent({ - old: { - element: doc.querySelector('LNodeType[id="testId"]')!, - }, - new: { - element: newElement, - }, - }) - ); - expect(elm.doc!.querySelector('LNodeType[id="testId"]')).to.not.be.null; - expect(elm.doc!.querySelector('LNodeType[id="testId1"]')).to.be.null; - }); - - it('moves an element on receiving a Move action', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - reference: null, - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]')) - .to.not.be.null; - }); - - it('triggers getReference with missing reference on Move action', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]')) - .to.not.be.null; - expect( - elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]') - ?.nextElementSibling - ).to.equal(elm.doc!.querySelector('VoltageLevel[name="J1"] > Function')); - }); - - it('does not move an element in case of name conflict', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - reference: null, - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]')) - .to.not.be.null; - expect( - elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]') - ?.nextElementSibling - ).to.be.null; - }); - - it('updates an element on receiving an Update action', () => { - const newAttributes: Record = {}; - newAttributes['name'] = 'Q03'; - - elm.dispatchEvent( - newActionEvent(createUpdateAction(element, newAttributes)) - ); - - expect(element.parentElement).to.equal(parent); - expect(element).to.have.attribute('name', 'Q03'); - expect(element).to.not.have.attribute('desc'); - }); - - it('allows empty string as attribute value', () => { - const newAttributes: Record = {}; - newAttributes['name'] = ''; - - elm.dispatchEvent( - newActionEvent(createUpdateAction(element, newAttributes)) - ); - - expect(element.parentElement).to.equal(parent); - expect(element).to.have.attribute('name', ''); - expect(element).to.not.have.attribute('desc'); - }); - - it('does not update an element in case of name conflict', () => { - const newAttributes: Record = {}; - newAttributes['name'] = 'Q02'; - - elm.dispatchEvent( - newActionEvent(createUpdateAction(element, newAttributes)) - ); - - expect(element.parentElement).to.equal(parent); - expect(element).to.have.attribute('name', 'Q01'); - expect(element).to.have.attribute('desc', 'Bay'); - }); - - it('does not update an element in case of id conflict', () => { - const newAttributes: Record = {}; - newAttributes['id'] = 'testId1'; - - elm.dispatchEvent( - newActionEvent( - createUpdateAction(doc.querySelector('LNodeType')!, newAttributes) - ) - ); - - expect(elm.doc!.querySelector('LNodeType[id="testId"]')).to.exist; - expect(elm.doc!.querySelector('LNodeType[id="testId1"]')).to.not.exist; - }); - - it('carries out subactions sequentially on receiving a ComplexAction', () => { - const child3 = elm.doc!.createElement('newBay'); - elm.dispatchEvent( - newActionEvent({ - title: 'Test complex action', - actions: [ - { - old: { element }, - new: { element: child3 }, - }, - { - old: { - parent, - element: child3, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - reference: null, - }, - }, - ], - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(elm.doc!.querySelector('VoltageLevel[name="J1"] > newBay')).to.not.be - .null; - }); - - it('triggers a validation event on receiving a ComplexAction', async () => { - const child3 = elm.doc!.createElement('newBay'); - elm.dispatchEvent( - newActionEvent({ - title: 'Test complex action', - actions: [ - { - old: { element }, - new: { element: child3 }, - }, - { - old: { - parent, - element: child3, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - reference: null, - }, - }, - ], - }) - ); - await elm.updateComplete; - - expect(validateEvent).to.be.calledOnce; - }); - - it('does not exchange doc with empty complex action', async () => { - elm.dispatchEvent( - newActionEvent({ - title: 'Test complex action', - actions: [], - }) - ); - await elm.updateComplete; - - expect(doc).to.equal(elm.doc); - }); - - it('does not trigger validation with empty complex action', async () => { - elm.dispatchEvent( - newActionEvent({ - title: 'Test complex action', - actions: [], - }) - ); - await elm.updateComplete; - - expect(validateEvent).to.not.been.called; - }); -}); diff --git a/packages/compas-open-scd/test/unit/Historing.test.ts b/packages/compas-open-scd/test/unit/Historing.test.ts deleted file mode 100644 index e8d44229a..000000000 --- a/packages/compas-open-scd/test/unit/Historing.test.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { expect, fixture, html } from '@open-wc/testing'; - -import './mock-logger.js'; -import { MockAction } from './mock-actions.js'; -import { MockLogger } from './mock-logger.js'; - -import { - CommitEntry, - newIssueEvent, - newLogEvent, -} from 'open-scd/src/foundation.js'; - -describe('HistoringElement', () => { - let element: MockLogger; - beforeEach(async () => { - element = await fixture(html``); - }); - - it('starts out with an empty log', () => - expect(element).property('log').to.be.empty); - - it('cannot undo', () => expect(element).property('canUndo').to.be.false); - it('cannot redo', () => expect(element).property('canRedo').to.be.false); - - it('cannot undo info messages', () => { - element.dispatchEvent(newLogEvent({ kind: 'info', title: 'test info' })); - expect(element).property('log').to.have.lengthOf(1); - expect(element).property('canUndo').to.be.false; - }); - - it('cannot undo warning messages', () => { - element.dispatchEvent( - newLogEvent({ kind: 'warning', title: 'test warning' }) - ); - expect(element).property('log').to.have.lengthOf(1); - expect(element).property('canUndo').to.be.false; - }); - - it('cannot undo error messages', () => { - element.dispatchEvent(newLogEvent({ kind: 'error', title: 'test error' })); - expect(element).property('log').to.have.lengthOf(1); - expect(element).property('canUndo').to.be.false; - }); - - it('has no previous action', () => - expect(element).to.have.property('previousAction', -1)); - it('has no edit count', () => - expect(element).to.have.property('editCount', -1)); - it('has no next action', () => - expect(element).to.have.property('nextAction', -1)); - - it('renders a placeholder message', () => - expect(element.logUI).to.contain('mwc-list-item[disabled]')); - // dirty hack: ask @open-wc/shadowDomDiff for contains support - - it('shows a snackbar on logging an info', () => { - expect(element.infoUI).to.have.property('open', false); - element.dispatchEvent(newLogEvent({ kind: 'info', title: 'test info' })); - expect(element.infoUI).to.have.property('open', true); - }); - - it('shows a snackbar on logging an warning', () => { - expect(element.warningUI).to.have.property('open', false); - element.dispatchEvent( - newLogEvent({ kind: 'warning', title: 'test warning' }) - ); - expect(element.warningUI).to.have.property('open', true); - }); - - it('shows a snackbar on logging an error', () => { - expect(element.errorUI).to.have.property('open', false); - element.dispatchEvent(newLogEvent({ kind: 'error', title: 'test error' })); - expect(element.errorUI).to.have.property('open', true); - }); - - it('shows a snackbar on an issue', () => { - expect(element.issueUI).to.have.property('open', false); - element.dispatchEvent( - newIssueEvent({ - validatorId: 'val', - title: 'test issue', - }) - ); - expect(element.issueUI).to.have.property('open', true); - }); - - it('opens the log dialog on snackbar "Show" button click', async () => { - expect(element.logUI).to.have.property('open', false); - await element.errorUI.querySelector('mwc-button')!.click(); - await element.updateComplete; - expect(element.logUI).to.have.property('open', true); - }); - - it('opens the diagnostics dialog on issue snackbar "Show" button click', async () => { - expect(element.diagnosticUI).to.have.property('open', false); - await element.issueUI.querySelector('mwc-button')!.click(); - await element.updateComplete; - expect(element.diagnosticUI).to.have.property('open', true); - }); - - describe('with an action logged', () => { - beforeEach(async () => { - element.dispatchEvent( - newLogEvent({ - kind: 'action', - title: 'test MockAction', - action: MockAction.cre, - }) - ); - element.requestUpdate(); - await element.updateComplete; - element.requestUpdate(); - await element.updateComplete; - }); - - it('can undo', () => expect(element).property('canUndo').to.be.true); - it('cannot redo', () => expect(element).property('canRedo').to.be.false); - - it('has no previous action', () => - expect(element).to.have.property('previousAction', -1)); - it('has an edit count', () => - expect(element).to.have.property('editCount', 0)); - it('has no next action', () => - expect(element).to.have.property('nextAction', -1)); - - it('does not log derived actions', () => { - expect(element).property('history').to.have.lengthOf(1); - element.dispatchEvent( - newLogEvent({ - kind: 'action', - title: 'test MockAction', - action: (element.history[0]).action, - }) - ); - expect(element).property('history').to.have.lengthOf(1); - }); - - it('can reset its log', () => { - element.dispatchEvent(newLogEvent({ kind: 'reset' })); - expect(element).property('log').to.be.empty; - expect(element).property('history').to.be.empty; - expect(element).to.have.property('editCount', -1); - }); - - it('renders a history message for the action', () => - expect(element.historyUI).to.contain.text('test')); - - describe('with a second action logged', () => { - beforeEach(() => { - element.dispatchEvent( - newLogEvent({ - kind: 'info', - title: 'test info', - }) - ); - element.dispatchEvent( - newLogEvent({ - kind: 'action', - title: 'test MockAction', - action: MockAction.del, - }) - ); - }); - - it('has a previous action', () => - expect(element).to.have.property('previousAction', 0)); - it('has an edit count', () => - expect(element).to.have.property('editCount', 1)); - it('has no next action', () => - expect(element).to.have.property('nextAction', -1)); - - describe('with an action undone', () => { - beforeEach(() => element.undo()); - - it('has no previous action', () => - expect(element).to.have.property('previousAction', -1)); - it('has an edit count', () => - expect(element).to.have.property('editCount', 0)); - it('has a next action', () => - expect(element).to.have.property('nextAction', 1)); - - it('can redo', () => expect(element).property('canRedo').to.be.true); - - it('removes the undone action when a new action is logged', () => { - element.dispatchEvent( - newLogEvent({ - kind: 'action', - title: 'test MockAction', - action: MockAction.mov, - }) - ); - expect(element).property('log').to.have.lengthOf(1); - expect(element).property('history').to.have.lengthOf(2); - expect(element).to.have.property('editCount', 1); - expect(element).to.have.property('nextAction', -1); - }); - - describe('with the second action undone', () => { - beforeEach(() => element.undo()); - - it('cannot undo any funther', () => - expect(element.undo()).to.be.false); - }); - - describe('with the action redone', () => { - beforeEach(() => element.redo()); - - it('has a previous action', () => - expect(element).to.have.property('previousAction', 0)); - it('has an edit count', () => - expect(element).to.have.property('editCount', 1)); - it('has no next action', () => - expect(element).to.have.property('nextAction', -1)); - - it('cannot redo any further', () => - expect(element.redo()).to.be.false); - }); - }); - }); - }); - - describe('with an issue incoming', () => { - beforeEach(async () => { - element.dispatchEvent( - newIssueEvent({ - validatorId: '/src/validators/ValidateSchema.js', - title: 'test run 1', - }) - ); - element.requestUpdate(); - await element.updateComplete; - element.requestUpdate(); - await element.updateComplete; - }); - - it('saves the issue to diagnose', () => { - expect(element.diagnoses.get('/src/validators/ValidateSchema.js')).to - .exist; - const issue = element.diagnoses.get( - '/src/validators/ValidateSchema.js' - )![0]; - expect(issue.title).to.equal('test run 1'); - }); - - it('does not contain issues from another validator', () => - expect(element.diagnoses.has('/src/validators/ValidateTemplates.js')).to - .be.false); - - describe('with another issue coming in - new validator', () => { - beforeEach(() => { - element.dispatchEvent( - newIssueEvent({ - validatorId: '/src/validators/ValidateTemplates.js', - title: 'test run 3', - }) - ); - }); - - it('keeps old issues from the other validator', () => { - expect(element.diagnoses.get('/src/validators/ValidateSchema.js')).to - .exist; - expect( - element.diagnoses.get('/src/validators/ValidateSchema.js')!.length - ).to.equal(1); - const issue = element.diagnoses.get( - '/src/validators/ValidateSchema.js' - )![0]; - expect(issue.title).to.equal('test run 1'); - }); - - it('in parallel saves the issues of the new validator', () => { - expect(element.diagnoses.get('/src/validators/ValidateTemplates.js')).to - .exist; - expect( - element.diagnoses.get('/src/validators/ValidateTemplates.js')!.length - ).to.equal(1); - const issue = element.diagnoses.get( - '/src/validators/ValidateTemplates.js' - )![0]; - expect(issue.title).to.equal('test run 3'); - }); - }); - - describe('with a CoMPAS issue coming in - CoMPAS validator', () => { - let substation: Element; - beforeEach(async () => { - const doc = await fetch('/test/testfiles/valid2007B4.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - substation = doc.querySelector('Substation')!; - - element.dispatchEvent( - newIssueEvent({ - validatorId: '/src/validators/CompasValidateSchema.js', - title: 'CoMPAS Run', - element: substation - }) - ); - }); - - it('in parallel saves the issues of the CoMPAS validator', () => { - expect(element.diagnoses.get('/src/validators/CompasValidateSchema.js')).to - .exist; - expect( - element.diagnoses.get('/src/validators/CompasValidateSchema.js')!.length - ).to.equal(1); - const issue = element.diagnoses.get( - '/src/validators/CompasValidateSchema.js' - )![0]; - expect(issue.title).to.equal('CoMPAS Run'); - expect(issue.element).to.equal(substation); - }); - - it('diagnostic dialog looks like the latest snapshot', async () => { - await element.issueUI.querySelector('mwc-button')!.click(); - await element.updateComplete; - - await expect(element.diagnosticUI).to.equalSnapshot(); - }); - }); - }); -}); \ No newline at end of file diff --git a/packages/compas-open-scd/test/unit/Setting.test.ts b/packages/compas-open-scd/test/unit/Setting.test.ts deleted file mode 100644 index 5b6cfa2ed..000000000 --- a/packages/compas-open-scd/test/unit/Setting.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { registerTranslateConfig, use } from 'lit-translate'; - -import { html, fixture, expect } from '@open-wc/testing'; - -import './mock-setter.js'; -import { MockSetter } from './mock-setter.js'; - -import { Button } from '@material/mwc-button'; -import { defaults } from '../../src/Setting.js'; - -describe('SettingElement', () => { - let element: MockSetter; - beforeEach(async () => { - localStorage.clear(); - element = await fixture(html``); - }); - - it('initially has default settings', () => - expect(element).to.have.deep.property('settings', defaults)); - - it('stores settings to localStorage', () => { - element.setSetting('theme', 'dark'); - expect(localStorage.getItem('theme')).to.equal('dark'); - }); - - it('retrieves settings from localStorage', () => { - localStorage.setItem('language', 'de'); - expect(element.settings).to.have.property('language', 'de'); - }); - - it('saves chosen settings on save button click', async () => { - element.settingsUI.show(); - element.darkThemeUI.checked = true; - await element.darkThemeUI.updateComplete; - await (