@schukai/monster 4.148.0 → 4.148.2

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.
@@ -86,6 +86,17 @@ function waitForCondition(check, {timeout = 4000, interval = 25} = {}) {
86
86
  });
87
87
  }
88
88
 
89
+ function configureRemotePaginatedSelect(select) {
90
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
91
+ select.setOption('filter.mode', 'remote');
92
+ select.setOption('mapping.selector', 'items.*');
93
+ select.setOption('mapping.labelTemplate', '${name}');
94
+ select.setOption('mapping.valueTemplate', '${id}');
95
+ select.setOption('mapping.total', 'pagination.total');
96
+ select.setOption('mapping.currentPage', 'pagination.page');
97
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
98
+ }
99
+
89
100
  let Select,
90
101
  SelectStyleSheet,
91
102
  getDefaultSelectPopperPositionProfile,
@@ -397,6 +408,46 @@ describe('Select', function () {
397
408
  }
398
409
  });
399
410
 
411
+ it('should keep the floating layout queue usable after cancelling a running reentrant job', async function () {
412
+ const cancelledPopper = document.createElement('div');
413
+ const nextPopper = document.createElement('div');
414
+ const releasePosition = createDeferred();
415
+ let positionStarted = false;
416
+ let positionFinished = false;
417
+ let nextMutationCount = 0;
418
+
419
+ enqueueFloatingLayout({
420
+ popperElement: cancelledPopper,
421
+ reason: FLOATING_LAYOUT_REASON.POSITION,
422
+ position: async () => {
423
+ positionStarted = true;
424
+ await releasePosition.promise;
425
+ await enqueueFloatingLayout({
426
+ popperElement: cancelledPopper,
427
+ reason: FLOATING_LAYOUT_REASON.SETTLE
428
+ });
429
+ positionFinished = true;
430
+ }
431
+ });
432
+
433
+ await waitForCondition(() => positionStarted === true);
434
+ cancelFloatingLayout(cancelledPopper);
435
+ releasePosition.resolve();
436
+ await waitForCondition(() => positionFinished === true);
437
+ await new Promise(resolve => setTimeout(resolve, 0));
438
+
439
+ enqueueFloatingLayout({
440
+ popperElement: nextPopper,
441
+ reason: FLOATING_LAYOUT_REASON.POSITION,
442
+ mutate: () => {
443
+ nextMutationCount += 1;
444
+ }
445
+ });
446
+ await flushFloatingLayoutQueueForTests();
447
+
448
+ expect(nextMutationCount).to.equal(1);
449
+ });
450
+
400
451
  it('should flush reentrant floating layout queue jobs through the watchdog', async function () {
401
452
  const originalRequestAnimationFrame = global.requestAnimationFrame;
402
453
  const originalCancelAnimationFrame = global.cancelAnimationFrame;
@@ -738,7 +789,7 @@ describe('Select', function () {
738
789
  expect(popper.style.display).to.equal('block');
739
790
  });
740
791
 
741
- it('should allow the popper to become wider than a narrow control', function (done) {
792
+ it('should allow the popper to become wider than a narrow control', async function () {
742
793
  const mocks = document.getElementById('mocks');
743
794
  const select = document.createElement('monster-select');
744
795
 
@@ -764,23 +815,12 @@ describe('Select', function () {
764
815
  y: 100
765
816
  });
766
817
 
767
- setTimeout(() => {
768
- try {
769
- shadowRoot.querySelector('[data-monster-role=container]').click();
770
- setTimeout(() => {
771
- try {
772
- expect(popper.style.minWidth).to.equal('240px');
773
- expect(popper.dataset.monsterWidthBehavior).to.equal('preferred');
774
- expect(popper.dataset.monsterPreferredWidth).to.equal('240');
775
- done();
776
- } catch (e) {
777
- done(e);
778
- }
779
- }, 80);
780
- } catch (e) {
781
- done(e);
782
- }
783
- }, 20);
818
+ await new Promise(resolve => setTimeout(resolve, 20));
819
+ shadowRoot.querySelector('[data-monster-role=container]').click();
820
+ await waitForCondition(() => popper.style.minWidth === '240px');
821
+
822
+ expect(popper.dataset.monsterWidthBehavior).to.equal('preferred');
823
+ expect(popper.dataset.monsterPreferredWidth).to.equal('240');
784
824
  });
785
825
 
786
826
  it('should use fixed positioning inside a control bar', function (done) {
@@ -1136,12 +1176,48 @@ describe('Select', function () {
1136
1176
  ]);
1137
1177
  });
1138
1178
 
1139
- it('should prefer the larger live viewport metrics after a resize', function () {
1179
+ it('should use the reduced visual viewport while a soft keyboard is open', function () {
1180
+ const result = resolveSelectViewportMetrics({
1181
+ layoutWidth: 390,
1182
+ layoutHeight: 844,
1183
+ visualWidth: 390,
1184
+ visualHeight: 480,
1185
+ offsetLeft: 0,
1186
+ offsetTop: 0,
1187
+ padding: 12
1188
+ });
1189
+
1190
+ expect(result.width).to.equal(390);
1191
+ expect(result.height).to.equal(480);
1192
+ expect(result.left).to.equal(0);
1193
+ expect(result.top).to.equal(0);
1194
+ expect(result.padding).to.equal(12);
1195
+ });
1196
+
1197
+ it('should preserve visual viewport offsets for zoomed mobile layouts', function () {
1198
+ const result = resolveSelectViewportMetrics({
1199
+ layoutWidth: 1200,
1200
+ layoutHeight: 900,
1201
+ visualWidth: 600,
1202
+ visualHeight: 450,
1203
+ offsetLeft: 20,
1204
+ offsetTop: 30,
1205
+ padding: 12
1206
+ });
1207
+
1208
+ expect(result.width).to.equal(600);
1209
+ expect(result.height).to.equal(450);
1210
+ expect(result.left).to.equal(20);
1211
+ expect(result.top).to.equal(30);
1212
+ expect(result.padding).to.equal(12);
1213
+ });
1214
+
1215
+ it('should fall back to layout viewport metrics without a visual viewport', function () {
1140
1216
  const result = resolveSelectViewportMetrics({
1141
1217
  layoutWidth: 1400,
1142
1218
  layoutHeight: 900,
1143
- visualWidth: 1024,
1144
- visualHeight: 700,
1219
+ visualWidth: 0,
1220
+ visualHeight: 0,
1145
1221
  offsetLeft: 20,
1146
1222
  offsetTop: 30,
1147
1223
  padding: 12
@@ -1149,8 +1225,8 @@ describe('Select', function () {
1149
1225
 
1150
1226
  expect(result.width).to.equal(1400);
1151
1227
  expect(result.height).to.equal(900);
1152
- expect(result.left).to.equal(20);
1153
- expect(result.top).to.equal(30);
1228
+ expect(result.left).to.equal(0);
1229
+ expect(result.top).to.equal(0);
1154
1230
  expect(result.padding).to.equal(12);
1155
1231
  });
1156
1232
 
@@ -1487,6 +1563,142 @@ describe('Select', function () {
1487
1563
  .catch((e) => done(e));
1488
1564
  }, 50);
1489
1565
  });
1566
+
1567
+ it('should ignore a remote page response that settles after reset', async function () {
1568
+ this.timeout(5000);
1569
+
1570
+ const deferredResponse = createDeferred();
1571
+ let requestStarted = false;
1572
+ global['fetch'] = function () {
1573
+ requestStarted = true;
1574
+ return deferredResponse.promise;
1575
+ };
1576
+
1577
+ const mocks = document.getElementById('mocks');
1578
+ const select = document.createElement('monster-select');
1579
+ configureRemotePaginatedSelect(select);
1580
+ mocks.appendChild(select);
1581
+
1582
+ const request = select.fetch('https://example.com/items?filter=old&page=3');
1583
+ await waitForCondition(() => requestStarted === true);
1584
+
1585
+ select.reset();
1586
+ await waitForCondition(() => {
1587
+ return select.getOption('options').length === 0 && select.getOption('total') === null;
1588
+ });
1589
+
1590
+ deferredResponse.resolve(
1591
+ await createJsonResponse({
1592
+ items: [{id: 'old-3', name: 'Old page 3'}],
1593
+ pagination: {
1594
+ total: 9,
1595
+ page: 3,
1596
+ perPage: 1
1597
+ }
1598
+ })
1599
+ );
1600
+ await request;
1601
+ await new Promise(resolve => setTimeout(resolve, 50));
1602
+
1603
+ const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]');
1604
+ expect(select.getOption('options')).to.deep.equal([]);
1605
+ expect(select.getOption('total')).to.equal(null);
1606
+ expect(pagination.style.display).to.equal('none');
1607
+ expect(pagination.getOption('currentPage')).to.equal(null);
1608
+ expect(pagination.getOption('pages')).to.equal(null);
1609
+ });
1610
+
1611
+ it('should ignore a remote page response after disconnect and reconnect', async function () {
1612
+ this.timeout(5000);
1613
+
1614
+ const deferredResponse = createDeferred();
1615
+ let requestStarted = false;
1616
+ global['fetch'] = function () {
1617
+ requestStarted = true;
1618
+ return deferredResponse.promise;
1619
+ };
1620
+
1621
+ const mocks = document.getElementById('mocks');
1622
+ const select = document.createElement('monster-select');
1623
+ configureRemotePaginatedSelect(select);
1624
+ mocks.appendChild(select);
1625
+
1626
+ const request = select.fetch('https://example.com/items?filter=old&page=2');
1627
+ await waitForCondition(() => requestStarted === true);
1628
+
1629
+ select.remove();
1630
+ deferredResponse.resolve(
1631
+ await createJsonResponse({
1632
+ items: [{id: 'old-2', name: 'Old page 2'}],
1633
+ pagination: {
1634
+ total: 4,
1635
+ page: 2,
1636
+ perPage: 1
1637
+ }
1638
+ })
1639
+ );
1640
+ await request;
1641
+ mocks.appendChild(select);
1642
+ await new Promise(resolve => setTimeout(resolve, 50));
1643
+
1644
+ const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]');
1645
+ expect(select.getOption('options')).to.deep.equal([]);
1646
+ expect(select.getOption('total')).to.equal(null);
1647
+ expect(pagination.style.display).to.equal('none');
1648
+ expect(pagination.getOption('currentPage')).to.equal(null);
1649
+ expect(pagination.getOption('pages')).to.equal(null);
1650
+ });
1651
+
1652
+ it('should keep the newest remote page when responses settle out of order', async function () {
1653
+ this.timeout(5000);
1654
+
1655
+ const page2Response = createDeferred();
1656
+ const page3Response = createDeferred();
1657
+ const requests = [];
1658
+ global['fetch'] = function (url) {
1659
+ const requestUrl = String(url);
1660
+ requests.push(requestUrl);
1661
+ return requestUrl.includes('page=2') ? page2Response.promise : page3Response.promise;
1662
+ };
1663
+
1664
+ const mocks = document.getElementById('mocks');
1665
+ const select = document.createElement('monster-select');
1666
+ configureRemotePaginatedSelect(select);
1667
+ mocks.appendChild(select);
1668
+
1669
+ const request2 = select.fetch('https://example.com/items?filter=all&page=2');
1670
+ const request3 = select.fetch('https://example.com/items?filter=all&page=3');
1671
+ await waitForCondition(() => requests.length === 2);
1672
+
1673
+ page3Response.resolve(
1674
+ await createJsonResponse({
1675
+ items: [{id: 'page-3', name: 'Page 3'}],
1676
+ pagination: {
1677
+ total: 3,
1678
+ page: 3,
1679
+ perPage: 1
1680
+ }
1681
+ })
1682
+ );
1683
+ await request3;
1684
+
1685
+ page2Response.resolve(
1686
+ await createJsonResponse({
1687
+ items: [{id: 'page-2', name: 'Page 2'}],
1688
+ pagination: {
1689
+ total: 3,
1690
+ page: 2,
1691
+ perPage: 1
1692
+ }
1693
+ })
1694
+ );
1695
+ await request2;
1696
+
1697
+ const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]');
1698
+ expect(select.getOption('options').map(option => option.value)).to.deep.equal(['page-3']);
1699
+ expect(pagination.getOption('currentPage')).to.equal(3);
1700
+ expect(pagination.getOption('pages')).to.equal(3);
1701
+ });
1490
1702
  });
1491
1703
 
1492
1704
  describe('document.createElement()', function () {
@@ -2488,6 +2700,47 @@ describe('Select', function () {
2488
2700
  expect(requests).to.have.length(1);
2489
2701
  });
2490
2702
 
2703
+ it('should cancel a pending remote filter request on reset', async function () {
2704
+ this.timeout(5000);
2705
+
2706
+ const mocks = document.getElementById('mocks');
2707
+ const requests = [];
2708
+ global['fetch'] = function (url) {
2709
+ requests.push(String(url));
2710
+ return createJsonResponse({
2711
+ items: [{id: 'alpha', name: 'Alpha'}],
2712
+ pagination: {
2713
+ total: 1,
2714
+ page: 1,
2715
+ perPage: 1
2716
+ }
2717
+ });
2718
+ };
2719
+
2720
+ const select = document.createElement('monster-select');
2721
+ configureRemotePaginatedSelect(select);
2722
+ select.setOption('filter.position', 'popper');
2723
+ mocks.appendChild(select);
2724
+
2725
+ await waitForCondition(() => {
2726
+ return select.shadowRoot.querySelector('[data-monster-role=filter][name="popper-filter"]') instanceof HTMLInputElement;
2727
+ });
2728
+
2729
+ const filterInput = select.shadowRoot.querySelector('[data-monster-role=filter][name="popper-filter"]');
2730
+ filterInput.value = 'alpha';
2731
+ filterInput.dispatchEvent(new Event('input', {
2732
+ bubbles: true,
2733
+ composed: true
2734
+ }));
2735
+ select.reset();
2736
+
2737
+ await new Promise(resolve => setTimeout(resolve, 260));
2738
+
2739
+ expect(requests).to.deep.equal([]);
2740
+ expect(select.getOption('options')).to.deep.equal([]);
2741
+ expect(select.getOption('total')).to.equal(null);
2742
+ });
2743
+
2491
2744
  it('should keep unresolved lookup values visible and mark their badge', function (done) {
2492
2745
  this.timeout(3000);
2493
2746
 
@@ -5,6 +5,111 @@ import {Pathfinder} from "../../../source/data/pathfinder.mjs";
5
5
 
6
6
  describe('Pathfinder', function () {
7
7
 
8
+ describe('mutation path security issue #492', function () {
9
+ const probe = 'monsterPathfinderSecurityProbe';
10
+
11
+ afterEach(function () {
12
+ delete Object.prototype[probe];
13
+ });
14
+
15
+ [
16
+ `__proto__.${probe}`,
17
+ `constructor.prototype.${probe}`,
18
+ ['__proto__', probe],
19
+ ['constructor', 'prototype', probe],
20
+ ].forEach(function (path) {
21
+ it(`setVia(${JSON.stringify(path)}) should reject unsafe mutation paths`, function () {
22
+ try {
23
+ expect(() => new Pathfinder({}).setVia(path, 'polluted')).to.throw(Error);
24
+ expect(Object.prototype).to.not.have.own.property(probe);
25
+ } finally {
26
+ delete Object.prototype[probe];
27
+ }
28
+ });
29
+ });
30
+
31
+ it('deleteVia() should not cross a prototype boundary', function () {
32
+ Object.defineProperty(Object.prototype, probe, {
33
+ configurable: true,
34
+ value: 'preserved',
35
+ writable: true,
36
+ });
37
+
38
+ try {
39
+ expect(() => new Pathfinder({}).deleteVia(`__proto__.${probe}`)).to.throw(Error);
40
+ expect(Object.prototype[probe]).to.equal('preserved');
41
+ } finally {
42
+ delete Object.prototype[probe];
43
+ }
44
+ });
45
+
46
+ it('setVia() should keep safe paths working', function () {
47
+ const subject = {};
48
+ new Pathfinder(subject).setVia('safe.nested.value', true);
49
+ expect(subject).to.deep.equal({safe: {nested: {value: true}}});
50
+ });
51
+ });
52
+
53
+ describe('array and collection path contracts issue #493', function () {
54
+ it('getVia() should accept an array path without mutating it', function () {
55
+ const path = ['a', 'b'];
56
+ const subject = {a: {b: 4}};
57
+
58
+ expect(new Pathfinder(subject).getVia(path)).to.equal(4);
59
+ expect(path).to.deep.equal(['a', 'b']);
60
+ });
61
+
62
+ it('getVia([]) should return the root subject', function () {
63
+ const subject = {a: true};
64
+ expect(new Pathfinder(subject).getVia([])).to.equal(subject);
65
+ });
66
+
67
+ it('setVia() should not mutate an array path', function () {
68
+ const path = ['a', 'b'];
69
+ const subject = {};
70
+
71
+ new Pathfinder(subject).setVia(path, 4);
72
+
73
+ expect(subject).to.deep.equal({a: {b: 4}});
74
+ expect(path).to.deep.equal(['a', 'b']);
75
+ });
76
+
77
+ it('deleteVia() should not mutate an array path', function () {
78
+ const path = ['a', 'b'];
79
+ const subject = {a: {b: 4, c: 5}};
80
+
81
+ new Pathfinder(subject).deleteVia(path);
82
+
83
+ expect(subject).to.deep.equal({a: {c: 5}});
84
+ expect(path).to.deep.equal(['a', 'b']);
85
+ });
86
+
87
+ it('empty mutation paths should be no-ops', function () {
88
+ const setPath = [];
89
+ const deletePath = [];
90
+ const subject = {a: true};
91
+ const pathfinder = new Pathfinder(subject);
92
+
93
+ pathfinder.setVia(setPath, false).deleteVia(deletePath);
94
+
95
+ expect(subject).to.deep.equal({a: true});
96
+ expect(setPath).to.deep.equal([]);
97
+ expect(deletePath).to.deep.equal([]);
98
+ });
99
+
100
+ it('setVia() should add values to a Set', function () {
101
+ const values = new Set(['first']);
102
+ new Pathfinder({values}).setVia('values.next', 'second');
103
+ expect([...values]).to.deep.equal(['first', 'second']);
104
+ });
105
+
106
+ it('setVia() should reject WeakSet writes explicitly', function () {
107
+ const values = new WeakSet();
108
+ expect(() => new Pathfinder({values}).setVia('values.next', {}))
109
+ .to.throw(Error, 'unsupported action for this data type in setValueViaPath');
110
+ });
111
+ });
112
+
8
113
  let convertMapResult = function (r) {
9
114
  if (r instanceof Map) {
10
115
  r = Object.fromEntries(r);
@@ -419,4 +524,3 @@ describe('Pathfinder', function () {
419
524
 
420
525
  });
421
526
  });
422
-