@meshagent/meshagent 0.0.11

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.
package/server.js ADDED
@@ -0,0 +1,310 @@
1
+ import * as Y from "yjs";
2
+ import { v4 } from "uuid";
3
+
4
+ const uuid = v4;
5
+
6
+ export class ServerXmlDocument {
7
+ constructor(doc, notifyChanges) {
8
+ this._y = doc.get("xml", Y.XmlElement);
9
+ this.doc = doc;
10
+ this._y.observeDeep(yxmlEvents=> {
11
+ for(let yxmlEvent of yxmlEvents) {
12
+ const target = yxmlEvent.target;
13
+
14
+ // Observe when child-elements are added or deleted.
15
+
16
+ if(target instanceof Y.XmlElement) {
17
+ const nodeID = target.getAttribute("$id");
18
+
19
+ const message = {
20
+ root: target == this._y,
21
+ target: nodeID,
22
+ elements: [],
23
+ attributes: {
24
+ set: [],
25
+ delete: []
26
+ },
27
+ text: []
28
+ };
29
+
30
+ // element changes
31
+ for(let delta of yxmlEvent.changes.delta) {
32
+ if(delta.insert) {
33
+ message.elements.push({ retain: delta.retain, insert: delta.insert.map(i => this.serialize(i)) });
34
+ } else if(delta.delete) {
35
+ message.elements.push({ retain: delta.retain, delete: delta.delete });
36
+ } else if(delta.retain) {
37
+ message.elements.push({ retain: delta.retain });
38
+ }
39
+ }
40
+
41
+ // attribute changes
42
+ yxmlEvent.changes.keys.forEach((change, key) => {
43
+ if (change.action === 'add') {
44
+ message.attributes.set.push({ "name": key, "value": target.getAttribute(key) });
45
+ } else if (change.action === 'update') {
46
+ message.attributes.set.push({ "name": key, "value": target.getAttribute(key) });
47
+ } else if (change.action === 'delete') {
48
+ message.attributes.delete.push(key);
49
+ }
50
+ })
51
+ notifyChanges(message);
52
+
53
+ } else if(target instanceof Y.XmlText) {
54
+ const nodeID = target.parent.getAttribute("$id");
55
+
56
+ const message = {
57
+ root: target == this._y,
58
+ target: nodeID,
59
+ elements: [],
60
+ attributes: {
61
+ set: [],
62
+ delete: []
63
+ },
64
+ text: []
65
+ };
66
+
67
+ // text changes
68
+ for(let delta of yxmlEvent.changes.delta) {
69
+ if(delta.insert) {
70
+ message.text.push({ retain: delta.retain, insert: delta.insert, attributes: delta.attributes });
71
+ } else if(delta.delete) {
72
+ message.text.push({ retain: delta.retain, delete: delta.delete });
73
+ } else if(delta.retain) {
74
+ message.text.push({ retain: delta.retain, attributes: delta.attributes });
75
+ }
76
+ }
77
+ notifyChanges(message);
78
+ } else {
79
+ throw new Error("Unexpected target type")
80
+ }
81
+
82
+ }
83
+ });
84
+ }
85
+
86
+ // changes should be Uint8Array
87
+ applyBackendChanges(changes) {
88
+ // TODO: need to set transaction origin?
89
+ Y.applyUpdate(this.doc, changes);
90
+ }
91
+
92
+ serialize(node) {
93
+
94
+ if(node instanceof Y.XmlElement) {
95
+ const children = [];
96
+ for(let n = node.firstChild; n != null; n = n.nextSibling) {
97
+ children.push(this.serialize(n));
98
+ }
99
+ return {
100
+ element: {
101
+ tagName: node.nodeName,
102
+ attributes: node.getAttributes(),
103
+ children: children,
104
+ }
105
+ }
106
+ } else if(node instanceof Y.XmlText) {
107
+ return {
108
+ text: {
109
+ delta: node.toDelta()
110
+ }
111
+ }
112
+ } else {
113
+ throw new Error("Unexpected node type "+node);
114
+ }
115
+ }
116
+
117
+
118
+ doDelete(element) {
119
+ let i = 0;
120
+ if (element.parent) {
121
+ for(let e = element.parent.firstChild; e != null; e = e.nextSibling) {
122
+ if(e == element) {
123
+ element.parent.delete(i, 1)
124
+ return;
125
+ }
126
+ i++;
127
+ }
128
+ throw new Error("element was not deleted" + element);
129
+ } else {
130
+ throw new Error("Cannot delete top level element");
131
+ }
132
+ }
133
+
134
+ createNodes(children) {
135
+ const nodes = [];
136
+ for(let child of children) {
137
+ if(child.text) {
138
+ const text = new Y.XmlText();
139
+ if(text.delta) {
140
+ text.applyDelta(text.delta);
141
+ }
142
+ nodes.push(text);
143
+ } else if(child.element) {
144
+ const element = new Y.XmlElement(child.element.name);
145
+ // assign a default node id
146
+ element.setAttribute("$id", child.element.attributes?.id ?? uuid());
147
+ if(child.element.attributes) {
148
+ for(let k of Object.keys(child.element.attributes)) {
149
+ element.setAttribute(k, child.element.attributes[k]);
150
+ }
151
+ }
152
+ if(child.element.children) {
153
+ element.insert(0, this.createNodes(child.element.children));
154
+ }
155
+ nodes.push(element);
156
+ } else {
157
+ throw new Error("Unexpected xml data encountered, item is not text or an element");
158
+ }
159
+ }
160
+ return nodes;
161
+ }
162
+
163
+ doInsertChildren(element, { after, index, children }) {
164
+ let afterElement;
165
+ if(after) {
166
+ for(let e = element.firstChild; e != null; e = e.nextSibling) {
167
+ if(e instanceof Y.XmlElement ) {
168
+ if(e.getAttribute("$id") == after) {
169
+ afterElement = e;
170
+ break;
171
+ }
172
+ }
173
+ }
174
+ if(afterElement == null) {
175
+ throw new Error("Unable to find child element to insert after: "+after);
176
+ }
177
+ }
178
+
179
+ if(afterElement) {
180
+ element.insertAfter(afterElement, this.createNodes(children));
181
+ } else {
182
+ if(index !== undefined && index != null) {
183
+ element.insert(index, this.createNodes(children));
184
+ } else {
185
+ element.push(this.createNodes(children));
186
+ }
187
+ }
188
+ }
189
+
190
+ doDeleteChildren(element, { after, index, length }) {
191
+ let afterElement;
192
+ if(after) {
193
+ let i = 0;
194
+ for(let e = element.firstChild; e != null; e = e.nextSibling) {
195
+ if(e instanceof Y.XmlElement ) {
196
+ if(e.getAttribute("$id") == after) {
197
+ afterElement = e;
198
+ break;
199
+ }
200
+ }
201
+ i++;
202
+ }
203
+ if(afterElement == null) {
204
+ throw new Error("Unable to find child element to insert after: "+after);
205
+ }
206
+ element.delete(i, length);
207
+ } else {
208
+ element.delete(index, length);
209
+ }
210
+ }
211
+
212
+
213
+ doRemoveAttributes(element, attributes) {
214
+ for(let k of attributes) {
215
+ element.removeAttribute(k);
216
+ }
217
+ }
218
+
219
+ doSetAttributes(element, attributes) {
220
+ for(let k of Object.keys(attributes)) {
221
+ element.setAttribute(k, attributes[k]);
222
+ }
223
+ }
224
+
225
+ findNode(nodeID, root = this._y) {
226
+ if(!nodeID) {
227
+ return root;
228
+ }
229
+ if(root instanceof Y.XmlElement) {
230
+ if(root.getAttribute("$id") == nodeID) {
231
+ return root;
232
+ }
233
+ for(let e = root.firstChild; e != null; e = e.nextSibling) {
234
+ let child = this.findNode(nodeID, e);
235
+ if(child) {
236
+ return child;
237
+ }
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+
243
+ doInsertText(element, {index, text, attributes}) {
244
+ if(typeof text != "string") {
245
+ throw new Error("Inserted text must be a string");
246
+ }
247
+ if(element.nodeName != "text") {
248
+ throw new Error("text can only be inserted in a text element");
249
+ }
250
+ const xmlText = element.firstChild;
251
+ xmlText.insert(index, text, attributes);
252
+ }
253
+
254
+ doFormatText(element, {from, length, attributes}) {
255
+ if(element.nodeName != "text") {
256
+ throw new Error("text can only be inserted in a text element");
257
+ }
258
+ const xmlText = element.firstChild;
259
+ xmlText.format(from, length, attributes);
260
+ }
261
+
262
+ doDeleteText(element, {index, length}) {
263
+ if(element.nodeName != "text") {
264
+ throw new Error("text can only be inserted in a text element");
265
+ }
266
+ const xmlText = element.firstChild;
267
+ xmlText.delete(index, length);
268
+ }
269
+
270
+ applyChanges(changes) {
271
+ for(let change of changes) {
272
+ const element = change.nodeID ? this.findNode(change.nodeID) : this._y;
273
+ if(element == null) {
274
+ throw new Error("Element was not found "+change.nodeID);
275
+ }
276
+
277
+ if(change.delete) {
278
+ this.doDelete(element);
279
+ }
280
+
281
+ if(change.insertChildren) {
282
+ this.doInsertChildren(element, change.insertChildren);
283
+ }
284
+
285
+ if(change.deleteChildren) {
286
+ this.doDeleteChildren(element, change.deleteChildren);
287
+ }
288
+
289
+ if(change.removeAttributes) {
290
+ this.doRemoveAttributes(element, change.removeAttributes);
291
+ }
292
+
293
+ if(change.setAttributes) {
294
+ this.doSetAttributes(element, change.setAttributes);
295
+ }
296
+
297
+ if(change.insertText) {
298
+ this.doInsertText(element, change.insertText);
299
+ }
300
+
301
+ if(change.formatText) {
302
+ this.doFormatText(element, change.formatText);
303
+ }
304
+
305
+ if(change.deleteText) {
306
+ this.doDeleteText(element, change.deleteText);
307
+ }
308
+ }
309
+ }
310
+ }
package/test/assert.js ADDED
@@ -0,0 +1,19 @@
1
+ export function assertEqual(v1,v2, message) {
2
+ if(v1 != v2) {
3
+ throw new Error(v1 + "!="+v2 +": expected "+ message);
4
+ }
5
+ }
6
+
7
+ export function assertDeltaEqual(d1, d2) {
8
+
9
+ // make sure our deltas match
10
+
11
+ assertEqual(d1.length, d2.length, "same length delta");
12
+ for(let i = 0; i < d1.length; i++) {
13
+ assertEqual(d1[i].insert, d2[i].insert, "delta insert is equal");
14
+ assertEqual(Object.keys(d1[i].attributes ?? {}).length, Object.keys(d2[i].attributes ?? {}).length, "same attribute count");
15
+ for(let k of Object.keys(d1[i].attributes ?? {})) {
16
+ assertEqual(d1[i].attributes[k], d2[i].attributes[k], "deltas attributes are equal");
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,33 @@
1
+ import { Protocol, WebSocketProtocolChannel, Completer } from "../protocol";
2
+ import { RoomClient } from "../document-server-client";
3
+ import WebSocket from "ws";
4
+ import * as runtime from "../entrypoint";
5
+
6
+ globalThis.WebSocket = WebSocket;
7
+
8
+ test('can sync changes',async () => {
9
+
10
+ const client = new RoomClient({protocol: new Protocol({channel: new WebSocketProtocolChannel({url: "ws://localhost:8080/rooms/room_name"})}), runtime: runtime});
11
+ const client2 = new RoomClient({protocol: new Protocol({channel: new WebSocketProtocolChannel({url: "ws://localhost:8080/rooms/room_name"})}), runtime: runtime});
12
+
13
+ client.start();
14
+ client2.start();
15
+
16
+ const doc = await client.sync.open("/rooms/room_name");
17
+ const doc2 = await client2.connect("/rooms/room_name");
18
+
19
+ console.log("CONNECTED");
20
+ doc.root.createChildElement("test", {"hello":"world"});
21
+
22
+ const completer = new Completer();
23
+
24
+ setTimeout(() => {
25
+ completer.resolve();
26
+ }, 1000*1);
27
+
28
+ await completer.fut;
29
+
30
+ if((doc2.root.getChildren()[0]).getAttribute("hello")!="world") {
31
+ throw new Error("attribute was not set")
32
+ }
33
+ })
@@ -0,0 +1,313 @@
1
+ import * as Y from "yjs";
2
+ import { ServerXmlDocument } from "../server.js";
3
+ import { ClientXmlDocument, XmlElement, XmlText } from "../client.js";
4
+ import { assertEqual, assertDeltaEqual } from "./assert.js";
5
+ import { v4 } from "uuid";
6
+
7
+ function makeDoc() {
8
+ const doc = new Y.Doc();
9
+ const root = doc.get("xml", Y.XmlElement);
10
+
11
+ const server = new ServerXmlDocument(doc, (message) => {
12
+ client.receiveChanges(message);
13
+ });
14
+
15
+ const client = new ClientXmlDocument({ id: v4(), sendChanges: (changes) => {
16
+ server.applyChanges(changes.changes);
17
+ }});
18
+ return [ root, client, server ];
19
+ }
20
+
21
+ test('set attribute', () => {
22
+ const [ root, client, server] = makeDoc();
23
+
24
+ client.root.setAttribute("test", "v1");
25
+
26
+ assertEqual(client.root.getAttribute("test"), "v1", "root attributes are equal");
27
+ });
28
+
29
+
30
+ test('insert and delete element', () => {
31
+ const [ serverRoot, client, server] = makeDoc();
32
+ const xmlElement = client.root.createChildElement("child", { "hello" : "world" });
33
+
34
+ // Can insert
35
+ let child = client.root.getChildren()[0];
36
+ assertEqual(child instanceof XmlElement, true, "inserted an element");
37
+ assertEqual(xmlElement instanceof XmlElement, true, "inserted an element");
38
+ assertEqual(xmlElement.id, child.id, "child id matches returned id");
39
+ assertEqual(child.tagName, "child", "inserted child tag names are equal");
40
+ assertEqual(child.getAttribute("hello"), "world", "inserted child attributes are equal");
41
+
42
+ // Can delete node
43
+ child.delete();
44
+ assertEqual(client.root.getChildren().length, 0, "can delete element");
45
+
46
+ });
47
+
48
+
49
+ test('update attribute', () => {
50
+ const [ serverRoot, client, server] = makeDoc();
51
+
52
+ const child = client.root.createChildElement("child", { "hello" : "world" });
53
+
54
+ // Can update attribute
55
+ child.setAttribute("hello","mod");
56
+ assertEqual(child.getAttribute("hello"), "mod", "child attributes are equal");
57
+
58
+ })
59
+
60
+ test('remove attribute', () => {
61
+ const [ serverRoot, client, server] = makeDoc();
62
+
63
+ const child = client.root.createChildElement("child", { "hello" : "world" });
64
+ child.setAttribute("hello","mod");
65
+
66
+ // Can remove attribute
67
+ child.removeAttribute("hello");
68
+ assertEqual(child.getAttribute("hello") == null, true, "child attribute was removed");
69
+
70
+ });
71
+
72
+ test('insert, extend, and shrink text delta', () => {
73
+
74
+ const [ serverRoot, client, server] = makeDoc();
75
+
76
+ // Can insert text
77
+ const child = client.root.createChildElement("text", { "hello" : "world" });
78
+ assertEqual(child instanceof XmlElement, true, "inserted an element");
79
+
80
+ let text = child.getChildren()[0];
81
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
82
+ assertEqual(text instanceof XmlText, true, "added a text element inside the element");
83
+ assertEqual(child.tagName, "text", "inserted text tag names are equal");
84
+ assertEqual(child.getAttribute("hello"), "world", "inserted child attributes are equal");
85
+
86
+ assertEqual(text.delta.length, 0, "deltas are empty");
87
+ text.insert(0, "hello world");
88
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
89
+ assertEqual(text.delta.length, 1, "inserted a delta");
90
+ assertEqual(text.delta[0].insert, "hello world", "text matches");
91
+
92
+ text.insert(0, "hello world");
93
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
94
+ assertEqual(text.delta.length, 1, "extended delta");
95
+ assertEqual(text.delta[0].insert, "hello worldhello world", "text was extended");
96
+
97
+ text.delete("hello world".length, "hello world".length);
98
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
99
+
100
+ assertEqual(text.delta.length, 1, "trimmed a delta");
101
+ assertEqual(text.delta[0].insert, "hello world", "text matches");
102
+
103
+ });
104
+
105
+
106
+ test('format text deltas', () => {
107
+
108
+ const [ serverRoot, client, server] = makeDoc();
109
+
110
+ // Can insert text
111
+ const child = client.root.createChildElement("text", { "hello" : "world" });
112
+ assertEqual(child instanceof XmlElement, true, "inserted an element");
113
+
114
+ let text = child.getChildren()[0];
115
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
116
+ assertEqual(text instanceof XmlText, true, "added a text element inside the element");
117
+ assertEqual(child.tagName, "text", "inserted text tag names are equal");
118
+ assertEqual(child.getAttribute("hello"), "world", "inserted child attributes are equal");
119
+
120
+ assertEqual(text.delta.length, 0, "deltas are empty");
121
+ text.insert(0, "hello world");
122
+ // format whole item
123
+ text.format(0, "hello world".length, {
124
+ "bold" : true
125
+ });
126
+
127
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
128
+
129
+ assertEqual(text.delta.length, 1, "kept delta");
130
+ assertEqual(text.delta[0].insert, "hello world", "text matches");
131
+ assertEqual(text.delta[0].attributes["bold"], true, "attribute matches");
132
+
133
+ // format start
134
+ text.format(0, "hello".length, {
135
+ "italic" : true
136
+ });
137
+
138
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
139
+
140
+ assertEqual(text.delta.length, 2, "split delta");
141
+ assertEqual(text.delta[0].insert, "hello", "first word matches");
142
+ assertEqual(text.delta[1].insert, " world", "second word matches");
143
+ assertEqual(text.delta[0].attributes["bold"], true, "attribute matches");
144
+ assertEqual(text.delta[0].attributes["italic"], true, "attribute matches");
145
+ assertEqual(text.delta[1].attributes["bold"], true, "attribute matches");
146
+ assertEqual(text.delta[1].attributes["italic"], null, "attribute matches");
147
+
148
+ // format end
149
+ text.format(3, 2, {
150
+ "underline" : true
151
+ });
152
+
153
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
154
+
155
+ assertEqual(text.delta.length, 3, "split delta");
156
+ assertEqual(text.delta[0].insert, "hel", "first word matches");
157
+ assertEqual(text.delta[1].insert, "lo", "second word matches");
158
+ assertEqual(text.delta[2].insert, " world", "third word matches");
159
+
160
+ assertEqual(text.delta[0].attributes["bold"], true, "attribute matches");
161
+ assertEqual(text.delta[0].attributes["italic"], true, "attribute matches");
162
+ assertEqual(text.delta[0].attributes["underline"], null, "attribute matches");
163
+
164
+ assertEqual(text.delta[1].attributes["bold"], true, "attribute matches");
165
+ assertEqual(text.delta[1].attributes["italic"], true, "attribute matches");
166
+ assertEqual(text.delta[1].attributes["underline"], true, "attribute matches");
167
+
168
+ assertEqual(text.delta[2].attributes["bold"], true, "attribute matches");
169
+ assertEqual(text.delta[2].attributes["italic"], null, "attribute matches");
170
+ assertEqual(text.delta[2].attributes["underline"], null, "attribute matches");
171
+
172
+
173
+ // format across items
174
+ text.format(0, "hello world".length, {
175
+ "strikethrough" : true
176
+ });
177
+
178
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
179
+ assertEqual(text.delta.length, 3, "retain delta");
180
+ assertEqual(text.delta[0].insert, "hel", "first word matches");
181
+ assertEqual(text.delta[1].insert, "lo", "second word matches");
182
+ assertEqual(text.delta[2].insert, " world", "third word matches");
183
+
184
+ assertEqual(text.delta[0].attributes["bold"], true, "attribute matches");
185
+ assertEqual(text.delta[0].attributes["italic"], true, "attribute matches");
186
+ assertEqual(text.delta[0].attributes["underline"], null, "attribute matches");
187
+ assertEqual(text.delta[0].attributes["strikethrough"], true, "attribute matches");
188
+
189
+ assertEqual(text.delta[1].attributes["bold"], true, "attribute matches");
190
+ assertEqual(text.delta[1].attributes["italic"], true, "attribute matches");
191
+ assertEqual(text.delta[1].attributes["underline"], true, "attribute matches");
192
+ assertEqual(text.delta[1].attributes["strikethrough"], true, "attribute matches");
193
+
194
+ assertEqual(text.delta[2].attributes["bold"], true, "attribute matches");
195
+ assertEqual(text.delta[2].attributes["italic"], null, "attribute matches");
196
+ assertEqual(text.delta[2].attributes["underline"], null, "attribute matches");
197
+ assertEqual(text.delta[2].attributes["strikethrough"], true, "attribute matches");
198
+
199
+ // format across items
200
+ text.format(1, 1, {
201
+ "dot" : true
202
+ });
203
+
204
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
205
+ assertEqual(text.delta.length, 5, "split delta");
206
+ assertEqual(text.delta[0].insert, "h", "first word matches");
207
+ assertEqual(text.delta[1].insert, "e", "second word matches");
208
+ assertEqual(text.delta[2].insert, "l", "third word matches");
209
+ });
210
+
211
+ test('delete start of delta text', () => {
212
+
213
+ const [ serverRoot, client, server] = makeDoc();
214
+
215
+ // Text Delete
216
+ client.root.createChildElement("text", { "hello" : "world" });
217
+ assertEqual(client.root.getChildren().length, 1, "inserted text element");
218
+ let child = client.root.getChildren()[0];
219
+
220
+ // Delete start
221
+ assertEqual(child instanceof XmlElement, true, "inserted an element");
222
+ let text = child.getChildren()[0];
223
+ assertEqual(text instanceof XmlText, true, "inserted a text element");
224
+ text.insert(0, "hello world");
225
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
226
+ assertEqual(text.delta.length, 1, "inserted a delta");
227
+ assertEqual(text.delta[0].insert, "hello world", "text matches");
228
+ text.delete(0, "hello ".length);
229
+ assertEqual(text.delta.length, 1, "kept delta");
230
+ assertEqual(text.delta[0].insert, "world", "text matches");
231
+
232
+ });
233
+
234
+ test('delete end of delta text', () => {
235
+
236
+ const [ serverRoot, client, server] = makeDoc();
237
+
238
+ // Text Delete
239
+ client.root.createChildElement("text", { "hello" : "world" });
240
+ assertEqual(client.root.getChildren().length, 1, "inserted text element");
241
+ let child = client.root.getChildren()[0];
242
+ let text = child.getChildren()[0];
243
+ text.insert(0, "world");
244
+
245
+ // Delete end
246
+ text.delete("world".length - 1, 1);
247
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
248
+ assertEqual(text.delta.length, 1, "kept delta");
249
+ assertEqual(text.delta[0].insert, "worl", "text matches");
250
+
251
+ });
252
+
253
+ test('delete center of delta text', () => {
254
+
255
+ const [ serverRoot, client, server] = makeDoc();
256
+
257
+ client.root.createChildElement("text", { "hello" : "world" });
258
+ assertEqual(client.root.getChildren().length, 1, "inserted text element");
259
+ let child = client.root.getChildren()[0];
260
+ let text = child.getChildren()[0];
261
+ text.insert(0, "worl");
262
+
263
+ // Delete center
264
+ text.delete(2, 1);
265
+ assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
266
+ assertEqual(text.delta.length, 1, "kept delta");
267
+ assertEqual(text.delta[0].insert, "wol", "text matches");
268
+
269
+ });
270
+
271
+ test('insert elements at positions', () => {
272
+
273
+ const [ serverRoot, client, server] = makeDoc();
274
+
275
+ // Inserts at end
276
+ client.root.createChildElement("child2", { "hello2" : "world2" });
277
+
278
+ let child = client.root.getChildren()[0];
279
+ assertEqual(child.tagName, "child2", "inserted child tag names are equal");
280
+ assertEqual(child.getAttribute("hello2"), "world2", "inserted child attributes are equal");
281
+
282
+
283
+ // Inserts deep
284
+ child.createChildElement("child3", { "hello3" : "world3" });
285
+
286
+ const deepChild = child;
287
+
288
+ child = child.getChildren()[0];
289
+ assertEqual(child.tagName, "child3", "inserted child tag names are equal");
290
+ assertEqual(child.getAttribute("hello3"), "world3", "inserted child attributes are equal");
291
+
292
+ // Inserts after deep
293
+ child.parent.createChildElement("child4", { "hello4" : "world4" });
294
+
295
+ child = deepChild.getChildren()[1];
296
+ assertEqual(child.tagName, "child4", "inserted child tag names are equal");
297
+ assertEqual(child.getAttribute("hello4"), "world4", "inserted child attributes are equal");
298
+
299
+ // Inserts after element deep
300
+ deepChild.createChildElementAfter(deepChild.getChildren()[0], "child5", { "hello5" : "world5" });
301
+
302
+ child = deepChild.getChildren()[1];
303
+ assertEqual(child.tagName, "child5", "inserted child tag names are equal");
304
+ assertEqual(child.getAttribute("hello5"), "world5", "inserted child attributes are equal");
305
+
306
+
307
+ // Inserts after element deep
308
+ deepChild.createChildElementAt(2, "child6", { "hello6" : "world6" });
309
+
310
+ child = deepChild.getChildren()[2];
311
+ assertEqual(child.tagName, "child6", "inserted child tag names are equal");
312
+ assertEqual(child.getAttribute("hello6"), "world6", "inserted child attributes are equal");
313
+ });