From db371f24552eb8f6936cd1958a5d2c999fd33c50 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 30 Oct 2025 11:08:23 +0100 Subject: [PATCH 01/14] feat: Add xml editor --- package-lock.json | 17 ++++++-- packages/core/api/editor/xml-editor.ts | 59 ++++++++++++++++++++++++++ packages/core/foundation.ts | 2 + packages/core/package.json | 3 +- packages/openscd/src/addons/Editor.ts | 13 ++++-- packages/openscd/src/addons/History.ts | 42 +++++++----------- packages/openscd/src/open-scd.ts | 7 ++- 7 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 packages/core/api/editor/xml-editor.ts diff --git a/package-lock.json b/package-lock.json index fc6a5a5fc1..28d4e9092a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7101,6 +7101,15 @@ "resolved": "packages/openscd", "link": true }, + "node_modules/@openscd/oscd-api": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@openscd/oscd-api/-/oscd-api-0.1.5.tgz", + "integrity": "sha512-SPm79bIqhSSxYMfnHIIwfuNpQa+UyJPY4Gxl6MVjHLY1jOkhevL+k/FfI4g08v3RdMG/+2MYu6ZxpB4zuQU5ZQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.1" + } + }, "node_modules/@openscd/plugins": { "resolved": "packages/plugins", "link": true @@ -27399,9 +27408,10 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/tsscmp": { "version": "1.0.6", @@ -31154,6 +31164,7 @@ "dependencies": { "@lit/localize": "^0.11.4", "@open-wc/lit-helpers": "^0.5.1", + "@openscd/oscd-api": "^0.1.5", "lit": "^2.2.7" }, "devDependencies": { diff --git a/packages/core/api/editor/xml-editor.ts b/packages/core/api/editor/xml-editor.ts new file mode 100644 index 0000000000..d873355779 --- /dev/null +++ b/packages/core/api/editor/xml-editor.ts @@ -0,0 +1,59 @@ +import { Transactor, TransactedCallback, Commit, CommitOptions } from '@openscd/oscd-api/dist/Transactor.js'; +import { EditV2 } from '@openscd/oscd-api/dist/editv2.js'; + +import { handleEditV2 } from '../../foundation.js'; + + +export class XMLEditor implements Transactor { + public past: Commit[] = []; + public future: Commit[] = []; + + private subscribers: TransactedCallback[] = []; + + commit(change: EditV2, { title, squash }: CommitOptions = {}): Commit { + const commit: Commit = + squash && this.past.length + ? this.past[this.past.length - 1] + : { undo: [], redo: [] }; + const undo = handleEditV2(change as any); + // typed as per https://github.com/microsoft/TypeScript/issues/49280#issuecomment-1144181818 recommendation: + commit.undo.unshift(...[undo].flat(Infinity as 1)); + commit.redo.push(...[change].flat(Infinity as 1)); + if (title) commit.title = title; + if (squash && this.past.length) this.past.pop(); + this.past.push(commit); + this.future = []; + this.subscribers.forEach((subscriber) => subscriber(commit)); + return commit; + }; + + undo(): Commit | undefined { + const commit = this.past.pop(); + if (!commit) return; + handleEditV2(commit.undo as any); + this.future.unshift(commit); + return commit; + }; + + redo(): Commit | undefined { + const commit = this.future.shift(); + if (!commit) return; + handleEditV2(commit.redo as any); + this.past.push(commit); + return commit; + }; + + subscribe(txCallback: TransactedCallback): () => TransactedCallback { + this.subscribers.push(txCallback); + + return () => { + const indexToRemove = this.subscribers.findIndex(t => t === txCallback); + + if (indexToRemove > -1) { + this.subscribers.splice(indexToRemove, 1); + } + + return txCallback; + }; + }; +} diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index a408b03b73..a7d30aa52d 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -68,3 +68,5 @@ export function crossProduct(...arrays: T[][]): T[][] { } export { OscdApi } from './api/api.js'; + +export { XMLEditor } from './api/editor/xml-editor.js'; diff --git a/packages/core/package.json b/packages/core/package.json index d2606d6d96..63c42b8d3f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -38,7 +38,8 @@ "dependencies": { "@lit/localize": "^0.11.4", "@open-wc/lit-helpers": "^0.5.1", - "lit": "^2.2.7" + "lit": "^2.2.7", + "@openscd/oscd-api": "^0.1.5" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.3", diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 70a07fca52..25a186fc58 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -10,7 +10,8 @@ import { isSetAttributesV2, isSetTextContentV2, isComplexV2, - newEditEventV2 + newEditEventV2, + XMLEditor } from '@openscd/core'; import { property, @@ -56,6 +57,8 @@ export class OscdEditor extends LitElement { @property({ type: String }) docName = ''; /** The UUID of the current [[`doc`]] */ @property({ type: String }) docId = ''; + /** XML Editor to apply changes to the scd */ + @property({ type: Object }) editor!: XMLEditor; @property({ type: Object, @@ -151,15 +154,18 @@ export class OscdEditor extends LitElement { } async handleEditEventV2(event: EditEventV2) { - const edit = event.detail.edit; + const { edit, title, squash } = event.detail; - const undoEdit = handleEditV2(edit); + // const undoEdit = handleEditV2(edit); + this.editor.commit(edit, { title, squash }); const shouldCreateHistoryEntry = event.detail.createHistoryEntry !== false; if (shouldCreateHistoryEntry) { const { title, message } = this.getLogText(edit); + // TODO + /* this.dispatchEvent(newLogEvent({ kind: 'action', title: event.detail.title ?? title, @@ -168,6 +174,7 @@ export class OscdEditor extends LitElement { undo: undoEdit, squash: event.detail.squash })); + */ } await this.updateComplete; diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index 2b330facdf..95fefb3dd3 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -38,7 +38,7 @@ import { import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../plugin.js'; -import { EditV2, isComplexV2, newEditEventV2 } from '@openscd/core'; +import { EditV2, isComplexV2, newEditEventV2, XMLEditor } from '@openscd/core'; export const historyStateEvent = 'history-state'; export interface HistoryState { @@ -149,6 +149,9 @@ export class OscdHistory extends LitElement { @property({ type: Array }) log: InfoEntry[] = []; + /** XML Editor to apply changes to the scd */ + @property({ type: Object }) editor!: XMLEditor; + /** All [[`CommitEntry`]]s received so far through [[`LogEvent`]]s */ @property({ type: Array }) history: CommitEntry[] = []; @@ -177,19 +180,13 @@ export class OscdHistory extends LitElement { @query('#issue') issueUI!: Snackbar; get canUndo(): boolean { - return this.editCount >= 0; + console.log(`Can undo: ${this.editor.past.length > 0}`) + return this.editor.past.length > 0; } get canRedo(): boolean { - return this.nextAction >= 0; + return this.editor.future.length > 0; } - get previousAction(): number { - if (!this.canUndo) return -1; - return this.history - .slice(0, this.editCount) - .map(entry => (entry.kind == 'action' ? true : false)) - .lastIndexOf(true); - } get nextAction(): number { let index = this.history .slice(this.editCount + 1) @@ -209,23 +206,11 @@ export class OscdHistory extends LitElement { this.issueUI.show(); } - undo(): boolean { - if (!this.canUndo) return false; - - const undoEdit = (this.history[this.editCount]).undo; - this.host.dispatchEvent(newEditEventV2(undoEdit, { createHistoryEntry: false })); - this.setEditCount(this.previousAction); - - return true; + undo(): void { + this.editor.undo(); } - redo(): boolean { - if (!this.canRedo) return false; - - const redoEdit = (this.history[this.nextAction]).redo; - this.host.dispatchEvent(newEditEventV2(redoEdit, { createHistoryEntry: false })); - this.setEditCount(this.nextAction); - - return true; + redo(): void { + this.editor.redo(); } private onHistory(detail: CommitDetail) { @@ -395,6 +380,11 @@ export class OscdHistory extends LitElement { connectedCallback(): void { super.connectedCallback(); + this.editor.subscribe(e => { + console.log(e) + this.requestUpdate('history', []); + }); + this.host.addEventListener('log', this.onLog); this.host.addEventListener('issue', this.onIssue); this.host.addEventListener('history-dialog-ui', this.historyUIHandler); diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index 56d29590bd..3760126c11 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -45,7 +45,7 @@ import type { Plugin as CorePlugin, EditCompletedEvent, } from '@openscd/core'; -import { OscdApi } from '@openscd/core'; +import { OscdApi, XMLEditor } from '@openscd/core'; import { HistoryState, historyStateEvent } from './addons/History.js'; @@ -65,13 +65,14 @@ export class OpenSCD extends LitElement { return html` - + Date: Thu, 30 Oct 2025 13:30:12 +0100 Subject: [PATCH 02/14] feat: Add undo and redo subscribe --- package-lock.json | 1 + packages/core/api/editor/subject.ts | 26 +++ packages/core/api/editor/xml-editor.ts | 35 ++-- .../core/foundation/deprecated/history.ts | 3 - packages/openscd/package.json | 1 + packages/openscd/src/addons/History.ts | 160 +++++------------- .../src/addons/history/get-log-text.ts | 31 ++++ 7 files changed, 128 insertions(+), 129 deletions(-) create mode 100644 packages/core/api/editor/subject.ts create mode 100644 packages/openscd/src/addons/history/get-log-text.ts diff --git a/package-lock.json b/package-lock.json index 28d4e9092a..b163c6a4ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32671,6 +32671,7 @@ "@material/mwc-textfield": "0.22.1", "@material/mwc-top-app-bar-fixed": "0.22.1", "@openscd/core": "*", + "@openscd/oscd-api": "^0.1.5", "@openscd/xml": "*", "ace-custom-element": "^1.6.5", "lit": "^2.2.7", diff --git a/packages/core/api/editor/subject.ts b/packages/core/api/editor/subject.ts new file mode 100644 index 0000000000..9719bb1dbe --- /dev/null +++ b/packages/core/api/editor/subject.ts @@ -0,0 +1,26 @@ +export type Subscriber = (value: T) => void; +export type Unsubscriber = () => Subscriber; + +export class Subject { + private subscribers: Subscriber[] = []; + + public next(value: T): void { + this.subscribers.forEach(s => s(value)); + } + + public subscribe(subscriber: Subscriber): Unsubscriber { + this.subscribers.push(subscriber); + + return () => { + this.unsubscribe(subscriber); + return subscriber; + } + } + + public unsubscribe(subscriber: Subscriber): void { + const indexToRemove = this.subscribers.findIndex(s => s === subscriber); + if (indexToRemove > -1) { + this.subscribers.splice(indexToRemove, 1); + } + } +} diff --git a/packages/core/api/editor/xml-editor.ts b/packages/core/api/editor/xml-editor.ts index d873355779..5c2417ecf0 100644 --- a/packages/core/api/editor/xml-editor.ts +++ b/packages/core/api/editor/xml-editor.ts @@ -1,6 +1,7 @@ import { Transactor, TransactedCallback, Commit, CommitOptions } from '@openscd/oscd-api/dist/Transactor.js'; import { EditV2 } from '@openscd/oscd-api/dist/editv2.js'; +import { Subject } from './subject.js'; import { handleEditV2 } from '../../foundation.js'; @@ -8,13 +9,16 @@ export class XMLEditor implements Transactor { public past: Commit[] = []; public future: Commit[] = []; - private subscribers: TransactedCallback[] = []; + private commitSubject = new Subject>(); + private undoSubject = new Subject>(); + private redoSubject = new Subject>(); commit(change: EditV2, { title, squash }: CommitOptions = {}): Commit { const commit: Commit = squash && this.past.length ? this.past[this.past.length - 1] : { undo: [], redo: [] }; + // TODO: Fix type once issue is fixed https://github.com/openscd/oscd-api/issues/57 const undo = handleEditV2(change as any); // typed as per https://github.com/microsoft/TypeScript/issues/49280#issuecomment-1144181818 recommendation: commit.undo.unshift(...[undo].flat(Infinity as 1)); @@ -23,13 +27,14 @@ export class XMLEditor implements Transactor { if (squash && this.past.length) this.past.pop(); this.past.push(commit); this.future = []; - this.subscribers.forEach((subscriber) => subscriber(commit)); + this.commitSubject.next(commit); return commit; }; undo(): Commit | undefined { const commit = this.past.pop(); if (!commit) return; + // TODO: Fix type once issue is fixed https://github.com/openscd/oscd-api/issues/57 handleEditV2(commit.undo as any); this.future.unshift(commit); return commit; @@ -38,22 +43,32 @@ export class XMLEditor implements Transactor { redo(): Commit | undefined { const commit = this.future.shift(); if (!commit) return; + // TODO: Fix type once issue is fixed https://github.com/openscd/oscd-api/issues/57 handleEditV2(commit.redo as any); this.past.push(commit); return commit; }; subscribe(txCallback: TransactedCallback): () => TransactedCallback { - this.subscribers.push(txCallback); + return this.commitSubject.subscribe(txCallback); + }; - return () => { - const indexToRemove = this.subscribers.findIndex(t => t === txCallback); + subscribeUndo(txCallback: TransactedCallback): () => TransactedCallback { + return this.undoSubject.subscribe(txCallback); + } + + subscribeRedo(txCallback: TransactedCallback): () => TransactedCallback { + return this.redoSubject.subscribe(txCallback); + } - if (indexToRemove > -1) { - this.subscribers.splice(indexToRemove, 1); - } + subscribeUndoRedo(txCallback: TransactedCallback): () => TransactedCallback { + const unsubscribeUndo = this.subscribeUndo(txCallback); + const unsubscribeRedo = this.subscribeRedo(txCallback); + return () => { + unsubscribeUndo(); + unsubscribeRedo(); return txCallback; - }; - }; + } + } } diff --git a/packages/core/foundation/deprecated/history.ts b/packages/core/foundation/deprecated/history.ts index b78f919432..39f462c34e 100644 --- a/packages/core/foundation/deprecated/history.ts +++ b/packages/core/foundation/deprecated/history.ts @@ -12,9 +12,6 @@ export interface LogDetailBase { /** The [[`LogEntry`]] for a committed [[`EditorAction`]]. */ export interface CommitDetail extends LogDetailBase { kind: 'action'; - redo: EditV2; - undo: EditV2; - squash?: boolean; } /** A [[`LogEntry`]] for notifying the user. */ export interface InfoDetail extends LogDetailBase { diff --git a/packages/openscd/package.json b/packages/openscd/package.json index 4b64689d4a..054ac4ab42 100644 --- a/packages/openscd/package.json +++ b/packages/openscd/package.json @@ -39,6 +39,7 @@ "@material/mwc-textarea": "0.22.1", "@material/mwc-textfield": "0.22.1", "@material/mwc-top-app-bar-fixed": "0.22.1", + "@openscd/oscd-api": "^0.1.5", "@openscd/core": "*", "@openscd/xml": "*", "ace-custom-element": "^1.6.5", diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index 95fefb3dd3..21f113b25d 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -40,6 +40,8 @@ import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../plugin.js'; import { EditV2, isComplexV2, newEditEventV2, XMLEditor } from '@openscd/core'; +import { getLogText } from './history/get-log-text.js'; + export const historyStateEvent = 'history-state'; export interface HistoryState { editCount: number; @@ -48,8 +50,11 @@ export interface HistoryState { } export type HistoryStateEvent = CustomEvent; -function newHistoryStateEvent(state: HistoryState): HistoryStateEvent { - return new CustomEvent(historyStateEvent, { detail: state }); +interface HistoryItem { + title: string; + message?: string; + time: Date | null; + isActive: boolean; } declare global { @@ -152,10 +157,6 @@ export class OscdHistory extends LitElement { /** XML Editor to apply changes to the scd */ @property({ type: Object }) editor!: XMLEditor; - /** All [[`CommitEntry`]]s received so far through [[`LogEvent`]]s */ - @property({ type: Array }) - history: CommitEntry[] = []; - /** Index of the last [[`EditorAction`]] applied. */ @property({ type: Number }) editCount = -1; @@ -163,14 +164,15 @@ export class OscdHistory extends LitElement { @property() diagnoses = new Map(); - @property({ - type: Object, - }) + @property({ type: Object }) host!: HTMLElement; @state() latestIssue!: IssueDetail; + @state() + history: HistoryItem[] = []; + @query('#log') logUI!: Dialog; @query('#history') historyUI!: Dialog; @query('#diagnostic') diagnosticUI!: Dialog; @@ -187,14 +189,6 @@ export class OscdHistory extends LitElement { return this.editor.future.length > 0; } - get nextAction(): number { - let index = this.history - .slice(this.editCount + 1) - .findIndex(entry => entry.kind == 'action'); - if (index >= 0) index += this.editCount + 1; - return index; - } - private onIssue(de: IssueEvent): void { const issues = this.diagnoses.get(de.detail.validatorId); @@ -207,74 +201,14 @@ export class OscdHistory extends LitElement { } undo(): void { + console.log('Run undo') this.editor.undo(); + this.updateHistory(); } redo(): void { + console.log('Run redo') this.editor.redo(); - } - - private onHistory(detail: CommitDetail) { - const entry: CommitEntry = { - time: new Date(), - ...detail, - }; - - if (this.nextAction !== -1) { - this.history.splice(this.nextAction); - } - - this.addHistoryEntry(entry); - this.setEditCount(this.history.length - 1); - this.requestUpdate('history', []); - } - - private addHistoryEntry(entry: CommitEntry) { - const shouldSquash = Boolean(entry.squash) && this.history.length > 0; - - if (shouldSquash) { - const previousEntry = this.history.pop() as CommitEntry; - const squashedEntry = this.squashHistoryEntries(entry, previousEntry); - this.history.push(squashedEntry); - } else { - this.history.push(entry); - } - } - - private squashHistoryEntries(current: CommitEntry, previous: CommitEntry): CommitEntry { - const undo = this.squashUndo(current.undo, previous.undo); - const redo = this.squashRedo(current.redo, previous.redo); - - return { - ...current, - undo, - redo - }; - } - - private squashUndo(current: EditV2, previous: EditV2): EditV2 { - const isCurrentComplex = isComplexV2(current); - const isPreviousComplex = isComplexV2(previous); - - const previousUndos: EditV2[] = (isPreviousComplex ? previous : [ previous ]) as EditV2[]; - const currentUndos: EditV2[] = (isCurrentComplex ? current : [ current ]) as EditV2[]; - - return [ - ...currentUndos, - ...previousUndos - ]; - } - - private squashRedo(current: EditV2, previous: EditV2): EditV2 { - const isCurrentComplex = isComplexV2(current); - const isPreviousComplex = isComplexV2(previous); - - const previousRedos: EditV2[] = (isPreviousComplex ? previous : [ previous ]) as EditV2[]; - const currentRedos: EditV2[] = (isCurrentComplex ? current : [ current ]) as EditV2[]; - - return [ - ...previousRedos, - ...currentRedos - ]; + this.updateHistory(); } private onReset() { @@ -285,17 +219,7 @@ export class OscdHistory extends LitElement { private setEditCount(count: number): void { this.editCount = count; - this.dispatchHistoryStateEvent(); - } - - private dispatchHistoryStateEvent(): void { - this.host.dispatchEvent( - newHistoryStateEvent({ - editCount: this.editCount, - canUndo: this.canUndo, - canRedo: this.canRedo - }) - ); + // this.dispatchHistoryStateEvent(); } private onInfo(detail: InfoDetail) { @@ -328,7 +252,8 @@ export class OscdHistory extends LitElement { this.onReset(); break; case 'action': - this.onHistory(le.detail); + // No longer needed + // this.onHistory(le.detail); break; default: this.onInfo(le.detail); @@ -364,6 +289,24 @@ export class OscdHistory extends LitElement { : this.diagnosticUI.show(); } + private updateHistory(): void { + const { past, future } = this.editor; + + const activeIndex = past.length - 1; + const allEntries = [ ...past, ...future ]; + + this.history = allEntries.map((e, index) => { + const { title, message } = getLogText(e.redo as any); + + return { + isActive: index === activeIndex, + time: null, + title: e.title ?? title, + message + }; + }); + } + constructor() { super(); this.undo = this.undo.bind(this); @@ -373,17 +316,13 @@ export class OscdHistory extends LitElement { this.historyUIHandler = this.historyUIHandler.bind(this); this.emptyIssuesHandler = this.emptyIssuesHandler.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); - this.dispatchHistoryStateEvent = this.dispatchHistoryStateEvent.bind(this); document.onkeydown = this.handleKeyPress; } connectedCallback(): void { super.connectedCallback(); - this.editor.subscribe(e => { - console.log(e) - this.requestUpdate('history', []); - }); + this.editor.subscribe(e => this.updateHistory()); this.host.addEventListener('log', this.onLog); this.host.addEventListener('issue', this.onIssue); @@ -424,32 +363,21 @@ export class OscdHistory extends LitElement { } renderHistoryEntry( - entry: CommitEntry, - index: number, - history: LogEntry[] + entry: HistoryItem ): TemplateResult { return html` ${entry.time?.toLocaleString()} - ${entry.title} + ${entry.title} + ${entry.message} - history - `; + + `; } private renderLog(): TemplateResult[] | TemplateResult { @@ -464,7 +392,7 @@ export class OscdHistory extends LitElement { private renderHistory(): TemplateResult[] | TemplateResult { if (this.history.length > 0) - return this.history.slice().reverse().map(this.renderHistoryEntry, this); + return this.history.slice().reverse().map(this.renderHistoryEntry); else return html` ${get('history.placeholder')} diff --git a/packages/openscd/src/addons/history/get-log-text.ts b/packages/openscd/src/addons/history/get-log-text.ts new file mode 100644 index 0000000000..ac13f48f5a --- /dev/null +++ b/packages/openscd/src/addons/history/get-log-text.ts @@ -0,0 +1,31 @@ +import { + EditV2, + isInsertV2, + isRemoveV2, + isSetAttributesV2, + isSetTextContentV2, + isComplexV2 +} from '@openscd/core'; +import { get } from 'lit-translate'; + +export const getLogText = (edit: EditV2): { title: string, message?: string } => { + if (isInsertV2(edit)) { + const name = edit.node instanceof Element ? + edit.node.tagName : + get('editing.node'); + return { title: get('editing.created', { name }) }; + } else if (isSetAttributesV2(edit) || isSetTextContentV2(edit)) { + const name = edit.element.tagName; + return { title: get('editing.updated', { name }) }; + } else if (isRemoveV2(edit)) { + const name = edit.node instanceof Element ? + edit.node.tagName : + get('editing.node'); + return { title: get('editing.deleted', { name }) }; + } else if (isComplexV2(edit)) { + const message = edit.map(e => getLogText(e)).map(({ title }) => title).join(', '); + return { title: get('editing.complex'), message }; + } + + return { title: '' }; +} From be8bfb26f5c87057ad83067d120c0665196f24b5 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 30 Oct 2025 14:08:23 +0100 Subject: [PATCH 03/14] feat: Unsubscribe on destroy --- packages/core/api/editor/xml-editor.ts | 2 ++ packages/openscd/src/addons/History.ts | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/core/api/editor/xml-editor.ts b/packages/core/api/editor/xml-editor.ts index 5c2417ecf0..3c51584d6f 100644 --- a/packages/core/api/editor/xml-editor.ts +++ b/packages/core/api/editor/xml-editor.ts @@ -37,6 +37,7 @@ export class XMLEditor implements Transactor { // TODO: Fix type once issue is fixed https://github.com/openscd/oscd-api/issues/57 handleEditV2(commit.undo as any); this.future.unshift(commit); + this.undoSubject.next(commit); return commit; }; @@ -46,6 +47,7 @@ export class XMLEditor implements Transactor { // TODO: Fix type once issue is fixed https://github.com/openscd/oscd-api/issues/57 handleEditV2(commit.redo as any); this.past.push(commit); + this.redoSubject.next(commit); return commit; }; diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index 21f113b25d..e122bc9eb6 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -181,6 +181,8 @@ export class OscdHistory extends LitElement { @query('#info') infoUI!: Snackbar; @query('#issue') issueUI!: Snackbar; + private unsubscribers: (() => any)[] = []; + get canUndo(): boolean { console.log(`Can undo: ${this.editor.past.length > 0}`) return this.editor.past.length > 0; @@ -201,14 +203,10 @@ export class OscdHistory extends LitElement { } undo(): void { - console.log('Run undo') this.editor.undo(); - this.updateHistory(); } redo(): void { - console.log('Run redo') this.editor.redo(); - this.updateHistory(); } private onReset() { @@ -322,17 +320,22 @@ export class OscdHistory extends LitElement { connectedCallback(): void { super.connectedCallback(); - this.editor.subscribe(e => this.updateHistory()); + this.unsubscribers.push( + this.editor.subscribe(e => this.updateHistory()), + this.editor.subscribeUndoRedo(e => this.updateHistory()) + ); this.host.addEventListener('log', this.onLog); this.host.addEventListener('issue', this.onIssue); this.host.addEventListener('history-dialog-ui', this.historyUIHandler); this.host.addEventListener('empty-issues', this.emptyIssuesHandler); - this.host.addEventListener('undo', this.undo); - this.host.addEventListener('redo', this.redo); this.diagnoses.clear(); } + disconnectedCallback(): void { + this.unsubscribers.forEach(u => u()); + } + renderLogEntry( entry: InfoEntry, index: number, From 613e81629146e052655ec99e9ce6a12851a88f49 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 30 Oct 2025 14:18:41 +0100 Subject: [PATCH 04/14] feat: Remove history state and adjust layout --- packages/openscd/src/addons/History.ts | 25 +-------------- packages/openscd/src/addons/Layout.ts | 30 ++++++------------ packages/openscd/src/open-scd.ts | 42 ++++++++++++++------------ 3 files changed, 33 insertions(+), 64 deletions(-) diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index e122bc9eb6..c6a218a8a8 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -38,18 +38,10 @@ import { import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../plugin.js'; -import { EditV2, isComplexV2, newEditEventV2, XMLEditor } from '@openscd/core'; +import { XMLEditor } from '@openscd/core'; import { getLogText } from './history/get-log-text.js'; -export const historyStateEvent = 'history-state'; -export interface HistoryState { - editCount: number; - canUndo: boolean; - canRedo: boolean; -} -export type HistoryStateEvent = CustomEvent; - interface HistoryItem { title: string; message?: string; @@ -57,12 +49,6 @@ interface HistoryItem { isActive: boolean; } -declare global { - interface ElementEventMap { - [historyStateEvent]: HistoryStateEvent; - } -} - const icons = { info: 'info', warning: 'warning', @@ -140,14 +126,6 @@ export function newEmptyIssuesEvent( }); } -export function newUndoEvent(): CustomEvent { - return new CustomEvent('undo', { bubbles: true, composed: true }); -} - -export function newRedoEvent(): CustomEvent { - return new CustomEvent('redo', { bubbles: true, composed: true }); -} - @customElement('oscd-history') export class OscdHistory extends LitElement { /** All [[`LogEntry`]]s received so far through [[`LogEvent`]]s. */ @@ -217,7 +195,6 @@ export class OscdHistory extends LitElement { private setEditCount(count: number): void { this.editCount = count; - // this.dispatchHistoryStateEvent(); } private onInfo(detail: InfoDetail) { diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index 659c3f15fa..de6fca4791 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -11,40 +11,27 @@ import { import { get } from 'lit-translate'; import { newPendingStateEvent } from '@openscd/core/foundation/deprecated/waiter.js'; import { newSettingsUIEvent } from '@openscd/core/foundation/deprecated/settings.js'; +import { XMLEditor } from '@openscd/core'; import { MenuItem, Validator, MenuPlugin, pluginIcons, - newResetPluginsEvent, - newAddExternalPluginEvent, - newSetPluginsEvent, } from '../open-scd.js'; import { - MenuPosition, Plugin, - menuPosition, - PluginKind, } from "../plugin.js" import { - HistoryState, HistoryUIKind, newEmptyIssuesEvent, - newHistoryUIEvent, - newRedoEvent, - newUndoEvent, + newHistoryUIEvent } from './History.js'; import type { Drawer } from '@material/mwc-drawer'; import type { ActionDetail } from '@material/mwc-list'; import { List } from '@material/mwc-list'; import type { ListItem } from '@material/mwc-list/mwc-list-item'; -import type { Dialog } from '@material/mwc-dialog'; -import type { MultiSelectedEvent } from '@material/mwc-list/mwc-list-foundation.js'; -import type { Select } from '@material/mwc-select'; -import type { Switch } from '@material/mwc-switch'; -import type { TextField } from '@material/mwc-textfield'; import '@material/mwc-drawer'; import '@material/mwc-list'; @@ -72,14 +59,15 @@ export class OscdLayout extends LitElement { /** Index of the last [[`EditorAction`]] applied. */ @property({ type: Number }) editCount = -1; + /** XML Editor to apply changes to the scd */ + @property({ type: Object }) editor!: XMLEditor; + /** The plugins to render the layout. */ @property({ type: Array }) plugins: Plugin[] = []; /** The open-scd host element */ @property({ type: Object }) host!: HTMLElement; - @property({ type: Object }) historyState!: HistoryState; - @state() validated: Promise = Promise.resolve(); @state() shouldValidate = false; @state() activeEditor: Plugin | undefined = this.calcActiveEditors()[0]; @@ -173,9 +161,9 @@ export class OscdLayout extends LitElement { name: 'undo', actionItem: true, action: (): void => { - this.dispatchEvent(newUndoEvent()); + this.editor.undo(); }, - disabled: (): boolean => !this.historyState.canUndo, + disabled: (): boolean => this.editor.past.length === 0, kind: 'static', content: () => html``, }, @@ -184,9 +172,9 @@ export class OscdLayout extends LitElement { name: 'redo', actionItem: true, action: (): void => { - this.dispatchEvent(newRedoEvent()); + this.editor.redo(); }, - disabled: (): boolean => !this.historyState.canRedo, + disabled: (): boolean => this.editor.future.length === 0, kind: 'static', content: () => html``, }, diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index 3760126c11..9de34177c2 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -47,8 +47,6 @@ import type { } from '@openscd/core'; import { OscdApi, XMLEditor } from '@openscd/core'; -import { HistoryState, historyStateEvent } from './addons/History.js'; - import { InstalledOfficialPlugin, MenuPosition, PluginKind, Plugin } from "./plugin.js" import { ConfigurePluginEvent, ConfigurePluginDetail, newConfigurePluginEvent } from './plugin.events.js'; import { newLogEvent } from '@openscd/core/foundation/deprecated/history'; @@ -65,13 +63,17 @@ export class OpenSCD extends LitElement { return html` - + @@ -103,13 +105,6 @@ export class OpenSCD extends LitElement { editor = new XMLEditor(); - @state() - historyState: HistoryState = { - editCount: -1, - canRedo: false, - canUndo: false, - } - /** Object containing all *.nsdoc files and a function extracting element's label form them*/ @property({ attribute: false }) nsdoc: Nsdoc = initializeNsdoc(); @@ -128,6 +123,10 @@ export class OpenSCD extends LitElement { @state() private storedPlugins: Plugin[] = []; + @state() private editCount = -1; + + private unsubscribers: (() => any)[] = []; + /** Loads and parses an `XMLDocument` after [[`src`]] has changed. */ private async loadDoc(src: string): Promise { const response = await fetch(src); @@ -187,14 +186,19 @@ export class OpenSCD extends LitElement { connectedCallback(): void { super.connectedCallback(); - this.loadPlugins() + this.loadPlugins(); + + this.unsubscribers.push( + this.editor.subscribe(e => this.editCount++), + this.editor.subscribeUndoRedo(e => this.editCount++) + ); // TODO: let Lit handle the event listeners, move to render() this.addEventListener('reset-plugins', this.resetPlugins); - this.addEventListener(historyStateEvent, (e: CustomEvent) => { - this.historyState = e.detail; - this.requestUpdate(); - }); + } + + disconnectedCallback(): void { + this.unsubscribers.forEach(u => u()); } @@ -428,7 +432,7 @@ export class OpenSCD extends LitElement { return staticTagHtml`<${tag} .doc=${this.doc} .docName=${this.docName} - .editCount=${this.historyState.editCount} + .editCount=${this.editCount} .plugins=${this.storedPlugins} .docId=${this.docId} .pluginId=${plugin.src} From a60bf2e7724efc3a5f1b68fb556d5add945e7c3e Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 30 Oct 2025 14:59:49 +0100 Subject: [PATCH 05/14] feat: Add time to commit --- packages/core/api/editor/xml-editor.ts | 42 ++++++++++++++++++-------- packages/openscd/src/addons/Editor.ts | 19 ------------ packages/openscd/src/addons/History.ts | 41 ++++++++----------------- packages/openscd/src/addons/Layout.ts | 4 +-- packages/openscd/src/open-scd.ts | 1 - 5 files changed, 44 insertions(+), 63 deletions(-) diff --git a/packages/core/api/editor/xml-editor.ts b/packages/core/api/editor/xml-editor.ts index 3c51584d6f..89d30f8f19 100644 --- a/packages/core/api/editor/xml-editor.ts +++ b/packages/core/api/editor/xml-editor.ts @@ -4,20 +4,36 @@ import { EditV2 } from '@openscd/oscd-api/dist/editv2.js'; import { Subject } from './subject.js'; import { handleEditV2 } from '../../foundation.js'; +export interface OscdCommit extends Commit { + time: number; +} export class XMLEditor implements Transactor { - public past: Commit[] = []; - public future: Commit[] = []; + public past: OscdCommit[] = []; + public future: OscdCommit[] = []; + + private commitSubject = new Subject>(); + private undoSubject = new Subject>(); + private redoSubject = new Subject>(); + + get canUndo(): boolean { + return this.past.length > 0; + } - private commitSubject = new Subject>(); - private undoSubject = new Subject>(); - private redoSubject = new Subject>(); + get canRedo(): boolean { + return this.future.length > 0; + } + + reset(): void { + this.past = []; + this.future = []; + } - commit(change: EditV2, { title, squash }: CommitOptions = {}): Commit { - const commit: Commit = + commit(change: EditV2, { title, squash }: CommitOptions = {}): OscdCommit { + const commit: OscdCommit = squash && this.past.length ? this.past[this.past.length - 1] - : { undo: [], redo: [] }; + : { undo: [], redo: [], time: Date.now() }; // TODO: Fix type once issue is fixed https://github.com/openscd/oscd-api/issues/57 const undo = handleEditV2(change as any); // typed as per https://github.com/microsoft/TypeScript/issues/49280#issuecomment-1144181818 recommendation: @@ -31,7 +47,7 @@ export class XMLEditor implements Transactor { return commit; }; - undo(): Commit | undefined { + undo(): OscdCommit | undefined { const commit = this.past.pop(); if (!commit) return; // TODO: Fix type once issue is fixed https://github.com/openscd/oscd-api/issues/57 @@ -41,7 +57,7 @@ export class XMLEditor implements Transactor { return commit; }; - redo(): Commit | undefined { + redo(): OscdCommit | undefined { const commit = this.future.shift(); if (!commit) return; // TODO: Fix type once issue is fixed https://github.com/openscd/oscd-api/issues/57 @@ -52,15 +68,15 @@ export class XMLEditor implements Transactor { }; subscribe(txCallback: TransactedCallback): () => TransactedCallback { - return this.commitSubject.subscribe(txCallback); + return this.commitSubject.subscribe(txCallback) as () => TransactedCallback; }; subscribeUndo(txCallback: TransactedCallback): () => TransactedCallback { - return this.undoSubject.subscribe(txCallback); + return this.undoSubject.subscribe(txCallback) as () => TransactedCallback; } subscribeRedo(txCallback: TransactedCallback): () => TransactedCallback { - return this.redoSubject.subscribe(txCallback); + return this.redoSubject.subscribe(txCallback) as () => TransactedCallback; } subscribeUndoRedo(txCallback: TransactedCallback): () => TransactedCallback { diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 25a186fc58..7342dca599 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -156,27 +156,8 @@ export class OscdEditor extends LitElement { async handleEditEventV2(event: EditEventV2) { const { edit, title, squash } = event.detail; - // const undoEdit = handleEditV2(edit); this.editor.commit(edit, { title, squash }); - const shouldCreateHistoryEntry = event.detail.createHistoryEntry !== false; - - if (shouldCreateHistoryEntry) { - const { title, message } = this.getLogText(edit); - - // TODO - /* - this.dispatchEvent(newLogEvent({ - kind: 'action', - title: event.detail.title ?? title, - message, - redo: edit, - undo: undoEdit, - squash: event.detail.squash - })); - */ - } - await this.updateComplete; this.dispatchEvent(newValidateEvent()); } diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index c6a218a8a8..56ba4b5d61 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -24,8 +24,6 @@ import { Snackbar } from '@material/mwc-snackbar'; import '../filtered-list.js'; import { - CommitDetail, - CommitEntry, InfoDetail, InfoEntry, IssueDetail, @@ -45,7 +43,7 @@ import { getLogText } from './history/get-log-text.js'; interface HistoryItem { title: string; message?: string; - time: Date | null; + time: number; isActive: boolean; } @@ -135,10 +133,6 @@ export class OscdHistory extends LitElement { /** XML Editor to apply changes to the scd */ @property({ type: Object }) editor!: XMLEditor; - /** Index of the last [[`EditorAction`]] applied. */ - @property({ type: Number }) - editCount = -1; - @property() diagnoses = new Map(); @@ -161,14 +155,6 @@ export class OscdHistory extends LitElement { private unsubscribers: (() => any)[] = []; - get canUndo(): boolean { - console.log(`Can undo: ${this.editor.past.length > 0}`) - return this.editor.past.length > 0; - } - get canRedo(): boolean { - return this.editor.future.length > 0; - } - private onIssue(de: IssueEvent): void { const issues = this.diagnoses.get(de.detail.validatorId); @@ -189,12 +175,6 @@ export class OscdHistory extends LitElement { private onReset() { this.log = []; - this.history = []; - this.setEditCount(-1); - } - - private setEditCount(count: number): void { - this.editCount = count; } private onInfo(detail: InfoDetail) { @@ -275,7 +255,7 @@ export class OscdHistory extends LitElement { return { isActive: index === activeIndex, - time: null, + time: e.time, title: e.title ?? title, message }; @@ -323,7 +303,6 @@ export class OscdHistory extends LitElement { class="${entry.kind}" graphic="icon" ?twoline=${!!entry.message} - ?activated=${this.editCount == log.length - index - 1} > @@ -351,8 +330,7 @@ export class OscdHistory extends LitElement { ?activated=${entry.isActive} > - - ${entry.time?.toLocaleString()} + ${this.formatTime(entry.time)} ${entry.title} ${entry.message} @@ -360,6 +338,13 @@ export class OscdHistory extends LitElement { `; } + private formatTime(time: number): string { + const date = new Date(time); + const hours = date.getHours(); + const minutes = date.getMinutes(); + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}` + } + private renderLog(): TemplateResult[] | TemplateResult { if (this.log.length > 0) return this.log.slice().reverse().map(this.renderLogEntry, this); @@ -372,7 +357,7 @@ export class OscdHistory extends LitElement { private renderHistory(): TemplateResult[] | TemplateResult { if (this.history.length > 0) - return this.history.slice().reverse().map(this.renderHistoryEntry); + return this.history.slice().reverse().map(e => this.renderHistoryEntry(e)); else return html` ${get('history.placeholder')} @@ -444,14 +429,14 @@ export class OscdHistory extends LitElement { diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index de6fca4791..58ad60aeed 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -163,7 +163,7 @@ export class OscdLayout extends LitElement { action: (): void => { this.editor.undo(); }, - disabled: (): boolean => this.editor.past.length === 0, + disabled: (): boolean => !this.editor.canUndo, kind: 'static', content: () => html``, }, @@ -174,7 +174,7 @@ export class OscdLayout extends LitElement { action: (): void => { this.editor.redo(); }, - disabled: (): boolean => this.editor.future.length === 0, + disabled: (): boolean => !this.editor.canRedo, kind: 'static', content: () => html``, }, diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index 9de34177c2..d64860ffd9 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -65,7 +65,6 @@ export class OpenSCD extends LitElement { Date: Thu, 30 Oct 2025 15:48:44 +0100 Subject: [PATCH 06/14] feat: Reset editor on new doc --- packages/openscd/src/addons/Editor.ts | 35 -------------------------- packages/openscd/src/addons/History.ts | 2 ++ packages/openscd/src/open-scd.ts | 1 + 3 files changed, 3 insertions(+), 35 deletions(-) diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 7342dca599..d385bbc580 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -1,15 +1,7 @@ import { - EditV2, EditEventV2, OpenEvent, - newEditCompletedEvent, newEditEvent, - handleEditV2, - isInsertV2, - isRemoveV2, - isSetAttributesV2, - isSetTextContentV2, - isComplexV2, newEditEventV2, XMLEditor } from '@openscd/core'; @@ -32,17 +24,12 @@ import { newValidateEvent } from '@openscd/core/foundation/deprecated/validation import { OpenDocEvent } from '@openscd/core/foundation/deprecated/open-event.js'; import { - AttributeValue, Edit, EditEvent, - Insert, isComplex, isInsert, - isNamespaced, isRemove, isUpdate, - Remove, - Update, } from '@openscd/core'; import { convertEditActiontoV1 } from './editor/edit-action-to-v1-converter.js'; @@ -65,28 +52,6 @@ export class OscdEditor extends LitElement { }) host!: HTMLElement; - private getLogText(edit: EditV2): { title: string, message?: string } { - if (isInsertV2(edit)) { - const name = edit.node instanceof Element ? - edit.node.tagName : - get('editing.node'); - return { title: get('editing.created', { name }) }; - } else if (isSetAttributesV2(edit) || isSetTextContentV2(edit)) { - const name = edit.element.tagName; - return { title: get('editing.updated', { name }) }; - } else if (isRemoveV2(edit)) { - const name = edit.node instanceof Element ? - edit.node.tagName : - get('editing.node'); - return { title: get('editing.deleted', { name }) }; - } else if (isComplexV2(edit)) { - const message = edit.map(e => this.getLogText(e)).map(({ title }) => title).join(', '); - return { title: get('editing.complex'), message }; - } - - return { title: '' }; - } - private onAction(event: EditorActionEvent) { const edit = convertEditActiontoV1(event.detail.action); const editV2 = convertEditV1toV2(edit); diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index 56ba4b5d61..b7b0cc9340 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -175,6 +175,8 @@ export class OscdHistory extends LitElement { private onReset() { this.log = []; + this.editor.reset(); + this.updateHistory(); } private onInfo(detail: InfoDetail) { diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index d64860ffd9..c0f88d50d8 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -439,6 +439,7 @@ export class OpenSCD extends LitElement { .docs=${this.docs} .locale=${this.locale} .oscdApi=${new OscdApi(tag)} + .editor=${this.editor} class="${classMap({ plugin: true, menu: plugin.kind === 'menu', From 09fe6f549560c82c87a767002d7f2227c57ccd70 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 3 Nov 2025 13:09:09 +0100 Subject: [PATCH 07/14] test: Fix editor tests --- packages/openscd/test/unit/Editor.test.ts | 55 ++++++----------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/packages/openscd/test/unit/Editor.test.ts b/packages/openscd/test/unit/Editor.test.ts index 0b1cbd6d18..b118281714 100644 --- a/packages/openscd/test/unit/Editor.test.ts +++ b/packages/openscd/test/unit/Editor.test.ts @@ -11,7 +11,8 @@ import { Update, SetAttributesV2, SetTextContentV2, - RemoveV2 + RemoveV2, + XMLEditor } from '@openscd/core'; import { CommitDetail, LogDetail } from '@openscd/core/foundation/deprecated/history.js'; @@ -20,6 +21,7 @@ describe('OSCD-Editor', () => { let element: OscdEditor; let host: HTMLElement; let scd: XMLDocument; + let editor: XMLEditor; let voltageLevel1: Element; let voltageLevel2: Element; @@ -57,8 +59,9 @@ describe('OSCD-Editor', () => { ); host = document.createElement('div'); + editor = new XMLEditor(); - element = await fixture(html``, { parentNode: host }); + element = await fixture(html``, { parentNode: host }); voltageLevel1 = scd.querySelector('VoltageLevel[name="v1"]')!; voltageLevel2 = scd.querySelector('VoltageLevel[name="v2"]')!; @@ -336,20 +339,6 @@ describe('OSCD-Editor', () => { }); }); - it('should log edit by default', () => { - const remove: RemoveV2 = { - node: bay2, - }; - - host.dispatchEvent(newEditEventV2(remove)); - - expect(log).to.have.lengthOf(1); - const logEntry = log[0] as CommitDetail; - expect(logEntry.kind).to.equal('action'); - expect(logEntry.title).to.equal('[editing.deleted]'); - expect(logEntry.redo).to.deep.equal(remove); - }); - describe('validate after edit', () => { let hasTriggeredValidate = false; beforeEach(() => { @@ -398,9 +387,7 @@ describe('OSCD-Editor', () => { host.dispatchEvent(newEditEventV2(insert)); - const undoInsert = log[0].undo as RemoveV2; - - host.dispatchEvent(newEditEventV2(undoInsert)); + editor.undo(); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; }); @@ -412,9 +399,7 @@ describe('OSCD-Editor', () => { host.dispatchEvent(newEditEventV2(remove)); - const undoRemove = log[0].undo as InsertV2; - - host.dispatchEvent(newEditEventV2(undoRemove)); + editor.undo(); const bay4FromScd = scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b4"]'); expect(bay4FromScd).to.deep.equal(bay4); @@ -432,9 +417,7 @@ describe('OSCD-Editor', () => { host.dispatchEvent(newEditEventV2(update)); - const undoUpdate = log[0].undo as SetAttributesV2; - - host.dispatchEvent(newEditEventV2(undoUpdate)); + editor.undo(); expect(bay1.getAttribute('desc')).to.be.null; expect(bay1.getAttribute('kind')).to.equal('bay'); @@ -448,9 +431,7 @@ describe('OSCD-Editor', () => { host.dispatchEvent(newEditEventV2(update)); - const undoUpdate = log[0].undo as SetTextContentV2; - - host.dispatchEvent(newEditEventV2(undoUpdate)); + editor.undo(); expect(bayWithoutTextContent.textContent).to.be.empty; }); @@ -465,9 +446,7 @@ describe('OSCD-Editor', () => { expect(bay2.children).to.be.empty; - const undoUpdate = log[0].undo as SetTextContentV2; - - host.dispatchEvent(newEditEventV2(undoUpdate)); + editor.undo(); expect(bay2.children[0]).to.deep.equal(lnode2); }); @@ -484,14 +463,11 @@ describe('OSCD-Editor', () => { host.dispatchEvent(newEditEventV2(insert)); - const undoIsert = log[0].undo; - const redoInsert = log[0].redo; - - host.dispatchEvent(newEditEventV2(undoIsert)); + editor.undo(); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; - host.dispatchEvent(newEditEventV2(redoInsert)); + editor.redo(); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); }); @@ -520,16 +496,13 @@ describe('OSCD-Editor', () => { host.dispatchEvent(newEditEventV2([insert, remove, update])); - const undoComplex = log[0].undo; - const redoComplex = log[0].redo; - - host.dispatchEvent(newEditEventV2(undoComplex)); + editor.undo(); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.deep.equal(bay2); expect(bay1.getAttribute('desc')).to.be.null; - host.dispatchEvent(newEditEventV2(redoComplex)); + editor.redo(); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; From d5fb427b86222a8f3b3155d437c8b10943c3e811 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 3 Nov 2025 13:19:13 +0100 Subject: [PATCH 08/14] test: Fix tests --- .../openscd/test/integration/Setting.test.ts | 5 +- packages/openscd/test/mock-editor-logger.ts | 7 +- packages/openscd/test/unit/Historing.test.ts | 134 +----------------- 3 files changed, 13 insertions(+), 133 deletions(-) diff --git a/packages/openscd/test/integration/Setting.test.ts b/packages/openscd/test/integration/Setting.test.ts index e5e7483e24..c2f4829dab 100644 --- a/packages/openscd/test/integration/Setting.test.ts +++ b/packages/openscd/test/integration/Setting.test.ts @@ -5,16 +5,19 @@ import '../../src/addons/History.js'; import '../../src/addons/Settings.js'; import { OscdHistory } from '../../src/addons/History.js'; import { OscdSettings } from '../../src/addons/Settings.js'; +import { XMLEditor } from '@openscd/core'; describe('Oscd-Settings', () => { let logger: OscdHistory; let settings: OscdSettings; + let editor: XMLEditor; beforeEach(async () => { localStorage.clear(); + editor = new XMLEditor(); logger = await fixture( - html` + html` ` ); diff --git a/packages/openscd/test/mock-editor-logger.ts b/packages/openscd/test/mock-editor-logger.ts index 2894e6206d..802fdc147e 100644 --- a/packages/openscd/test/mock-editor-logger.ts +++ b/packages/openscd/test/mock-editor-logger.ts @@ -12,6 +12,7 @@ import '../src/addons/Editor.js'; import '../src/addons/History.js'; import { OscdEditor } from '../src/addons/Editor.js'; import { OscdHistory } from '../src/addons/History.js'; +import { XMLEditor } from '@openscd/core'; @customElement('mock-editor-logger') export class MockEditorLogger extends LitElement { @@ -30,14 +31,18 @@ export class MockEditorLogger extends LitElement { @query('oscd-editor') editor!: OscdEditor; + @state() + xmlEditor = new XMLEditor(); + render(): TemplateResult { - return html` + return html` `; diff --git a/packages/openscd/test/unit/Historing.test.ts b/packages/openscd/test/unit/Historing.test.ts index c93869afba..73dffd80cc 100644 --- a/packages/openscd/test/unit/Historing.test.ts +++ b/packages/openscd/test/unit/Historing.test.ts @@ -19,38 +19,7 @@ describe('HistoringElement', () => { element = mock.historyAddon; }); - 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('starts out with an empty log', () => expect(element).property('log').to.be.empty); it('renders a placeholder message', () => expect(element.logUI).to.contain('mwc-list-item[disabled]')); @@ -101,14 +70,13 @@ describe('HistoringElement', () => { expect(element.diagnosticUI).to.have.property('open', true); }); + // TODO: Replace wiht editor -> history tests describe('with an action logged', () => { beforeEach(async () => { element.dispatchEvent( newLogEvent({ kind: 'action', - title: 'test MockAction', - redo: mockEdits.insert(), - undo: mockEdits.insert() + title: 'test MockAction' }) ); element.requestUpdate(); @@ -117,106 +85,10 @@ describe('HistoringElement', () => { await mock.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('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', - redo: mockEdits.remove(), - undo: mockEdits.remove() - }) - ); - }); - - 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', async () => { - element.dispatchEvent( - newLogEvent({ - kind: 'action', - title: 'test MockAction', - redo: mockEdits.insert(), - undo: mockEdits.insert() - }) - ); - await element.updateComplete; - 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(async () => { - element.undo(); - await element.updateComplete; - await mock.updateComplete; - }); - - it('cannot undo any funther', () => { - console.log('error'); - 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); - }); - }); }); }); From 0bf76e129122fd9c83eeff4397ec25c802281ef4 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 3 Nov 2025 13:49:36 +0100 Subject: [PATCH 09/14] test: Add history tests --- packages/openscd/test/unit/Historing.test.ts | 70 +++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/packages/openscd/test/unit/Historing.test.ts b/packages/openscd/test/unit/Historing.test.ts index 73dffd80cc..722c207dcc 100644 --- a/packages/openscd/test/unit/Historing.test.ts +++ b/packages/openscd/test/unit/Historing.test.ts @@ -10,12 +10,27 @@ import { newLogEvent, } from '@openscd/core/foundation/deprecated/history.js'; import { OscdHistory } from '../../src/addons/History.js'; +import { InsertV2 } from '@openscd/core'; +import { createElement } from '@openscd/xml'; describe('HistoringElement', () => { let mock: MockOpenSCD; let element: OscdHistory; + let scd: XMLDocument; + beforeEach(async () => { - mock = await fixture(html``); + scd = new DOMParser().parseFromString( + ` + + + + + + `, + 'application/xml', + ); + + mock = await fixture(html``); element = mock.historyAddon; }); @@ -70,21 +85,60 @@ describe('HistoringElement', () => { expect(element.diagnosticUI).to.have.property('open', true); }); - // TODO: Replace wiht editor -> history tests describe('with an action logged', () => { + const insertTitle = 'Insert bay 2'; + let voltageLevel: Element; + beforeEach(async () => { - element.dispatchEvent( - newLogEvent({ - kind: 'action', - title: 'test MockAction' - }) - ); + voltageLevel = scd.querySelector('VoltageLevel')!; + const bay2 = createElement(scd, 'Bay', { name: 'b2' }); + const insert: InsertV2 = { + parent: voltageLevel, + node: bay2, + reference: null + }; + element.editor.commit(insert, { title: insertTitle }); + element.requestUpdate(); await element.updateComplete; mock.requestUpdate(); await mock.updateComplete; }); + it('should have a history', () => { + expect(element.history.length).to.equal(1); + const insertEntry = element.history[0]; + expect(insertEntry.title).to.equal(insertTitle); + expect(insertEntry.isActive).to.true; + }); + + it('should keep undone entries in history and set is active accordingly', () => { + const bay3 = createElement(scd, 'Bay', { name: 'b3' }); + const insert: InsertV2 = { + parent: voltageLevel, + node: bay3, + reference: null + }; + + element.editor.commit(insert); + + let [ bay2Insert, bay3Insert ] = element.history; + expect(bay2Insert.isActive).to.be.false; + expect(bay3Insert.isActive).to.be.true; + + element.editor.undo(); + + [ bay2Insert, bay3Insert ] = element.history; + expect(bay2Insert.isActive).to.be.true; + expect(bay3Insert.isActive).to.be.false; + + element.editor.redo(); + + [ bay2Insert, bay3Insert ] = element.history; + expect(bay2Insert.isActive).to.be.false; + expect(bay3Insert.isActive).to.be.true; + }); + it('can reset its log', () => { element.dispatchEvent(newLogEvent({ kind: 'reset' })); expect(element).property('log').to.be.empty; From 84fc40276f9c8a1ad0fc8663d10d77295e0cb007 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 3 Nov 2025 14:34:39 +0100 Subject: [PATCH 10/14] test: Fix mock wizard editor --- packages/openscd/test/mock-wizard-editor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/openscd/test/mock-wizard-editor.ts b/packages/openscd/test/mock-wizard-editor.ts index a1962b0846..3ce08afe54 100644 --- a/packages/openscd/test/mock-wizard-editor.ts +++ b/packages/openscd/test/mock-wizard-editor.ts @@ -12,6 +12,7 @@ import '../src/addons/Wizards.js'; import '../src/addons/Editor.js'; import { OscdWizards } from '../src/addons/Wizards.js'; +import { XMLEditor } from '@openscd/core'; @customElement('mock-wizard-editor') export class MockWizardEditor extends LitElement { @@ -20,6 +21,8 @@ export class MockWizardEditor extends LitElement { @query('oscd-wizards') wizards!: OscdWizards; + editor = new XMLEditor(); + render(): TemplateResult { return html` From 7eb50b64f324dac1dde5575dc7b18649dbe89e5f Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 3 Nov 2025 15:13:38 +0100 Subject: [PATCH 11/14] test: Add tests --- package-lock.json | 1 + packages/core/.gitignore | 1 + packages/core/package.json | 2 + packages/core/test/subject.test.ts | 63 +++++++++ packages/core/web-test-runner.config.js | 170 +++--------------------- 5 files changed, 84 insertions(+), 153 deletions(-) create mode 100644 packages/core/test/subject.test.ts diff --git a/package-lock.json b/package-lock.json index b163c6a4ab..cb8756a38a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31178,6 +31178,7 @@ "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", "@web/dev-server": "^0.1.32", + "@web/dev-server-esbuild": "^0.2.16", "@web/test-runner": "next", "@web/test-runner-playwright": "^0.8.10", "@web/test-runner-visual-regression": "^0.6.6", diff --git a/packages/core/.gitignore b/packages/core/.gitignore index d56a73fc47..d6df7fd911 100644 --- a/packages/core/.gitignore +++ b/packages/core/.gitignore @@ -3,3 +3,4 @@ dist/ node_modules/ doc/ +coverage/ diff --git a/packages/core/package.json b/packages/core/package.json index 63c42b8d3f..16f992eaab 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ "clean": "rimraf .tsbuildinfo dist", "build": "tsc -b", "doc": "typedoc --out doc foundation.ts", + "test": "web-test-runner --coverage", "prepublish": "npm run lint && npm run build && npm run doc", "lint": "eslint --ext .ts,.html . --ignore-path .gitignore && prettier \"**/*.ts\" --check --ignore-path .gitignore", "format": "eslint --ext .ts,.html . --fix --ignore-path .gitignore && prettier \"**/*.ts\" --write --ignore-path .gitignore" @@ -53,6 +54,7 @@ "@typescript-eslint/parser": "^5.30.7", "@web/dev-server": "^0.1.32", "@web/test-runner": "next", + "@web/dev-server-esbuild": "^0.2.16", "@web/test-runner-playwright": "^0.8.10", "@web/test-runner-visual-regression": "^0.6.6", "concurrently": "^7.3.0", diff --git a/packages/core/test/subject.test.ts b/packages/core/test/subject.test.ts new file mode 100644 index 0000000000..0eef5aacab --- /dev/null +++ b/packages/core/test/subject.test.ts @@ -0,0 +1,63 @@ +import { expect } from '@open-wc/testing'; + +import { Subject } from '../api/editor/subject.js'; + +describe('Validators', () => { + let subject: Subject; + + let subOneValues: string[]; + let subTwoValues: string[]; + + beforeEach(() => { + subject = new Subject(); + + subOneValues = []; + subTwoValues = []; + }); + + it('should call subscribers on next', () => { + const subscriberOne = (v: string) => subOneValues.push(v); + const subscriberTwo = (v: string) => subTwoValues.push(v); + + subject.subscribe(subscriberOne); + + subject.next('first'); + + expect(subOneValues).to.deep.equal([ 'first' ]); + expect(subTwoValues).to.deep.equal([]); + + subject.subscribe(subscriberTwo); + + subject.next('second'); + + expect(subOneValues).to.deep.equal([ 'first', 'second' ]); + expect(subTwoValues).to.deep.equal([ 'second' ]); + }); + + it('should remove correct subscriber on unsubscribe', () => { + const subscriberOne = (v: string) => subOneValues.push(v); + const subscriberTwo = (v: string) => subTwoValues.push(v); + + const unsubscribeOne = subject.subscribe(subscriberOne); + const unsubscribeTwo = subject.subscribe(subscriberTwo); + + subject.next('first'); + + expect(subOneValues).to.deep.equal([ 'first' ]); + expect(subTwoValues).to.deep.equal([ 'first' ]); + + unsubscribeOne(); + + subject.next('second'); + + expect(subOneValues).to.deep.equal([ 'first' ]); + expect(subTwoValues).to.deep.equal([ 'first', 'second' ]); + + unsubscribeTwo(); + + subject.next('third'); + + expect(subOneValues).to.deep.equal([ 'first' ]); + expect(subTwoValues).to.deep.equal([ 'first', 'second' ]); + }); +}); diff --git a/packages/core/web-test-runner.config.js b/packages/core/web-test-runner.config.js index 334e8ef62d..9e4f774453 100644 --- a/packages/core/web-test-runner.config.js +++ b/packages/core/web-test-runner.config.js @@ -1,162 +1,26 @@ -import { visualRegressionPlugin } from '@web/test-runner-visual-regression/plugin'; -import { playwrightLauncher } from '@web/test-runner-playwright'; - -import pixelmatch from 'pixelmatch'; -import { PNG } from 'pngjs'; - -const fuzzy = ['win32', 'darwin'].includes(process.platform); // allow for 1% difference on non-linux OSs -const local = !process.env.CI; - -console.assert(local, 'Running in CI!'); -console.assert(!fuzzy, 'Running on OS with 1% test pixel diff threshold!'); - -const thresholdPercentage = fuzzy && local ? 1 : 0; - -const filteredLogs = [ - 'Running in dev mode', - 'Lit is in dev mode', - 'mwc-list-item scheduled an update', -]; - -const browsers = [ - playwrightLauncher({ product: 'chromium' }), - // playwrightLauncher({ product: 'firefox' }), - // playwrightLauncher({ product: 'webkit' }), - ]; - -function defaultGetImageDiff({ baselineImage, image, options }) { - let error = ''; - let basePng = PNG.sync.read(baselineImage); - let png = PNG.sync.read(image); - let { width, height } = png; - - if (basePng.width !== png.width || basePng.height !== png.height) { - error = - `Screenshot is not the same width and height as the baseline. ` + - `Baseline: { width: ${basePng.width}, height: ${basePng.height} }` + - `Screenshot: { width: ${png.width}, height: ${png.height} }`; - width = Math.max(basePng.width, png.width); - height = Math.max(basePng.height, png.height); - let oldPng = basePng; - basePng = new PNG({ width, height }); - oldPng.data.copy(basePng.data, 0, 0, oldPng.data.length); - oldPng = png; - png = new PNG({ width, height }); - oldPng.data.copy(png.data, 0, 0, oldPng.data.length); - } - - const diff = new PNG({ width, height }); - - const numDiffPixels = pixelmatch(basePng.data, png.data, diff.data, width, height, options); - const diffPercentage = (numDiffPixels / (width * height)) * 100; - - return { - error, - diffImage: PNG.sync.write(diff), - diffPercentage, - }; -} +// import { playwrightLauncher } from '@web/test-runner-playwright'; +import { esbuildPlugin } from '@web/dev-server-esbuild'; export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({ - plugins: [ - visualRegressionPlugin({ - update: process.argv.includes('--update-visual-baseline'), - getImageDiff: (options) => { - const result = defaultGetImageDiff(options); - if (result.diffPercentage < thresholdPercentage) - result.diffPercentage = 0; - return result; - } - }), - ], + /** we run test directly on TypeScript files */ + plugins: [esbuildPlugin({ ts: true })], - files: 'dist/**/*.spec.js', + /** Resolve bare module imports */ + nodeResolve: true, + + /** filter browser logs + * Plugins have a fix URL and do not fit to the file structure in test environment. + * Creating open-scd in the tests leads to error in the browser log - we had to disable the browser log + */ + browserLogs: false, + /** specify groups for unit and integrations tests + * hint: no --group definition runs all groups + */ groups: [ { - name: 'visual', - files: 'dist/**/*.test.js', - testRunnerHtml: testFramework => ` - - - - - - - - - - - - -`, + name: 'unit', + files: 'test/**/*.test.ts', }, ], - - /** Resolve bare module imports */ - nodeResolve: { - exportConditions: ['browser', 'development'], - }, - - /** Filter out lit dev mode logs */ - filterBrowserLogs(log) { - for (const arg of log.args) { - if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) { - return false; - } - } - return true; - }, - - /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */ - // esbuildTarget: 'auto', - - /** Amount of browsers to run concurrently */ - concurrentBrowsers: 3, - - /** Amount of test files per browser to test concurrently */ - concurrency: 2, - - /** Browsers to run tests on */ - browsers, - - // See documentation for all available options }); From 80af0b7bce7776763419e839b88bba6df31c955f Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 3 Nov 2025 15:40:07 +0100 Subject: [PATCH 12/14] test: Add tests --- packages/core/test/subject.test.ts | 2 +- packages/core/test/xml-editor.test.ts | 107 ++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/xml-editor.test.ts diff --git a/packages/core/test/subject.test.ts b/packages/core/test/subject.test.ts index 0eef5aacab..f72640966b 100644 --- a/packages/core/test/subject.test.ts +++ b/packages/core/test/subject.test.ts @@ -2,7 +2,7 @@ import { expect } from '@open-wc/testing'; import { Subject } from '../api/editor/subject.js'; -describe('Validators', () => { +describe('Subject', () => { let subject: Subject; let subOneValues: string[]; diff --git a/packages/core/test/xml-editor.test.ts b/packages/core/test/xml-editor.test.ts new file mode 100644 index 0000000000..9ee352d2de --- /dev/null +++ b/packages/core/test/xml-editor.test.ts @@ -0,0 +1,107 @@ +import { expect } from '@open-wc/testing'; +import { EditV2 } from '@openscd/oscd-api/dist/editv2.js'; + +import { OscdCommit, XMLEditor } from '../api/editor/xml-editor.js'; +import { RemoveV2 } from '../foundation.js'; + +describe('XMLEditor', () => { + let editor: XMLEditor; + let scd: XMLDocument; + let subscriberValues: OscdCommit[]; + + let substation: Element; + let voltageLevel: Element; + let bay1: Element; + + beforeEach(() => { + editor = new XMLEditor(); + + subscriberValues = []; + + scd = new DOMParser().parseFromString( + ` + + + + + + `, + 'application/xml' + ); + + substation = scd.querySelector('Substation')!; + voltageLevel = scd.querySelector('VoltageLevel')!; + bay1 = scd.querySelector('Bay')!; + }); + + it('should call subscriber on commit', () => { + editor.subscribe(c => subscriberValues.push(c as any)); + + const deleteBay: RemoveV2 = { + node: bay1 + }; + + editor.commit(deleteBay); + + const [ commit ] = subscriberValues; + expect(commit.redo).to.deep.equal([ deleteBay ]); + }); + + it('should set title in commit', () => { + const title = 'Important change'; + + const deleteBay: RemoveV2 = { + node: bay1 + }; + + editor.commit(deleteBay, { title }); + + const [ commit ] = editor.past; + expect(commit.title).to.equal(title); + }); + + it('should undo and redo changes', () => { + const deleteBay: RemoveV2 = { + node: bay1 + }; + + editor.commit(deleteBay); + + const bayAfterDelete = scd.querySelector('Bay'); + expect(bayAfterDelete).to.be.null; + + editor.undo(); + + const bayAfterUndo = scd.querySelector('Bay'); + expect(bayAfterUndo).to.equal(bay1); + + editor.redo(); + + const bayAfterRedo = scd.querySelector('Bay'); + expect(bayAfterRedo).to.be.null; + }); + + it('should call subscribers on undo and redo', () => { + const undos = []; + const redos = []; + + editor.subscribeUndo(c => undos.push(c)); + editor.subscribeRedo(c => redos.push(c)); + + const deleteBay: RemoveV2 = { + node: bay1 + }; + + editor.commit(deleteBay); + + editor.undo(); + + const [ lastUndo ] = undos; + expect(lastUndo.redo).to.deep.equal([ deleteBay ]); + + editor.redo(); + + const [ lastRedo ] = redos; + expect(lastRedo.redo).to.deep.equal([ deleteBay ]); + }); +}); From c6d987fd817cd0fab676309a0b2749bf84e41158 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 3 Nov 2025 15:59:21 +0100 Subject: [PATCH 13/14] chore: Fix build --- packages/core/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 5274920d1c..65e3040775 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -19,5 +19,6 @@ "tsBuildInfoFile": ".tsbuildinfo", "incremental": true }, - "include": ["**/*.ts"] + "include": ["**/*.ts"], + "exclude": ["**/*.test.ts"] } From 1e3d9c0b903cbf48dae4ec6ae1953d6b76f1d241 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Tue, 4 Nov 2025 16:47:19 +0100 Subject: [PATCH 14/14] chore: Remove unneeded code --- packages/openscd/src/addons/History.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index b7b0cc9340..e1072fe1a4 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -210,7 +210,6 @@ export class OscdHistory extends LitElement { break; case 'action': // No longer needed - // this.onHistory(le.detail); break; default: this.onInfo(le.detail);