@schukai/monster 4.127.1 → 4.128.1

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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  import * as chai from "chai";
4
4
 
5
+ import { addToObjectLink } from "../../../source/dom/attributes.mjs";
6
+ import { customElementUpdaterLinkSymbol } from "../../../source/dom/constants.mjs";
5
7
  import { ID } from "../../../source/types/id.mjs";
6
8
  import { Observer } from "../../../source/types/observer.mjs";
7
9
  import { ProxyObserver } from "../../../source/types/proxyobserver.mjs";
@@ -123,6 +125,49 @@ let htmlNested = `
123
125
  </div>
124
126
  `;
125
127
 
128
+ let htmlStatefulReplace = `
129
+ <div id="test-stateful">
130
+ <div data-monster-replace="path:content"></div>
131
+ </div>
132
+ `;
133
+
134
+ let htmlPatch = `
135
+ <template id="patch-item">
136
+ <li data-monster-patch="path:patch-item.label"></li>
137
+ </template>
138
+ <div id="test-patch">
139
+ <div id="patch-text" data-monster-patch="path:text"></div>
140
+ <div id="patch-node" data-monster-patch="path:contentNode"></div>
141
+ <div id="patch-fragment" data-monster-patch="path:fragmentNode"></div>
142
+ <div id="patch-array" data-monster-patch="path:itemsArray"></div>
143
+ <div id="patch-keyed" data-monster-patch="path:keyedItems" data-monster-patch-key="call:getPatchKey"></div>
144
+ <div id="patch-keyed-render" data-monster-patch="path:keyedDataItems" data-monster-patch-key="path:id" data-monster-patch-render="path:label"></div>
145
+ <ul id="patch-list" data-monster-insert="patch-item path:items"></ul>
146
+ </div>
147
+ `;
148
+
149
+ let htmlPatchVsReplace = `
150
+ <div id="test-patch-vs-replace">
151
+ <div id="replace-target" data-monster-replace="path:replaceHtml"></div>
152
+ <div
153
+ id="patch-target"
154
+ data-monster-patch="path:patchItems"
155
+ data-monster-patch-key="path:id"
156
+ data-monster-patch-render="call:renderPatchItem"
157
+ ></div>
158
+ <div
159
+ id="replace-race-target"
160
+ data-monster-replace="path:replaceRaceHtml"
161
+ ></div>
162
+ <div
163
+ id="patch-race-target"
164
+ data-monster-patch="path:patchRaceItems"
165
+ data-monster-patch-key="path:id"
166
+ data-monster-patch-render="call:renderPatchRaceItem"
167
+ ></div>
168
+ </div>
169
+ `;
170
+
126
171
  describe("DOM", function () {
127
172
  let Updater = null;
128
173
 
@@ -193,6 +238,100 @@ describe("DOM", function () {
193
238
  );
194
239
  }
195
240
 
241
+ if (!customElements.get("monster-test-churn-item")) {
242
+ class MonsterTestChurnItem extends HTMLElement {
243
+ static stats = {
244
+ replace: { created: 0, connected: 0, disconnected: 0 },
245
+ patch: { created: 0, connected: 0, disconnected: 0 },
246
+ };
247
+
248
+ connectedCallback() {
249
+ const mode = this.getAttribute("data-mode") || "replace";
250
+ if (this.__countedCreated !== true) {
251
+ this.__countedCreated = true;
252
+ MonsterTestChurnItem.stats[mode].created++;
253
+ }
254
+ MonsterTestChurnItem.stats[mode].connected++;
255
+ }
256
+
257
+ disconnectedCallback() {
258
+ const mode = this.getAttribute("data-mode") || "replace";
259
+ MonsterTestChurnItem.stats[mode].disconnected++;
260
+ }
261
+
262
+ static resetStats() {
263
+ MonsterTestChurnItem.stats = {
264
+ replace: { created: 0, connected: 0, disconnected: 0 },
265
+ patch: { created: 0, connected: 0, disconnected: 0 },
266
+ };
267
+ }
268
+ }
269
+
270
+ customElements.define(
271
+ "monster-test-churn-item",
272
+ MonsterTestChurnItem,
273
+ );
274
+ }
275
+
276
+ if (!customElements.get("monster-test-race-item")) {
277
+ class MonsterTestRaceItem extends HTMLElement {
278
+ static stats = {
279
+ replace: {
280
+ connected: 0,
281
+ disconnected: 0,
282
+ firedConnected: 0,
283
+ firedDisconnected: 0,
284
+ },
285
+ patch: {
286
+ connected: 0,
287
+ disconnected: 0,
288
+ firedConnected: 0,
289
+ firedDisconnected: 0,
290
+ },
291
+ };
292
+
293
+ connectedCallback() {
294
+ const mode = this.getAttribute("data-mode") || "replace";
295
+ MonsterTestRaceItem.stats[mode].connected++;
296
+
297
+ setTimeout(() => {
298
+ if (this.isConnected) {
299
+ MonsterTestRaceItem.stats[mode].firedConnected++;
300
+ } else {
301
+ MonsterTestRaceItem.stats[mode].firedDisconnected++;
302
+ }
303
+ }, 10);
304
+ }
305
+
306
+ disconnectedCallback() {
307
+ const mode = this.getAttribute("data-mode") || "replace";
308
+ MonsterTestRaceItem.stats[mode].disconnected++;
309
+ }
310
+
311
+ static resetStats() {
312
+ MonsterTestRaceItem.stats = {
313
+ replace: {
314
+ connected: 0,
315
+ disconnected: 0,
316
+ firedConnected: 0,
317
+ firedDisconnected: 0,
318
+ },
319
+ patch: {
320
+ connected: 0,
321
+ disconnected: 0,
322
+ firedConnected: 0,
323
+ firedDisconnected: 0,
324
+ },
325
+ };
326
+ }
327
+ }
328
+
329
+ customElements.define(
330
+ "monster-test-race-item",
331
+ MonsterTestRaceItem,
332
+ );
333
+ }
334
+
196
335
  done();
197
336
  })
198
337
  .catch((e) => {
@@ -222,7 +361,7 @@ describe("DOM", function () {
222
361
  element = document.getElementById("test1");
223
362
  });
224
363
 
225
- describe("Configuration Methods", function () {
364
+ describe("Configuration Methods", function () {
226
365
  it("setEventTypes() should be chainable", function () {
227
366
  const u = new Updater(element);
228
367
  expect(u.setEventTypes(["touch"])).to.be.instanceof(Updater);
@@ -239,11 +378,42 @@ describe("DOM", function () {
239
378
  expect(u.enableEventProcessing()).to.be.instanceof(Updater);
240
379
  });
241
380
 
242
- it("disableEventProcessing() should be chainable", function () {
243
- const u = new Updater(element);
244
- expect(u.disableEventProcessing()).to.be.instanceof(Updater);
245
- });
246
- });
381
+ it("disableEventProcessing() should be chainable", function () {
382
+ const u = new Updater(element);
383
+ expect(u.disableEventProcessing()).to.be.instanceof(Updater);
384
+ });
385
+
386
+ it("dispose() should stop future DOM updates", function (done) {
387
+ const subject = { text: "first" };
388
+ const target = document.createElement("div");
389
+ target.innerHTML = `<div data-monster-replace="path:text"></div>`;
390
+ document.getElementById("mocks").appendChild(target);
391
+
392
+ const u = new Updater(target, subject);
393
+ u.run()
394
+ .then(() => {
395
+ setTimeout(() => {
396
+ try {
397
+ expect(target).contain.html("<div data-monster-replace=\"path:text\">first</div>");
398
+ u.dispose();
399
+ u.getSubject().text = "second";
400
+
401
+ setTimeout(() => {
402
+ try {
403
+ expect(target).contain.html("<div data-monster-replace=\"path:text\">first</div>");
404
+ done();
405
+ } catch (e) {
406
+ done(e);
407
+ }
408
+ }, 20);
409
+ } catch (e) {
410
+ done(e);
411
+ }
412
+ }, 0);
413
+ })
414
+ .catch((e) => done(e));
415
+ });
416
+ });
247
417
 
248
418
  describe("Constructor Error Handling", function () {
249
419
  it("should throw a TypeError if no HTMLElement is provided", function () {
@@ -332,6 +502,199 @@ describe("DOM", function () {
332
502
  });
333
503
  });
334
504
 
505
+ describe("Updater()", function () {
506
+ beforeEach(() => {
507
+ let mocks = document.getElementById("mocks");
508
+ mocks.innerHTML = htmlPatchVsReplace;
509
+ customElements.get("monster-test-churn-item").resetStats();
510
+ customElements.get("monster-test-race-item").resetStats();
511
+ });
512
+
513
+ describe("Patch versus Replace", function () {
514
+ it("should show less lifecycle churn for keyed patch than for replace under repeated updates", function (done) {
515
+ const buildReplaceHtml = (items) =>
516
+ items
517
+ .map(
518
+ ({ id, label }) =>
519
+ `<monster-test-churn-item data-mode="replace" data-id="${id}">${label}</monster-test-churn-item>`,
520
+ )
521
+ .join("");
522
+
523
+ const patchNodes = new Map();
524
+ const renderPatchItem = (item) => {
525
+ let node = patchNodes.get(item.id);
526
+ if (!(node instanceof HTMLElement)) {
527
+ node = document.createElement("monster-test-churn-item");
528
+ node.setAttribute("data-mode", "patch");
529
+ node.setAttribute("data-id", item.id);
530
+ patchNodes.set(item.id, node);
531
+ }
532
+ node.textContent = item.label;
533
+ return node;
534
+ };
535
+
536
+ const createItems = (suffix = "") => [
537
+ { id: "a", label: `Alpha${suffix}` },
538
+ { id: "b", label: `Beta${suffix}` },
539
+ { id: "c", label: `Gamma${suffix}` },
540
+ { id: "d", label: `Delta${suffix}` },
541
+ ];
542
+
543
+ const subject = {
544
+ replaceHtml: buildReplaceHtml(createItems()),
545
+ patchItems: createItems(),
546
+ };
547
+
548
+ const updater = new Updater(
549
+ document.getElementById("test-patch-vs-replace"),
550
+ subject,
551
+ );
552
+ updater.setCallback("renderPatchItem", renderPatchItem);
553
+
554
+ updater
555
+ .run()
556
+ .then(() => {
557
+ setTimeout(() => {
558
+ try {
559
+ const sequences = [
560
+ ["d", "a", "b", "c"],
561
+ ["c", "d", "a", "b"],
562
+ ["b", "c", "d", "a"],
563
+ ["a", "b", "c", "d"],
564
+ ];
565
+
566
+ for (let i = 0; i < 6; i++) {
567
+ const next = sequences[i % sequences.length].map((id) => {
568
+ const labelMap = {
569
+ a: "Alpha",
570
+ b: "Beta",
571
+ c: "Gamma",
572
+ d: "Delta",
573
+ };
574
+ return {
575
+ id,
576
+ label: `${labelMap[id]}-${i}`,
577
+ };
578
+ });
579
+ updater.getSubject().replaceHtml = buildReplaceHtml(next);
580
+ updater.getSubject().patchItems = next;
581
+ }
582
+
583
+ setTimeout(() => {
584
+ try {
585
+ const churnStats =
586
+ customElements.get("monster-test-churn-item").stats;
587
+ expect(churnStats.patch.created).to.equal(4);
588
+ expect(churnStats.replace.created).to.be.greaterThan(
589
+ churnStats.patch.created,
590
+ );
591
+ expect(churnStats.replace.disconnected).to.be.greaterThan(0);
592
+ expect(churnStats.replace.created).to.be.greaterThan(
593
+ churnStats.replace.disconnected / 2,
594
+ );
595
+ done();
596
+ } catch (e) {
597
+ done(e);
598
+ }
599
+ }, 40);
600
+ } catch (e) {
601
+ done(e);
602
+ }
603
+ }, 20);
604
+ })
605
+ .catch((e) => done(new Error(e)));
606
+ });
607
+
608
+ it("should reduce disconnected async callback surface with patch compared to replace", function (done) {
609
+ const buildReplaceRaceHtml = (items) =>
610
+ items
611
+ .map(
612
+ ({ id, label }) =>
613
+ `<monster-test-race-item data-mode="replace" data-id="${id}">${label}</monster-test-race-item>`,
614
+ )
615
+ .join("");
616
+
617
+ const patchNodes = new Map();
618
+ const renderPatchRaceItem = (item) => {
619
+ let node = patchNodes.get(item.id);
620
+ if (!(node instanceof HTMLElement)) {
621
+ node = document.createElement("monster-test-race-item");
622
+ node.setAttribute("data-mode", "patch");
623
+ node.setAttribute("data-id", item.id);
624
+ patchNodes.set(item.id, node);
625
+ }
626
+ node.textContent = item.label;
627
+ return node;
628
+ };
629
+
630
+ const sequences = [
631
+ [
632
+ { id: "a", label: "Alpha-0" },
633
+ { id: "b", label: "Beta-0" },
634
+ { id: "c", label: "Gamma-0" },
635
+ ],
636
+ [
637
+ { id: "c", label: "Gamma-1" },
638
+ { id: "a", label: "Alpha-1" },
639
+ { id: "b", label: "Beta-1" },
640
+ ],
641
+ [
642
+ { id: "b", label: "Beta-2" },
643
+ { id: "c", label: "Gamma-2" },
644
+ { id: "a", label: "Alpha-2" },
645
+ ],
646
+ ];
647
+
648
+ const updater = new Updater(
649
+ document.getElementById("test-patch-vs-replace"),
650
+ {
651
+ replaceRaceHtml: buildReplaceRaceHtml(sequences[0]),
652
+ patchRaceItems: sequences[0],
653
+ },
654
+ );
655
+ updater.setCallback("renderPatchRaceItem", renderPatchRaceItem);
656
+
657
+ updater
658
+ .run()
659
+ .then(() => {
660
+ setTimeout(() => {
661
+ try {
662
+ updater.getSubject().replaceRaceHtml = buildReplaceRaceHtml(
663
+ sequences[1],
664
+ );
665
+ updater.getSubject().patchRaceItems = sequences[1];
666
+
667
+ updater.getSubject().replaceRaceHtml = buildReplaceRaceHtml(
668
+ sequences[2],
669
+ );
670
+ updater.getSubject().patchRaceItems = sequences[2];
671
+
672
+ setTimeout(() => {
673
+ try {
674
+ const raceStats =
675
+ customElements.get("monster-test-race-item").stats;
676
+ expect(raceStats.patch.firedDisconnected).to.equal(0);
677
+ expect(raceStats.replace.firedDisconnected).to.be.greaterThan(
678
+ 0,
679
+ );
680
+ expect(raceStats.replace.firedDisconnected).to.be.greaterThan(
681
+ raceStats.patch.firedDisconnected,
682
+ );
683
+ done();
684
+ } catch (e) {
685
+ done(e);
686
+ }
687
+ }, 50);
688
+ } catch (e) {
689
+ done(e);
690
+ }
691
+ }, 0);
692
+ })
693
+ .catch((e) => done(new Error(e)));
694
+ });
695
+ });
696
+ });
697
+
335
698
  describe("Updater()", function () {
336
699
  describe("Repeat", function () {
337
700
  it("should build 6 li elements from an array", function (done) {
@@ -750,6 +1113,485 @@ describe("DOM", function () {
750
1113
  done(new Error(e));
751
1114
  });
752
1115
  });
1116
+
1117
+ it("should dispose linked updaters when replacing a mounted stateful subtree", function (done) {
1118
+ let mocks = document.getElementById("mocks");
1119
+ mocks.innerHTML = htmlStatefulReplace;
1120
+
1121
+ const statefulElement = document.createElement("div");
1122
+ statefulElement.innerHTML = "<span>stateful</span>";
1123
+
1124
+ let disposeCount = 0;
1125
+ addToObjectLink(
1126
+ statefulElement,
1127
+ customElementUpdaterLinkSymbol,
1128
+ new Set([
1129
+ {
1130
+ dispose() {
1131
+ disposeCount++;
1132
+ },
1133
+ },
1134
+ ]),
1135
+ );
1136
+
1137
+ let d = new Updater(document.getElementById("test-stateful"), {
1138
+ content: statefulElement,
1139
+ });
1140
+
1141
+ d.run()
1142
+ .then(() => {
1143
+ setTimeout(() => {
1144
+ try {
1145
+ expect(statefulElement.isConnected).to.equal(true);
1146
+ d.getSubject().content = "<div>replaced</div>";
1147
+
1148
+ setTimeout(() => {
1149
+ try {
1150
+ expect(disposeCount).to.equal(1);
1151
+ expect(statefulElement.isConnected).to.equal(false);
1152
+ expect(document.getElementById("test-stateful")).contain.html(
1153
+ "<div>replaced</div>",
1154
+ );
1155
+ done();
1156
+ } catch (e) {
1157
+ done(e);
1158
+ }
1159
+ }, 40);
1160
+ } catch (e) {
1161
+ done(e);
1162
+ }
1163
+ }, 0);
1164
+ })
1165
+ .catch((e) => {
1166
+ done(new Error(e));
1167
+ });
1168
+ });
1169
+ });
1170
+ });
1171
+
1172
+ describe("Updater()", function () {
1173
+ beforeEach(() => {
1174
+ let mocks = document.getElementById("mocks");
1175
+ mocks.innerHTML = htmlPatch;
1176
+ });
1177
+
1178
+ describe("Patch", function () {
1179
+ it("should patch primitive values as text content without parsing HTML", function (done) {
1180
+ let element = document.getElementById("test-patch");
1181
+ let d = new Updater(element, {
1182
+ text: "<strong>Hello</strong>",
1183
+ fragmentNode: document.createDocumentFragment(),
1184
+ itemsArray: [],
1185
+ keyedItems: [],
1186
+ keyedDataItems: [],
1187
+ items: [],
1188
+ });
1189
+
1190
+ d.run()
1191
+ .then(() => {
1192
+ setTimeout(() => {
1193
+ try {
1194
+ const target = document.getElementById("patch-text");
1195
+ expect(target.innerHTML).to.equal("&lt;strong&gt;Hello&lt;/strong&gt;");
1196
+ expect(target.textContent).to.equal("<strong>Hello</strong>");
1197
+ done();
1198
+ } catch (e) {
1199
+ done(e);
1200
+ }
1201
+ }, 20);
1202
+ })
1203
+ .catch((e) => done(new Error(e)));
1204
+ });
1205
+
1206
+ it("should patch HTMLElement values without reparsing HTML", function (done) {
1207
+ let element = document.getElementById("test-patch");
1208
+ const stateful = document.createElement("monster-message-state-button");
1209
+ stateful.innerHTML = "Save";
1210
+
1211
+ let d = new Updater(element, {
1212
+ text: "",
1213
+ contentNode: stateful,
1214
+ fragmentNode: document.createDocumentFragment(),
1215
+ itemsArray: [],
1216
+ keyedItems: [],
1217
+ keyedDataItems: [],
1218
+ items: [],
1219
+ });
1220
+
1221
+ d.run()
1222
+ .then(() => {
1223
+ setTimeout(() => {
1224
+ try {
1225
+ const target = document.getElementById("patch-node");
1226
+ expect(target.firstElementChild).to.equal(stateful);
1227
+ expect(stateful.isConnected).to.equal(true);
1228
+ done();
1229
+ } catch (e) {
1230
+ done(e);
1231
+ }
1232
+ }, 20);
1233
+ })
1234
+ .catch((e) => done(new Error(e)));
1235
+ });
1236
+
1237
+ it("should rewrite nested patch paths in inserted templates", function (done) {
1238
+ let element = document.getElementById("test-patch");
1239
+ let d = new Updater(element, {
1240
+ text: "",
1241
+ contentNode: document.createElement("div"),
1242
+ fragmentNode: document.createDocumentFragment(),
1243
+ itemsArray: [],
1244
+ keyedItems: [],
1245
+ keyedDataItems: [],
1246
+ items: [{ label: "One" }, { label: "Two" }],
1247
+ });
1248
+
1249
+ d.run()
1250
+ .then(() => {
1251
+ setTimeout(() => {
1252
+ try {
1253
+ const list = document.getElementById("patch-list");
1254
+ expect(list.children.length).to.equal(2);
1255
+ expect(list.children[0].getAttribute("data-monster-patch")).to.equal(
1256
+ "path:items.0.label",
1257
+ );
1258
+ expect(list.children[0].textContent).to.equal("One");
1259
+ expect(list.children[1].getAttribute("data-monster-patch")).to.equal(
1260
+ "path:items.1.label",
1261
+ );
1262
+ expect(list.children[1].textContent).to.equal("Two");
1263
+ done();
1264
+ } catch (e) {
1265
+ done(e);
1266
+ }
1267
+ }, 20);
1268
+ })
1269
+ .catch((e) => done(new Error(e)));
1270
+ });
1271
+
1272
+ it("should patch DocumentFragment values without reparsing HTML", function (done) {
1273
+ let element = document.getElementById("test-patch");
1274
+ const fragment = document.createDocumentFragment();
1275
+ const first = document.createElement("span");
1276
+ const second = document.createElement("strong");
1277
+ first.textContent = "Alpha";
1278
+ second.textContent = "Beta";
1279
+ fragment.appendChild(first);
1280
+ fragment.appendChild(second);
1281
+
1282
+ let d = new Updater(element, {
1283
+ text: "",
1284
+ contentNode: document.createElement("div"),
1285
+ fragmentNode: fragment,
1286
+ itemsArray: [],
1287
+ keyedItems: [],
1288
+ keyedDataItems: [],
1289
+ items: [],
1290
+ });
1291
+
1292
+ d.run()
1293
+ .then(() => {
1294
+ setTimeout(() => {
1295
+ try {
1296
+ const target = document.getElementById("patch-fragment");
1297
+ expect(target.children.length).to.equal(2);
1298
+ expect(target.children[0].textContent).to.equal("Alpha");
1299
+ expect(target.children[1].textContent).to.equal("Beta");
1300
+ done();
1301
+ } catch (e) {
1302
+ done(e);
1303
+ }
1304
+ }, 20);
1305
+ })
1306
+ .catch((e) => done(new Error(e)));
1307
+ });
1308
+
1309
+ it("should patch arrays of primitives and nodes without using innerHTML", function (done) {
1310
+ let element = document.getElementById("test-patch");
1311
+ const badge = document.createElement("span");
1312
+ badge.textContent = "Node";
1313
+
1314
+ let d = new Updater(element, {
1315
+ text: "",
1316
+ contentNode: document.createElement("div"),
1317
+ fragmentNode: document.createDocumentFragment(),
1318
+ itemsArray: ["Alpha", badge, "Omega"],
1319
+ keyedItems: [],
1320
+ keyedDataItems: [],
1321
+ items: [],
1322
+ });
1323
+
1324
+ d.run()
1325
+ .then(() => {
1326
+ setTimeout(() => {
1327
+ try {
1328
+ const target = document.getElementById("patch-array");
1329
+ expect(target.childNodes.length).to.equal(3);
1330
+ expect(target.childNodes[0].textContent).to.equal("Alpha");
1331
+ expect(target.childNodes[1]).to.equal(badge);
1332
+ expect(target.childNodes[2].textContent).to.equal("Omega");
1333
+ expect(target.innerHTML).to.equal("Alpha<span>Node</span>Omega");
1334
+ done();
1335
+ } catch (e) {
1336
+ done(e);
1337
+ }
1338
+ }, 20);
1339
+ })
1340
+ .catch((e) => done(new Error(e)));
1341
+ });
1342
+
1343
+ it("should reorder keyed nodes without recreating them", function (done) {
1344
+ let element = document.getElementById("test-patch");
1345
+ const a = document.createElement("span");
1346
+ const b = document.createElement("span");
1347
+ const c = document.createElement("span");
1348
+ a.dataset.key = "a";
1349
+ b.dataset.key = "b";
1350
+ c.dataset.key = "c";
1351
+ a.textContent = "A";
1352
+ b.textContent = "B";
1353
+ c.textContent = "C";
1354
+
1355
+ let d = new Updater(element, {
1356
+ text: "",
1357
+ contentNode: document.createElement("div"),
1358
+ fragmentNode: document.createDocumentFragment(),
1359
+ itemsArray: [],
1360
+ keyedItems: [a, b, c],
1361
+ keyedDataItems: [],
1362
+ items: [],
1363
+ });
1364
+ d.setCallback("getPatchKey", (value) => value?.dataset?.key);
1365
+
1366
+ d.run()
1367
+ .then(() => {
1368
+ setTimeout(() => {
1369
+ try {
1370
+ const target = document.getElementById("patch-keyed");
1371
+ expect(Array.from(target.children)).to.deep.equal([a, b, c]);
1372
+
1373
+ d.getSubject().keyedItems = [c, a, b];
1374
+
1375
+ setTimeout(() => {
1376
+ try {
1377
+ expect(Array.from(target.children)).to.deep.equal([c, a, b]);
1378
+ expect(target.children[0]).to.equal(c);
1379
+ expect(target.children[1]).to.equal(a);
1380
+ expect(target.children[2]).to.equal(b);
1381
+ done();
1382
+ } catch (e) {
1383
+ done(e);
1384
+ }
1385
+ }, 20);
1386
+ } catch (e) {
1387
+ done(e);
1388
+ }
1389
+ }, 20);
1390
+ })
1391
+ .catch((e) => done(new Error(e)));
1392
+ });
1393
+
1394
+ it("should remove stale keyed nodes and keep surviving node identity", function (done) {
1395
+ let element = document.getElementById("test-patch");
1396
+ const a = document.createElement("span");
1397
+ const b = document.createElement("span");
1398
+ const dNode = document.createElement("span");
1399
+ a.dataset.key = "a";
1400
+ b.dataset.key = "b";
1401
+ dNode.dataset.key = "d";
1402
+ a.textContent = "A";
1403
+ b.textContent = "B";
1404
+ dNode.textContent = "D";
1405
+
1406
+ let d = new Updater(element, {
1407
+ text: "",
1408
+ contentNode: document.createElement("div"),
1409
+ fragmentNode: document.createDocumentFragment(),
1410
+ itemsArray: [],
1411
+ keyedItems: [a, b],
1412
+ keyedDataItems: [],
1413
+ items: [],
1414
+ });
1415
+ d.setCallback("getPatchKey", (value) => value?.dataset?.key);
1416
+
1417
+ d.run()
1418
+ .then(() => {
1419
+ setTimeout(() => {
1420
+ try {
1421
+ const target = document.getElementById("patch-keyed");
1422
+ d.getSubject().keyedItems = [b, dNode];
1423
+
1424
+ setTimeout(() => {
1425
+ try {
1426
+ expect(Array.from(target.children)).to.deep.equal([b, dNode]);
1427
+ expect(b.isConnected).to.equal(true);
1428
+ expect(a.isConnected).to.equal(false);
1429
+ done();
1430
+ } catch (e) {
1431
+ done(e);
1432
+ }
1433
+ }, 20);
1434
+ } catch (e) {
1435
+ done(e);
1436
+ }
1437
+ }, 20);
1438
+ })
1439
+ .catch((e) => done(new Error(e)));
1440
+ });
1441
+
1442
+ it("should reorder keyed data items via patch-render without recreating text nodes", function (done) {
1443
+ let element = document.getElementById("test-patch");
1444
+
1445
+ let d = new Updater(element, {
1446
+ text: "",
1447
+ contentNode: document.createElement("div"),
1448
+ fragmentNode: document.createDocumentFragment(),
1449
+ itemsArray: [],
1450
+ keyedItems: [],
1451
+ keyedDataItems: [
1452
+ { id: "a", label: "Alpha" },
1453
+ { id: "b", label: "Beta" },
1454
+ { id: "c", label: "Gamma" },
1455
+ ],
1456
+ items: [],
1457
+ });
1458
+
1459
+ d.run()
1460
+ .then(() => {
1461
+ setTimeout(() => {
1462
+ try {
1463
+ const target = document.getElementById("patch-keyed-render");
1464
+ const initialNodes = Array.from(target.childNodes);
1465
+ expect(initialNodes.map((node) => node.textContent)).to.deep.equal([
1466
+ "Alpha",
1467
+ "Beta",
1468
+ "Gamma",
1469
+ ]);
1470
+
1471
+ d.getSubject().keyedDataItems = [
1472
+ { id: "c", label: "Gamma" },
1473
+ { id: "a", label: "Alpha" },
1474
+ { id: "b", label: "Beta" },
1475
+ ];
1476
+
1477
+ setTimeout(() => {
1478
+ try {
1479
+ const reorderedNodes = Array.from(target.childNodes);
1480
+ expect(reorderedNodes.map((node) => node.textContent)).to.deep.equal([
1481
+ "Gamma",
1482
+ "Alpha",
1483
+ "Beta",
1484
+ ]);
1485
+ expect(reorderedNodes[0]).to.equal(initialNodes[2]);
1486
+ expect(reorderedNodes[1]).to.equal(initialNodes[0]);
1487
+ expect(reorderedNodes[2]).to.equal(initialNodes[1]);
1488
+ done();
1489
+ } catch (e) {
1490
+ done(e);
1491
+ }
1492
+ }, 20);
1493
+ } catch (e) {
1494
+ done(e);
1495
+ }
1496
+ }, 20);
1497
+ })
1498
+ .catch((e) => done(new Error(e)));
1499
+ });
1500
+
1501
+ it("should update keyed data item text in place via patch-render", function (done) {
1502
+ let element = document.getElementById("test-patch");
1503
+
1504
+ let d = new Updater(element, {
1505
+ text: "",
1506
+ contentNode: document.createElement("div"),
1507
+ fragmentNode: document.createDocumentFragment(),
1508
+ itemsArray: [],
1509
+ keyedItems: [],
1510
+ keyedDataItems: [
1511
+ { id: "a", label: "Alpha" },
1512
+ { id: "b", label: "Beta" },
1513
+ ],
1514
+ items: [],
1515
+ });
1516
+
1517
+ d.run()
1518
+ .then(() => {
1519
+ setTimeout(() => {
1520
+ try {
1521
+ const target = document.getElementById("patch-keyed-render");
1522
+ const betaNode = target.childNodes[1];
1523
+
1524
+ d.getSubject().keyedDataItems = [
1525
+ { id: "a", label: "Alpha" },
1526
+ { id: "b", label: "Bravo" },
1527
+ ];
1528
+
1529
+ setTimeout(() => {
1530
+ try {
1531
+ expect(target.childNodes[1]).to.equal(betaNode);
1532
+ expect(target.childNodes[1].textContent).to.equal("Bravo");
1533
+ done();
1534
+ } catch (e) {
1535
+ done(e);
1536
+ }
1537
+ }, 20);
1538
+ } catch (e) {
1539
+ done(e);
1540
+ }
1541
+ }, 20);
1542
+ })
1543
+ .catch((e) => done(new Error(e)));
1544
+ });
1545
+
1546
+ it("should remove stale keyed data items and append new ones via patch-render", function (done) {
1547
+ let element = document.getElementById("test-patch");
1548
+
1549
+ let d = new Updater(element, {
1550
+ text: "",
1551
+ contentNode: document.createElement("div"),
1552
+ fragmentNode: document.createDocumentFragment(),
1553
+ itemsArray: [],
1554
+ keyedItems: [],
1555
+ keyedDataItems: [
1556
+ { id: "a", label: "Alpha" },
1557
+ { id: "b", label: "Beta" },
1558
+ ],
1559
+ items: [],
1560
+ });
1561
+
1562
+ d.run()
1563
+ .then(() => {
1564
+ setTimeout(() => {
1565
+ try {
1566
+ const target = document.getElementById("patch-keyed-render");
1567
+ const alphaNode = target.childNodes[0];
1568
+ const betaNode = target.childNodes[1];
1569
+
1570
+ d.getSubject().keyedDataItems = [
1571
+ { id: "b", label: "Beta" },
1572
+ { id: "d", label: "Delta" },
1573
+ ];
1574
+
1575
+ setTimeout(() => {
1576
+ try {
1577
+ expect(Array.from(target.childNodes).map((node) => node.textContent)).to.deep.equal([
1578
+ "Beta",
1579
+ "Delta",
1580
+ ]);
1581
+ expect(target.childNodes[0]).to.equal(betaNode);
1582
+ expect(alphaNode.isConnected).to.equal(false);
1583
+ done();
1584
+ } catch (e) {
1585
+ done(e);
1586
+ }
1587
+ }, 20);
1588
+ } catch (e) {
1589
+ done(e);
1590
+ }
1591
+ }, 20);
1592
+ })
1593
+ .catch((e) => done(new Error(e)));
1594
+ });
753
1595
  });
754
1596
  });
755
1597