@omicronenergy/oscd-shell 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/foundation/cyrb64.d.ts +6 -0
  2. package/dist/foundation/cyrb64.js +26 -0
  3. package/dist/foundation/cyrb64.js.map +1 -0
  4. package/dist/foundation.d.ts +1 -0
  5. package/dist/foundation.js +2 -0
  6. package/dist/foundation.js.map +1 -0
  7. package/dist/locales/de.d.ts +10 -0
  8. package/dist/locales/de.js +15 -0
  9. package/dist/locales/de.js.map +1 -0
  10. package/dist/locales.d.ts +13 -0
  11. package/dist/locales.js +21 -0
  12. package/dist/locales.js.map +1 -0
  13. package/dist/oscd-shell.d.ts +115 -0
  14. package/dist/oscd-shell.js +732 -0
  15. package/dist/oscd-shell.js.map +1 -0
  16. package/package.json +32 -29
  17. package/.editorconfig +0 -22
  18. package/.github/actions/setup-node-env/action.yml +0 -49
  19. package/.github/workflows/release-please.yml +0 -84
  20. package/.github/workflows/test.yml +0 -18
  21. package/.github/workflows/update-screenshots.yml +0 -29
  22. package/.husky/commit-msg +0 -1
  23. package/.husky/pre-commit +0 -1
  24. package/.release-please-manifest.json +0 -1
  25. package/CHANGELOG.md +0 -73
  26. package/CONTRIBUTING.md +0 -393
  27. package/commitlint.config.js +0 -1
  28. package/demo/AddPlugins.js +0 -42
  29. package/demo/DemoPluginSrc.js +0 -9
  30. package/demo/embedded.html +0 -17
  31. package/demo/index.html +0 -95
  32. package/foundation/cyrb64.ts +0 -27
  33. package/foundation/edit-event.ts +0 -273
  34. package/foundation/open-event.ts +0 -21
  35. package/foundation.ts +0 -28
  36. package/lit-localize.json +0 -15
  37. package/localization/de.xlf +0 -75
  38. package/oscd-shell.editing.spec.ts +0 -813
  39. package/oscd-shell.plugging.spec.ts +0 -78
  40. package/oscd-shell.spec.ts +0 -431
  41. package/oscd-shell.test.ts +0 -382
  42. package/oscd-shell.ts +0 -906
  43. package/release-please-config.json +0 -21
  44. package/rollup.config.js +0 -51
  45. package/screenshots/Chromium/baseline/app-bar-de.png +0 -0
  46. package/screenshots/Chromium/baseline/app-bar-en.png +0 -0
  47. package/screenshots/Chromium/baseline/document-name-de.png +0 -0
  48. package/screenshots/Chromium/baseline/document-name-en.png +0 -0
  49. package/screenshots/Chromium/baseline/editor-plugins-de.png +0 -0
  50. package/screenshots/Chromium/baseline/editor-plugins-en.png +0 -0
  51. package/screenshots/Chromium/baseline/editor-plugins-selected-de.png +0 -0
  52. package/screenshots/Chromium/baseline/editor-plugins-selected-en.png +0 -0
  53. package/screenshots/Chromium/baseline/editor-plugins-with-doc-de.png +0 -0
  54. package/screenshots/Chromium/baseline/editor-plugins-with-doc-en.png +0 -0
  55. package/screenshots/Chromium/baseline/log-entries-de.png +0 -0
  56. package/screenshots/Chromium/baseline/log-entries-en.png +0 -0
  57. package/screenshots/Chromium/baseline/log-entries-redone-de.png +0 -0
  58. package/screenshots/Chromium/baseline/log-entries-redone-en.png +0 -0
  59. package/screenshots/Chromium/baseline/log-entries-undone-de.png +0 -0
  60. package/screenshots/Chromium/baseline/log-entries-undone-en.png +0 -0
  61. package/screenshots/Chromium/baseline/log-screen-de.png +0 -0
  62. package/screenshots/Chromium/baseline/log-screen-en.png +0 -0
  63. package/screenshots/Chromium/baseline/menu-drawer-de.png +0 -0
  64. package/screenshots/Chromium/baseline/menu-drawer-en.png +0 -0
  65. package/screenshots/Chromium/baseline/menu-plugins-de.png +0 -0
  66. package/screenshots/Chromium/baseline/menu-plugins-en.png +0 -0
  67. package/screenshots/Chromium/baseline/menu-plugins-triggered-de.png +0 -0
  68. package/screenshots/Chromium/baseline/menu-plugins-triggered-en.png +0 -0
  69. package/screenshots/Firefox/baseline/app-bar-de.png +0 -0
  70. package/screenshots/Firefox/baseline/app-bar-en.png +0 -0
  71. package/screenshots/Firefox/baseline/document-name-de.png +0 -0
  72. package/screenshots/Firefox/baseline/document-name-en.png +0 -0
  73. package/screenshots/Firefox/baseline/editor-plugins-de.png +0 -0
  74. package/screenshots/Firefox/baseline/editor-plugins-en.png +0 -0
  75. package/screenshots/Firefox/baseline/editor-plugins-selected-de.png +0 -0
  76. package/screenshots/Firefox/baseline/editor-plugins-selected-en.png +0 -0
  77. package/screenshots/Firefox/baseline/editor-plugins-with-doc-de.png +0 -0
  78. package/screenshots/Firefox/baseline/editor-plugins-with-doc-en.png +0 -0
  79. package/screenshots/Firefox/baseline/log-entries-de.png +0 -0
  80. package/screenshots/Firefox/baseline/log-entries-en.png +0 -0
  81. package/screenshots/Firefox/baseline/log-entries-redone-de.png +0 -0
  82. package/screenshots/Firefox/baseline/log-entries-redone-en.png +0 -0
  83. package/screenshots/Firefox/baseline/log-entries-undone-de.png +0 -0
  84. package/screenshots/Firefox/baseline/log-entries-undone-en.png +0 -0
  85. package/screenshots/Firefox/baseline/log-screen-de.png +0 -0
  86. package/screenshots/Firefox/baseline/log-screen-en.png +0 -0
  87. package/screenshots/Firefox/baseline/menu-drawer-de.png +0 -0
  88. package/screenshots/Firefox/baseline/menu-drawer-en.png +0 -0
  89. package/screenshots/Firefox/baseline/menu-plugins-de.png +0 -0
  90. package/screenshots/Firefox/baseline/menu-plugins-en.png +0 -0
  91. package/screenshots/Firefox/baseline/menu-plugins-triggered-de.png +0 -0
  92. package/screenshots/Firefox/baseline/menu-plugins-triggered-en.png +0 -0
  93. package/screenshots/Webkit/baseline/app-bar-de.png +0 -0
  94. package/screenshots/Webkit/baseline/app-bar-en.png +0 -0
  95. package/screenshots/Webkit/baseline/document-name-de.png +0 -0
  96. package/screenshots/Webkit/baseline/document-name-en.png +0 -0
  97. package/screenshots/Webkit/baseline/editor-plugins-de.png +0 -0
  98. package/screenshots/Webkit/baseline/editor-plugins-en.png +0 -0
  99. package/screenshots/Webkit/baseline/editor-plugins-selected-de.png +0 -0
  100. package/screenshots/Webkit/baseline/editor-plugins-selected-en.png +0 -0
  101. package/screenshots/Webkit/baseline/editor-plugins-with-doc-de.png +0 -0
  102. package/screenshots/Webkit/baseline/editor-plugins-with-doc-en.png +0 -0
  103. package/screenshots/Webkit/baseline/log-entries-de.png +0 -0
  104. package/screenshots/Webkit/baseline/log-entries-en.png +0 -0
  105. package/screenshots/Webkit/baseline/log-entries-redone-de.png +0 -0
  106. package/screenshots/Webkit/baseline/log-entries-redone-en.png +0 -0
  107. package/screenshots/Webkit/baseline/log-entries-undone-de.png +0 -0
  108. package/screenshots/Webkit/baseline/log-entries-undone-en.png +0 -0
  109. package/screenshots/Webkit/baseline/log-screen-de.png +0 -0
  110. package/screenshots/Webkit/baseline/log-screen-en.png +0 -0
  111. package/screenshots/Webkit/baseline/menu-drawer-de.png +0 -0
  112. package/screenshots/Webkit/baseline/menu-drawer-en.png +0 -0
  113. package/screenshots/Webkit/baseline/menu-plugins-de.png +0 -0
  114. package/screenshots/Webkit/baseline/menu-plugins-en.png +0 -0
  115. package/screenshots/Webkit/baseline/menu-plugins-triggered-de.png +0 -0
  116. package/screenshots/Webkit/baseline/menu-plugins-triggered-en.png +0 -0
  117. package/tsconfig.json +0 -23
  118. package/utils/testing.spec.ts +0 -210
  119. package/utils/testing.ts +0 -65
  120. package/web-dev-server.config.js +0 -24
  121. package/web-test-runner.config.js +0 -180
@@ -1,813 +0,0 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
2
-
3
- import {
4
- Arbitrary,
5
- array,
6
- assert,
7
- constant,
8
- constantFrom,
9
- dictionary,
10
- object as objectArbitrary,
11
- oneof,
12
- property,
13
- record,
14
- string as stringArbitrary,
15
- tuple,
16
- webUrl,
17
- } from 'fast-check';
18
-
19
- import { OscdFilledSelect } from '@omicronenergy/oscd-ui/select/oscd-filled-select.js';
20
- import { OscdFilledTextField } from '@omicronenergy/oscd-ui/textfield/oscd-filled-text-field.js';
21
- import { OscdListItem } from '@omicronenergy/oscd-ui/list/OscdListItem.js';
22
-
23
- import { OscdMenuItem } from '@omicronenergy/oscd-ui/menu/OscdMenuItem.js';
24
- import { OscdTextButton } from '@omicronenergy/oscd-ui/button/OscdTextButton.js';
25
-
26
- import {
27
- Edit,
28
- Insert,
29
- isNamespaced,
30
- NamespacedAttributeValue,
31
- newEditEvent,
32
- newOpenEvent,
33
- Remove,
34
- Update,
35
- } from './foundation.js';
36
- import {
37
- findButtonByIcon,
38
- querySelectorContainingText,
39
- simulateKeypressOnElement,
40
- waitForDialogState,
41
- } from './utils/testing.js';
42
-
43
- import type { OpenSCD } from './oscd-shell.js';
44
-
45
- import './oscd-shell.js';
46
- import { UpdateNS, Value } from './foundation/edit-event.js';
47
-
48
- export namespace util {
49
- export const xmlAttributeName =
50
- /^(?!xml|Xml|xMl|xmL|XMl|xML|XmL|XML)[A-Za-z_][A-Za-z0-9-_.]*(:[A-Za-z_][A-Za-z0-9-_.]*)?$/;
51
-
52
- export function descendants(parent: Element | XMLDocument): Node[] {
53
- return (Array.from(parent.childNodes) as Node[]).concat(
54
- ...Array.from(parent.children).map(child => descendants(child)),
55
- );
56
- }
57
-
58
- export const sclDocString = `<?xml version="1.0" encoding="UTF-8"?>
59
- <SCL version="2007" revision="B" xmlns="http://www.iec.ch/61850/2003/SCL" xmlns:ens1="http://example.org/somePreexistingExtensionNamespace">
60
- <Substation ens1:foo="a" name="A1" desc="test substation"></Substation>
61
- </SCL>`;
62
- export const testDocStrings = [
63
- sclDocString,
64
- `<?xml version="1.0" encoding="UTF-8"?>
65
- <testDoc1>
66
- <element1 property1="value1" property2="value2">SomeText</element1>
67
- <element2 property2="value2" property3="value3"><!--AComment--></element2>
68
- <element3 property3="value3" property1="value1">
69
- <subelement1 property1="value1" property2="value2">SomeMoreText</subelement1>
70
- <subelement2 property2="value2" property3="value3"><!----></subelement2>
71
- <subelement3 property3="value3" property1="value1"></subelement3>
72
- </element3>
73
- </testDoc1>`,
74
- `<?xml version="1.0" encoding="UTF-8"?>
75
- <testDoc2>
76
- <element1 property1="value1" property2="value2">SomeText</element1>
77
- <element2 property2="value2" property3="value3"><!--AComment--></element2>
78
- <element3 property3="value3" property1="value1">
79
- <subelement1 property1="value1" property2="value2">SomeMoreText</subelement1>
80
- <subelement2 property2="value2" property3="value3"><!----></subelement2>
81
- <subelement3 property3="value3" property1="value1"></subelement3>
82
- </element3>
83
- </testDoc2>`,
84
- ];
85
-
86
- export type TestDoc = { doc: XMLDocument; nodes: Node[] };
87
- export const testDocs = tuple(
88
- constantFrom(...testDocStrings),
89
- constantFrom(...testDocStrings),
90
- )
91
- .map(strs =>
92
- strs.map(str => new DOMParser().parseFromString(str, 'application/xml')),
93
- )
94
- .map(docs =>
95
- docs.map(doc => ({ doc, nodes: descendants(doc).concat([doc]) })),
96
- ) as Arbitrary<[TestDoc, TestDoc]>;
97
-
98
- export function remove(nodes: Node[]): Arbitrary<Remove> {
99
- const node = oneof(
100
- { arbitrary: constantFrom(...nodes), weight: nodes.length },
101
- testDocs.chain(docs => constantFrom(...docs.map(d => d.doc))),
102
- );
103
- return record({ node });
104
- }
105
-
106
- export function insert(nodes: Node[]): Arbitrary<Insert> {
107
- const references = (nodes as (Node | null)[]).concat([null]);
108
- const parent = constantFrom(...nodes);
109
- const node = constantFrom(...nodes);
110
- const reference = constantFrom(...references);
111
- return record({ parent, node, reference });
112
- }
113
-
114
- export function update(nodes: Node[]): Arbitrary<Update> {
115
- const element = <Arbitrary<Element>>(
116
- constantFrom(...nodes.filter(nd => nd.nodeType === Node.ELEMENT_NODE))
117
- );
118
- const attributes = dictionary(
119
- oneof(stringArbitrary(), constant('colliding-attribute-name')),
120
- oneof(stringArbitrary(), constant(null)),
121
- );
122
- return record({ element, attributes });
123
- }
124
-
125
- export function updateNS(nodes: Node[]): Arbitrary<UpdateNS> {
126
- const element = <Arbitrary<Element>>(
127
- constantFrom(...nodes.filter(nd => nd.nodeType === Node.ELEMENT_NODE))
128
- );
129
- const attributes = dictionary(
130
- stringArbitrary(),
131
- oneof(stringArbitrary(), constant(null)),
132
- );
133
- // object() instead of nested dictionary() necessary for performance reasons
134
- const attributesNS = objectArbitrary({
135
- key: webUrl(),
136
- values: [stringArbitrary(), constant(null)],
137
- maxDepth: 1,
138
- }).map(
139
- aNS =>
140
- Object.fromEntries(
141
- Object.entries(aNS).filter(
142
- ([_, attrs]) => attrs && !(typeof attrs === 'string'),
143
- ),
144
- ) as Partial<Record<string, Partial<Record<string, Value>>>>,
145
- );
146
- return record({ element, attributes, attributesNS });
147
- }
148
-
149
- export function simpleEdit(
150
- nodes: Node[],
151
- ): Arbitrary<Insert | Update | Remove> {
152
- return oneof(remove(nodes), insert(nodes), update(nodes), updateNS(nodes));
153
- }
154
-
155
- export function complexEdit(nodes: Node[]): Arbitrary<Edit[]> {
156
- return array(simpleEdit(nodes));
157
- }
158
-
159
- export function edit(nodes: Node[]): Arbitrary<Edit> {
160
- return oneof(
161
- { arbitrary: simpleEdit(nodes), weight: 2 },
162
- complexEdit(nodes),
163
- );
164
- }
165
-
166
- /** A series of arbitrary edits that allow us to test undo and redo */
167
- export type UndoRedoTestCase = {
168
- doc1: XMLDocument;
169
- doc2: XMLDocument;
170
- edits: Edit[];
171
- };
172
- export function undoRedoTestCases(
173
- testDoc1: TestDoc,
174
- testDoc2: TestDoc,
175
- ): Arbitrary<UndoRedoTestCase> {
176
- const nodes = testDoc1.nodes.concat(testDoc2.nodes);
177
- return record({
178
- doc1: constant(testDoc1.doc),
179
- doc2: constant(testDoc2.doc),
180
- edits: array(edit(nodes)),
181
- });
182
- }
183
-
184
- export function isParentNode(node: Node): node is ParentNode {
185
- return (
186
- node instanceof Element ||
187
- node instanceof Document ||
188
- node instanceof DocumentFragment
189
- );
190
- }
191
-
192
- export function isParentOf(parent: Node, node: Node | null) {
193
- return (
194
- isParentNode(parent) &&
195
- (node === null ||
196
- Array.from(parent.childNodes).includes(node as ChildNode))
197
- );
198
- }
199
-
200
- export function isValidInsert({ parent, node, reference }: Insert) {
201
- return (
202
- node !== reference &&
203
- isParentOf(parent, reference) &&
204
- !node.contains(parent) &&
205
- ![Node.DOCUMENT_NODE, Node.DOCUMENT_TYPE_NODE].some(
206
- nodeType => node.nodeType === nodeType,
207
- ) &&
208
- !(
209
- parent instanceof Document &&
210
- (parent.documentElement || !(node instanceof Element))
211
- )
212
- );
213
- }
214
- }
215
-
216
- function newTestDoc() {
217
- const docString =
218
- util.testDocStrings[Math.floor(Math.random() * util.testDocStrings.length)];
219
- return new DOMParser().parseFromString(docString, 'application/xml');
220
- }
221
-
222
- describe('oscd-shell', () => {
223
- let editor: OpenSCD;
224
- let sclDoc: XMLDocument;
225
-
226
- beforeEach(async () => {
227
- editor = <OpenSCD>await fixture(html`<oscd-shell></oscd-shell>`);
228
- sclDoc = new DOMParser().parseFromString(
229
- util.sclDocString,
230
- 'application/xml',
231
- );
232
- editor.logUI.quick = true; // enable quick mode for faster tests
233
- });
234
-
235
- it('loads a non-SCL document on OpenDocEvent', async () => {
236
- editor.dispatchEvent(newOpenEvent(sclDoc, 'test.xml'));
237
- await editor.updateComplete;
238
- expect(editor.docs).to.have.property('test.xml', sclDoc);
239
- expect(editor).to.have.property('doc', undefined);
240
- expect(editor).to.not.have.property('docName', 'test.xml');
241
- });
242
-
243
- it('opens an SCL document for editing on OpenDocEvent', async () => {
244
- editor.dispatchEvent(newOpenEvent(sclDoc, 'test.scd'));
245
- await editor.updateComplete;
246
- expect(editor.docs).to.have.property('test.scd', sclDoc);
247
- expect(editor).to.have.property('doc', sclDoc);
248
- expect(editor).to.have.property('docName', 'test.scd');
249
- });
250
-
251
- describe('with an SCL document loaded', () => {
252
- beforeEach(async () => {
253
- editor.dispatchEvent(newOpenEvent(sclDoc, 'test.scd'));
254
- await editor.updateComplete;
255
- });
256
-
257
- it('updates the UI when a document with the same name is opened', async () => {
258
- const newDoc = newTestDoc();
259
- const oldUpdate = editor.updateComplete;
260
- editor.dispatchEvent(newOpenEvent(newDoc, 'test.scd'));
261
- expect(oldUpdate).to.not.equal(editor.updateComplete);
262
- });
263
-
264
- it('allows the user to change the current doc name', async () => {
265
- findButtonByIcon(
266
- editor.shadowRoot!,
267
- 'oscd-filled-icon-button',
268
- 'edit',
269
- )?.click();
270
- const dialog = editor.editFileUI;
271
- await dialog.updateComplete;
272
- const textfield = dialog.querySelector<OscdFilledTextField>(
273
- 'oscd-filled-text-field',
274
- )!;
275
- textfield.value = 'newName';
276
- const select = dialog.querySelector(
277
- 'oscd-filled-select',
278
- )! as OscdFilledSelect;
279
- select.value = 'cid';
280
- await textfield.updateComplete;
281
- await select.updateComplete;
282
- findButtonByIcon(dialog, 'oscd-text-button', 'edit')?.click();
283
- await editor.updateComplete;
284
- expect(editor).to.have.property('docName', 'newName.cid');
285
- expect(editor).to.have.property('doc', sclDoc);
286
- });
287
-
288
- it('allows the user to close the current doc', async () => {
289
- const currentDocName = editor.docName;
290
- const editButton = findButtonByIcon(
291
- editor.shadowRoot!,
292
- 'oscd-filled-icon-button',
293
- 'edit',
294
- );
295
- expect(editButton).to.exist;
296
- editButton?.click();
297
-
298
- const dialog = editor.editFileUI;
299
- await dialog.updateComplete;
300
- const deleteButton = findButtonByIcon(
301
- dialog,
302
- 'oscd-text-button',
303
- 'delete',
304
- );
305
- expect(deleteButton).to.exist;
306
- deleteButton?.click();
307
-
308
- await new Promise<void>(resolve => {
309
- dialog.addEventListener('closed', () => resolve(), { once: true });
310
- });
311
-
312
- await editor.updateComplete;
313
- expect(editor.docName).to.not.equal(currentDocName);
314
- expect(editor.doc).to.be.undefined;
315
- });
316
- });
317
-
318
- describe('with several documents loaded', () => {
319
- beforeEach(async () => {
320
- for (let i = 0; i < 5; i += 1) {
321
- editor.dispatchEvent(newOpenEvent(newTestDoc(), `test${i}.scd`));
322
- }
323
- await editor.updateComplete;
324
- });
325
-
326
- it('allows the user to switch documents', async () => {
327
- editor.fileMenuButtonUI?.click();
328
- await editor.fileMenuUI.updateComplete;
329
- (editor.fileMenuUI.firstElementChild as OscdMenuItem).click();
330
- await editor.updateComplete;
331
- const oldDocName = editor.docName;
332
- editor.fileMenuButtonUI?.click();
333
- await editor.fileMenuUI.updateComplete;
334
- (editor.fileMenuUI.lastElementChild as OscdMenuItem).click();
335
- await editor.updateComplete;
336
- expect(editor).to.not.have.property('docName', oldDocName);
337
- });
338
-
339
- it('prevents the user from renaming the current doc to an already opened doc name', async () => {
340
- const anotherDocNameWithoutExtension = Object.keys(editor.docs)
341
- ?.find(docName => docName !== editor.docName)
342
- ?.split('.')[0];
343
- const existingDocNameWithExtension = editor.docName;
344
- findButtonByIcon(
345
- editor.shadowRoot!,
346
- 'oscd-app-bar oscd-filled-icon-button',
347
- 'edit',
348
- )?.click();
349
- const dialog = editor.editFileUI;
350
- await dialog.updateComplete;
351
- const textfield = dialog.querySelector<OscdFilledTextField>('#fileName')!;
352
- textfield.value = anotherDocNameWithoutExtension!;
353
- textfield.dispatchEvent(
354
- new Event('input', { bubbles: true, composed: true }),
355
- );
356
- await textfield.updateComplete;
357
- const renameButton = findButtonByIcon(dialog, 'oscd-text-button', 'edit');
358
- expect(renameButton).to.exist;
359
- renameButton!.click();
360
- await editor.updateComplete;
361
- expect(editor.editFileUI.open).to.be.true;
362
-
363
- const [filename] = existingDocNameWithExtension.split('.');
364
- textfield.value = filename;
365
- textfield.dispatchEvent(
366
- new Event('input', { bubbles: true, composed: true }),
367
- );
368
- await editor.updateComplete;
369
- renameButton!.click();
370
- await editor.updateComplete;
371
- await waitForDialogState(editor.editFileUI, 'closed');
372
- expect(editor.editFileUI.open).not.to.be.true;
373
- expect(editor).to.have.property('docName', existingDocNameWithExtension);
374
- });
375
- });
376
-
377
- it('inserts an element on Insert', () => {
378
- const parent = sclDoc.documentElement;
379
- const node = sclDoc.createElement('test');
380
- const reference = sclDoc.querySelector('Substation');
381
- editor.dispatchEvent(newEditEvent({ parent, node, reference }));
382
- expect(sclDoc.documentElement.querySelector('test')).to.have.property(
383
- 'nextSibling',
384
- reference,
385
- );
386
- });
387
-
388
- it('removes an element on Remove', () => {
389
- const node = sclDoc.querySelector('Substation')!;
390
- editor.dispatchEvent(newEditEvent({ node }));
391
- expect(sclDoc.querySelector('Substation')).to.not.exist;
392
- });
393
-
394
- it("updates an element's attributes on Update", () => {
395
- const element = sclDoc.querySelector('Substation')!;
396
- editor.dispatchEvent(
397
- newEditEvent({
398
- element,
399
- attributes: {
400
- name: 'A2',
401
- desc: null,
402
- ['__proto__']: 'a string', // covers a rare edge case branch
403
- 'ens1:attr': {
404
- value: 'namespaced value',
405
- namespaceURI: 'http://example.org/myns',
406
- },
407
- attr2: {
408
- value: 'namespaced value',
409
- namespaceURI: 'http://example.org/myns',
410
- },
411
- 'ens1:foo': 'namespaced value set with a string not an object',
412
- },
413
- }),
414
- );
415
- expect(element).to.have.attribute('name', 'A2');
416
- expect(element).to.not.have.attribute('desc');
417
- expect(element).to.have.attribute('__proto__', 'a string');
418
- expect(element).to.have.attribute('ens1:attr', 'namespaced value');
419
- });
420
-
421
- it("updates an element's attributes on UpdateNS", () => {
422
- const element = sclDoc.querySelector('Substation')!;
423
- editor.dispatchEvent(
424
- newEditEvent({
425
- element,
426
- attributes: {
427
- name: 'A2',
428
- desc: null,
429
- ['__proto__']: 'a string', // covers a rare edge case branch
430
- },
431
- attributesNS: {
432
- 'http://example.org/myns': {
433
- 'myns:attr': 'value1',
434
- 'myns:attr2': 'value1',
435
- },
436
- 'http://example.org/myns2': {
437
- attr: 'value2',
438
- attr2: 'value2',
439
- },
440
- 'http://example.org/myns3': {
441
- attr: 'value3',
442
- attr2: 'value3',
443
- },
444
- },
445
- }),
446
- );
447
- expect(element).to.have.attribute('name', 'A2');
448
- expect(element).to.not.have.attribute('desc');
449
- expect(element).to.have.attribute('__proto__', 'a string');
450
- expect(element).to.have.attribute('myns:attr', 'value1');
451
- expect(element).to.have.attribute('myns:attr2', 'value1');
452
- expect(element).to.have.attribute('ens2:attr', 'value2');
453
- expect(element).to.have.attribute('ens2:attr2', 'value2');
454
- expect(element).to.have.attribute('ens3:attr', 'value3');
455
- expect(element).to.have.attribute('ens3:attr2', 'value3');
456
- });
457
-
458
- it('processes complex edits in the given order', () => {
459
- const parent = sclDoc.documentElement;
460
- const reference = sclDoc.querySelector('Substation');
461
- const node1 = sclDoc.createElement('test1');
462
- const node2 = sclDoc.createElement('test2');
463
- editor.dispatchEvent(
464
- newEditEvent([
465
- { parent, node: node1, reference },
466
- { parent, node: node2, reference },
467
- ]),
468
- );
469
- expect(sclDoc.documentElement.querySelector('test1')).to.have.property(
470
- 'nextSibling',
471
- node2,
472
- );
473
- expect(sclDoc.documentElement.querySelector('test2')).to.have.property(
474
- 'nextSibling',
475
- reference,
476
- );
477
- });
478
-
479
- it('undoes a edit from Undo menu option', async () => {
480
- const node = sclDoc.querySelector('Substation')!;
481
- editor.dispatchEvent(newEditEvent({ node }));
482
-
483
- const navMenuButton = findButtonByIcon(
484
- editor.shadowRoot!,
485
- 'oscd-filled-icon-button',
486
- 'menu',
487
- );
488
- expect(navMenuButton).to.exist;
489
- navMenuButton?.click();
490
-
491
- await editor.updateComplete;
492
- const undoButton = querySelectorContainingText(
493
- editor.menuUI,
494
- 'oscd-list-item > span',
495
- 'Undo',
496
- )?.closest('oscd-list-item') as OscdListItem;
497
- expect(undoButton).to.exist;
498
- expect(undoButton).to.have.property('disabled', false);
499
- undoButton?.click();
500
- await editor.updateComplete;
501
- expect(sclDoc.querySelector('Substation')).to.exist;
502
- });
503
-
504
- it('redoes an undone edit from Redo menu option', async () => {
505
- const node = sclDoc.querySelector('Substation')!;
506
- editor.dispatchEvent(newEditEvent({ node }));
507
- expect(sclDoc.querySelector('Substation')).to.not.exist;
508
- editor.undo();
509
- await editor.updateComplete;
510
- expect(sclDoc.querySelector('Substation')).to.exist;
511
- const redoButton = querySelectorContainingText(
512
- editor.menuUI,
513
- 'oscd-list-item > span',
514
- 'Redo',
515
- )?.closest('oscd-list-item') as OscdListItem;
516
- expect(redoButton).to.exist;
517
- expect(redoButton).to.have.property('disabled', false);
518
- redoButton?.click();
519
- await editor.updateComplete;
520
-
521
- expect(sclDoc.querySelector('Substation')).to.not.exist;
522
- });
523
-
524
- describe('use the keyboard shortcuts', () => {
525
- it('displays the menu with Ctrl+m', async () => {
526
- simulateKeypressOnElement('m', true);
527
- await editor.updateComplete;
528
- expect(editor.menuUI).to.have.property('opened');
529
- });
530
-
531
- it('undoes the last edit with Ctrl+z', async () => {
532
- const node = sclDoc.querySelector('Substation')!;
533
- editor.dispatchEvent(newEditEvent({ node }));
534
- await editor.updateComplete;
535
-
536
- expect(sclDoc.querySelector('Substation')).to.not.exist;
537
- simulateKeypressOnElement('z', true);
538
- await editor.updateComplete;
539
- expect(sclDoc.querySelector('Substation')).to.exist;
540
- });
541
-
542
- it('redoes the last edit with Ctrl+y', async () => {
543
- const node = sclDoc.querySelector('Substation')!;
544
- editor.dispatchEvent(newEditEvent({ node }));
545
- expect(sclDoc.querySelector('Substation')).to.not.exist;
546
- editor.undo();
547
- await editor.updateComplete;
548
- expect(sclDoc.querySelector('Substation')).to.exist;
549
- simulateKeypressOnElement('y', true);
550
- await editor.updateComplete;
551
- expect(sclDoc.querySelector('Substation')).to.not.exist;
552
- });
553
-
554
- it('it redoes the last edit with Ctrl+Z', async () => {
555
- const node = sclDoc.querySelector('Substation')!;
556
- editor.dispatchEvent(newEditEvent({ node }));
557
- expect(sclDoc.querySelector('Substation')).to.not.exist;
558
- editor.undo();
559
- await editor.updateComplete;
560
- expect(sclDoc.querySelector('Substation')).to.exist;
561
- simulateKeypressOnElement('Z', true);
562
- await editor.updateComplete;
563
- expect(sclDoc.querySelector('Substation')).to.not.exist;
564
- });
565
-
566
- it('displays the logs dialog on Ctrl+l', async () => {
567
- simulateKeypressOnElement('l', true);
568
- await editor.updateComplete;
569
- await waitForDialogState(editor.logUI, 'open');
570
- expect(editor.logUI).to.have.attribute('open');
571
- const closeButton = editor.logUI?.querySelector(
572
- 'oscd-text-button[value="close"]',
573
- ) as OscdTextButton;
574
- expect(closeButton).to.exist;
575
- closeButton?.click();
576
- await editor.updateComplete;
577
- await waitForDialogState(editor.logUI, 'closed');
578
- expect(editor.logUI).to.not.have.attribute('open');
579
- });
580
-
581
- it('closes the dialog on Ctrl+l', async () => {
582
- await editor.logUI.show();
583
- await waitForDialogState(editor.logUI, 'open');
584
- expect(editor.logUI).to.have.attribute('open');
585
- simulateKeypressOnElement('l', true);
586
- await waitForDialogState(editor.logUI, 'closed');
587
- await editor.updateComplete;
588
- expect(editor.logUI).to.not.have.attribute('open');
589
- });
590
-
591
- it('does not trigger anything if the Ctrl button was not pressed', async () => {
592
- simulateKeypressOnElement('m', false);
593
- await editor.updateComplete;
594
- expect(editor.menuUI).to.not.have.attribute('open');
595
- expect(editor.logUI).to.not.have.attribute('open');
596
- });
597
-
598
- it('does not trigger anything if the Ctrl button was pressed but the key was not one of the shortcuts', async () => {
599
- simulateKeypressOnElement('a', true);
600
- await editor.updateComplete;
601
- expect(editor.menuUI).to.not.have.attribute('open');
602
- expect(editor.logUI).to.not.have.attribute('open');
603
- });
604
- });
605
-
606
- describe('with the editing history dialog open', () => {
607
- beforeEach(async () => {
608
- editor.logUI.show();
609
- await editor.updateComplete;
610
- });
611
-
612
- it('displays the edit history', async () => {
613
- const historyItemsCount =
614
- editor.logUI?.querySelectorAll('oscd-list > abbr').length;
615
- const node = sclDoc.querySelector('Substation')!;
616
- editor.dispatchEvent(newEditEvent({ node }));
617
- await editor.updateComplete;
618
-
619
- expect(
620
- editor.logUI?.querySelectorAll('oscd-list > abbr'),
621
- ).to.have.lengthOf(historyItemsCount! + 1);
622
- });
623
-
624
- it('undoes the last edit when clicking the undo button in the log dialog', async () => {
625
- const node = sclDoc.querySelector('Substation')!;
626
- editor.dispatchEvent(newEditEvent({ node }));
627
- await editor.updateComplete;
628
- const undoButton = editor.logUI?.querySelector(
629
- 'oscd-text-button[value="undo"]',
630
- ) as HTMLButtonElement;
631
- expect(undoButton).to.exist;
632
- expect(undoButton).to.have.property('disabled', false);
633
- undoButton?.click();
634
- await editor.updateComplete;
635
- expect(sclDoc.querySelector('Substation')).to.exist;
636
- });
637
-
638
- it('redoes the last undone when clicking the redo button in the log dialog', async () => {
639
- const node = sclDoc.querySelector('Substation')!;
640
- editor.dispatchEvent(newEditEvent({ node }));
641
- await editor.updateComplete;
642
- editor.undo();
643
- await editor.updateComplete;
644
- expect(sclDoc.querySelector('Substation')).to.exist;
645
- const redoButton = editor.logUI?.querySelector(
646
- 'oscd-text-button[value="redo"]',
647
- ) as HTMLButtonElement;
648
- expect(redoButton).to.exist;
649
- expect(redoButton).to.have.property('disabled', false);
650
- redoButton?.click();
651
- await editor.updateComplete;
652
- expect(sclDoc.querySelector('Substation')).to.not.exist;
653
- });
654
- });
655
-
656
- describe('generally', () => {
657
- it('inserts elements on Insert edit events', () =>
658
- assert(
659
- property(
660
- util.testDocs.chain(([doc1, doc2]) => {
661
- const nodes = doc1.nodes.concat(doc2.nodes);
662
- return util.insert(nodes);
663
- }),
664
- edit => {
665
- editor.dispatchEvent(newEditEvent(edit));
666
- if (util.isValidInsert(edit)) {
667
- return (
668
- edit.node.parentElement === edit.parent &&
669
- edit.node.nextSibling === edit.reference
670
- );
671
- }
672
- return true;
673
- },
674
- ),
675
- ));
676
-
677
- it('updates default namespace attributes on Update edit events', () =>
678
- assert(
679
- property(
680
- util.testDocs.chain(([{ nodes }]) => util.update(nodes)),
681
- edit => {
682
- editor.dispatchEvent(newEditEvent(edit));
683
- return Object.entries(edit.attributes)
684
- .filter(
685
- ([name, value]) =>
686
- util.xmlAttributeName.test(name) && !isNamespaced(value!),
687
- )
688
- .every(
689
- ([name, value]) => edit.element.getAttribute(name) === value,
690
- );
691
- },
692
- ),
693
- ));
694
-
695
- it('updates namespaced attributes on Update edit events', () =>
696
- assert(
697
- property(
698
- util.testDocs.chain(([{ nodes }]) => util.update(nodes)),
699
- edit => {
700
- editor.dispatchEvent(newEditEvent(edit));
701
- return Object.entries(edit.attributes)
702
- .filter(
703
- ([name, value]) =>
704
- util.xmlAttributeName.test(name) &&
705
- isNamespaced(value!) &&
706
- value.namespaceURI,
707
- )
708
- .map(entry => entry as [string, NamespacedAttributeValue])
709
- .every(
710
- ([name, { value, namespaceURI }]) =>
711
- edit.element.getAttributeNS(
712
- <string>namespaceURI,
713
- name.includes(':') ? <string>name.split(':', 2)[1] : name,
714
- ) === value,
715
- );
716
- },
717
- ),
718
- ));
719
-
720
- it('updates default- and foreign-namespace attributes on UpdateNS events', () =>
721
- assert(
722
- property(
723
- util.testDocs.chain(([{ nodes }]) => util.updateNS(nodes)),
724
- edit => {
725
- editor.dispatchEvent(newEditEvent(edit));
726
- return (
727
- Object.entries(edit.attributes)
728
- .filter(([name]) => util.xmlAttributeName.test(name))
729
- .map(entry => entry as [string, Value])
730
- .every(
731
- ([name, value]) => edit.element.getAttribute(name) === value,
732
- ) &&
733
- Object.entries(edit.attributesNS)
734
- .map(entry => entry as [string, Record<string, Value>])
735
- .every(([ns, attributes]) =>
736
- Object.entries(attributes)
737
- .filter(([name]) => util.xmlAttributeName.test(name))
738
- .map(entry => entry as [string, Value])
739
- .every(
740
- ([name, value]) =>
741
- edit.element.getAttributeNS(
742
- ns,
743
- name.includes(':')
744
- ? <string>name.split(':', 2)[1]
745
- : name,
746
- ) === value,
747
- ),
748
- )
749
- );
750
- },
751
- ),
752
- )).timeout(20000);
753
-
754
- it('removes elements on Remove edit events', () =>
755
- assert(
756
- property(
757
- util.testDocs.chain(([{ nodes }]) => util.remove(nodes)),
758
- ({ node }) => {
759
- editor.dispatchEvent(newEditEvent({ node }));
760
- return !node.parentNode;
761
- },
762
- ),
763
- ));
764
-
765
- it('undoes up to n edits on undo(n) call', () =>
766
- assert(
767
- property(
768
- util.testDocs.chain(docs => util.undoRedoTestCases(...docs)),
769
- ({ doc1, doc2, edits }: util.UndoRedoTestCase) => {
770
- const [oldDoc1, oldDoc2] = [doc1, doc2].map(doc =>
771
- doc.cloneNode(true),
772
- );
773
- edits.forEach((a: Edit) => {
774
- editor.dispatchEvent(newEditEvent(a));
775
- });
776
- if (edits.length) {
777
- editor.undo(edits.length);
778
- }
779
- expect(doc1).to.satisfy((doc: XMLDocument) =>
780
- doc.isEqualNode(oldDoc1),
781
- );
782
- expect(doc2).to.satisfy((doc: XMLDocument) =>
783
- doc.isEqualNode(oldDoc2),
784
- );
785
- return true;
786
- },
787
- ),
788
- )).timeout(20000);
789
-
790
- it('redoes up to n edits on redo(n) call', () =>
791
- assert(
792
- property(
793
- util.testDocs.chain(docs => util.undoRedoTestCases(...docs)),
794
- ({ doc1, doc2, edits }: util.UndoRedoTestCase) => {
795
- edits.forEach((a: Edit) => {
796
- editor.dispatchEvent(newEditEvent(a));
797
- });
798
- const [oldDoc1, oldDoc2] = [doc1, doc2].map(doc =>
799
- new XMLSerializer().serializeToString(doc),
800
- );
801
- if (edits.length) {
802
- editor.undo(edits.length + 1);
803
- editor.redo(edits.length + 1);
804
- }
805
- const [newDoc1, newDoc2] = [doc1, doc2].map(doc =>
806
- new XMLSerializer().serializeToString(doc),
807
- );
808
- return oldDoc1 === newDoc1 && oldDoc2 === newDoc2;
809
- },
810
- ),
811
- )).timeout(20000);
812
- });
813
- });