@schukai/monster 4.148.2 → 4.148.4

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 (29) hide show
  1. package/package.json +1 -1
  2. package/source/components/datatable/filter/date-presets.mjs +13 -0
  3. package/source/components/datatable/filter/date-range.mjs +12 -0
  4. package/source/components/datatable/filter/date-time.mjs +14 -1
  5. package/source/components/datatable/filter/date.mjs +14 -1
  6. package/source/components/datatable/filter/input.mjs +14 -1
  7. package/source/components/datatable/filter/range.mjs +11 -0
  8. package/source/components/datatable/filter/text-operator.mjs +19 -0
  9. package/source/components/datatable/filter/time.mjs +14 -1
  10. package/source/components/form/buy-box.mjs +2 -0
  11. package/source/components/form/cart-control.mjs +2 -0
  12. package/source/components/form/select.mjs +104 -16
  13. package/source/components/state/state.mjs +2 -0
  14. package/source/data/extend.mjs +7 -1
  15. package/source/dom/customcontrol.mjs +28 -23
  16. package/source/dom/customelement.mjs +105 -39
  17. package/source/dom/util/extract-keys.mjs +2 -1
  18. package/source/dom/util/init-options-from-attributes.mjs +1 -1
  19. package/source/dom/util/set-option-from-attribute.mjs +14 -3
  20. package/test/cases/components/datatable/filter-form-value.mjs +98 -0
  21. package/test/cases/components/form/non-value-form-association.mjs +53 -0
  22. package/test/cases/components/form/select.mjs +275 -0
  23. package/test/cases/data/extend.mjs +27 -1
  24. package/test/cases/dom/customcontrol.mjs +188 -6
  25. package/test/cases/dom/customelement-initfromscripthost.mjs +62 -0
  26. package/test/cases/dom/customelement.mjs +123 -2
  27. package/test/cases/dom/util/extract-keys.mjs +14 -0
  28. package/test/cases/dom/util/init-options-from-attributes.mjs +18 -0
  29. package/test/cases/dom/util/set-option-from-attribute.mjs +151 -0
@@ -97,6 +97,14 @@ function configureRemotePaginatedSelect(select) {
97
97
  select.setOption('mapping.objectsPerPage', 'pagination.perPage');
98
98
  }
99
99
 
100
+ function configureLazySelect(select) {
101
+ select.setOption('url', 'https://example.com/lazy-options');
102
+ select.setOption('features.lazyLoad', true);
103
+ select.setOption('mapping.selector', 'items.*');
104
+ select.setOption('mapping.labelTemplate', '${name}');
105
+ select.setOption('mapping.valueTemplate', '${id}');
106
+ }
107
+
100
108
  let Select,
101
109
  SelectStyleSheet,
102
110
  getDefaultSelectPopperPositionProfile,
@@ -643,6 +651,223 @@ describe('Select', function () {
643
651
 
644
652
  });
645
653
 
654
+ describe('Lazy load lifecycle', function () {
655
+ this.timeout(5000);
656
+
657
+ afterEach(() => {
658
+ const mocks = document.getElementById('mocks');
659
+ mocks.innerHTML = '';
660
+ global['fetch'] = fetchReference;
661
+ });
662
+
663
+ it('should open from the first click after a delayed lazy response renders', async function () {
664
+ const deferredResponse = createDeferred();
665
+ let requestCount = 0;
666
+ global['fetch'] = function () {
667
+ requestCount += 1;
668
+ return deferredResponse.promise;
669
+ };
670
+
671
+ const mocks = document.getElementById('mocks');
672
+ const select = document.createElement('monster-select');
673
+ configureLazySelect(select);
674
+ mocks.appendChild(select);
675
+
676
+ await waitForCondition(() => {
677
+ return select.shadowRoot.querySelector('[data-monster-role=container]') instanceof HTMLElement;
678
+ });
679
+
680
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
681
+ const popper = select.shadowRoot.querySelector('[data-monster-role=popper]');
682
+ container.click();
683
+
684
+ await waitForCondition(() => requestCount === 1);
685
+ container.click();
686
+ await new Promise(resolve => setTimeout(resolve, 250));
687
+ expect(requestCount).to.equal(1);
688
+ deferredResponse.resolve(
689
+ await createJsonResponse({
690
+ items: [{id: 'alpha', name: 'Alpha'}]
691
+ })
692
+ );
693
+
694
+ await waitForCondition(() => {
695
+ return select.shadowRoot.querySelectorAll('[data-monster-role=option]').length === 1;
696
+ });
697
+ await waitForCondition(() => popper.style.display === 'block');
698
+
699
+ expect(requestCount).to.equal(1);
700
+ expect(popper.style.display).to.equal('block');
701
+ });
702
+
703
+ it('should keep a failed lazy load in the error state', async function () {
704
+ let requestCount = 0;
705
+ global['fetch'] = function () {
706
+ requestCount += 1;
707
+ return createJsonResponse({}, 500);
708
+ };
709
+
710
+ const mocks = document.getElementById('mocks');
711
+ const select = document.createElement('monster-select');
712
+ configureLazySelect(select);
713
+ mocks.appendChild(select);
714
+
715
+ await new Promise(resolve => setTimeout(resolve, 50));
716
+ select.shadowRoot.querySelector('[data-monster-role=container]').click();
717
+
718
+ await waitForCondition(() => requestCount === 1);
719
+ await waitForCondition(() => {
720
+ return (select.getAttribute('data-monster-error') ?? '').includes('HTTP error! status: 500');
721
+ });
722
+ await new Promise(resolve => setTimeout(resolve, 350));
723
+
724
+ expect(select.getOption('classes.statusOrRemoveBadge')).to.equal('error');
725
+ expect(select.getAttribute('data-monster-error')).to.not.contain('No options available.');
726
+ });
727
+
728
+ it('should retry a failed lazy load on the next click', async function () {
729
+ let requestCount = 0;
730
+ global['fetch'] = function () {
731
+ requestCount += 1;
732
+ if (requestCount === 1) {
733
+ return createJsonResponse({}, 500);
734
+ }
735
+
736
+ return createJsonResponse({
737
+ items: [{id: 'alpha', name: 'Alpha'}]
738
+ });
739
+ };
740
+
741
+ const mocks = document.getElementById('mocks');
742
+ const select = document.createElement('monster-select');
743
+ configureLazySelect(select);
744
+ mocks.appendChild(select);
745
+
746
+ await new Promise(resolve => setTimeout(resolve, 50));
747
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
748
+ const popper = select.shadowRoot.querySelector('[data-monster-role=popper]');
749
+ container.click();
750
+
751
+ await waitForCondition(() => requestCount === 1);
752
+ await waitForCondition(() => {
753
+ return (select.getAttribute('data-monster-error') ?? '').includes('HTTP error! status: 500');
754
+ });
755
+
756
+ container.click();
757
+
758
+ await waitForCondition(() => requestCount === 2, {timeout: 1500});
759
+ await waitForCondition(() => {
760
+ return select.shadowRoot.querySelectorAll('[data-monster-role=option]').length === 1;
761
+ });
762
+ await waitForCondition(() => popper.style.display === 'block');
763
+
764
+ expect(requestCount).to.equal(2);
765
+ expect(select.getAttribute('data-monster-error') ?? '').to.not.contain('HTTP error! status: 500');
766
+ });
767
+
768
+ it('should retry lazy loading after disconnecting during the request', async function () {
769
+ const firstResponse = createDeferred();
770
+ let requestCount = 0;
771
+ global['fetch'] = function () {
772
+ requestCount += 1;
773
+ if (requestCount === 1) {
774
+ return firstResponse.promise;
775
+ }
776
+
777
+ return createJsonResponse({
778
+ items: [{id: 'fresh', name: 'Fresh'}]
779
+ });
780
+ };
781
+
782
+ const mocks = document.getElementById('mocks');
783
+ const select = document.createElement('monster-select');
784
+ configureLazySelect(select);
785
+ mocks.appendChild(select);
786
+
787
+ await new Promise(resolve => setTimeout(resolve, 50));
788
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
789
+ container.click();
790
+ await waitForCondition(() => requestCount === 1);
791
+
792
+ select.remove();
793
+ mocks.appendChild(select);
794
+ firstResponse.resolve(
795
+ await createJsonResponse({
796
+ items: [{id: 'stale', name: 'Stale'}]
797
+ })
798
+ );
799
+ await new Promise(resolve => setTimeout(resolve, 100));
800
+
801
+ container.click();
802
+
803
+ await waitForCondition(() => requestCount === 2, {timeout: 1500});
804
+ await waitForCondition(() => {
805
+ return select.getOption('options').map(option => option.value).includes('fresh');
806
+ });
807
+ expect(select.getOption('options').map(option => option.value)).to.deep.equal(['fresh']);
808
+ });
809
+
810
+ it('should not start a delayed lazy request after disconnecting', async function () {
811
+ let requestCount = 0;
812
+ global['fetch'] = function () {
813
+ requestCount += 1;
814
+ return createJsonResponse({
815
+ items: [{id: 'fresh', name: 'Fresh'}]
816
+ });
817
+ };
818
+
819
+ const mocks = document.getElementById('mocks');
820
+ const select = document.createElement('monster-select');
821
+ configureLazySelect(select);
822
+ mocks.appendChild(select);
823
+
824
+ await new Promise(resolve => setTimeout(resolve, 50));
825
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
826
+ container.click();
827
+ select.remove();
828
+
829
+ await new Promise(resolve => setTimeout(resolve, 300));
830
+ expect(requestCount).to.equal(0);
831
+
832
+ mocks.appendChild(select);
833
+ container.click();
834
+ await waitForCondition(() => requestCount === 1);
835
+ await waitForCondition(() => {
836
+ return select.getOption('options').map(option => option.value).includes('fresh');
837
+ });
838
+ });
839
+
840
+ it('should not reload a completed lazy select after reconnecting', async function () {
841
+ let requestCount = 0;
842
+ global['fetch'] = function () {
843
+ requestCount += 1;
844
+ return createJsonResponse({
845
+ items: [{id: 'alpha', name: 'Alpha'}]
846
+ });
847
+ };
848
+
849
+ const mocks = document.getElementById('mocks');
850
+ const select = document.createElement('monster-select');
851
+ configureLazySelect(select);
852
+ mocks.appendChild(select);
853
+
854
+ await new Promise(resolve => setTimeout(resolve, 50));
855
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
856
+ const popper = select.shadowRoot.querySelector('[data-monster-role=popper]');
857
+ container.click();
858
+ await waitForCondition(() => popper.style.display === 'block');
859
+
860
+ container.click();
861
+ await waitForCondition(() => popper.style.display === 'none');
862
+ select.remove();
863
+ mocks.appendChild(select);
864
+ container.click();
865
+ await waitForCondition(() => popper.style.display === 'block');
866
+
867
+ expect(requestCount).to.equal(1);
868
+ });
869
+ });
870
+
646
871
  describe('Popper sizing', function () {
647
872
  this.timeout(5000);
648
873
 
@@ -1701,6 +1926,56 @@ describe('Select', function () {
1701
1926
  });
1702
1927
  });
1703
1928
 
1929
+ describe('Remote open lifecycle', function () {
1930
+ this.timeout(5000);
1931
+
1932
+ afterEach(() => {
1933
+ const mocks = document.getElementById('mocks');
1934
+ mocks.innerHTML = '';
1935
+ global['fetch'] = fetchReference;
1936
+ });
1937
+
1938
+ it('should request an immediately entered remote filter only once', async function () {
1939
+ const requests = [];
1940
+ global['fetch'] = function (url) {
1941
+ requests.push(String(url));
1942
+ return createJsonResponse({
1943
+ items: [{id: 'alpha', name: 'Alpha'}]
1944
+ });
1945
+ };
1946
+
1947
+ const mocks = document.getElementById('mocks');
1948
+ const select = document.createElement('monster-select');
1949
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
1950
+ select.setOption('filter.mode', 'remote');
1951
+ select.setOption('filter.position', 'popper');
1952
+ select.setOption('mapping.selector', 'items.*');
1953
+ select.setOption('mapping.labelTemplate', '${name}');
1954
+ select.setOption('mapping.valueTemplate', '${id}');
1955
+ mocks.appendChild(select);
1956
+
1957
+ await waitForCondition(() => {
1958
+ return select.shadowRoot.querySelector('[data-monster-role=filter][name="popper-filter"]') instanceof HTMLInputElement;
1959
+ });
1960
+
1961
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
1962
+ const filterInput = select.shadowRoot.querySelector('[data-monster-role=filter][name="popper-filter"]');
1963
+ container.click();
1964
+ filterInput.value = 'alpha';
1965
+ filterInput.dispatchEvent(new Event('input', {
1966
+ bubbles: true,
1967
+ composed: true
1968
+ }));
1969
+
1970
+ await waitForCondition(() => requests.length >= 1);
1971
+ await new Promise(resolve => setTimeout(resolve, 350));
1972
+
1973
+ expect(requests).to.deep.equal([
1974
+ 'https://example.com/items?filter=alpha&page=1'
1975
+ ]);
1976
+ });
1977
+ });
1978
+
1704
1979
  describe('document.createElement()', function () {
1705
1980
 
1706
1981
  afterEach(() => {
@@ -156,7 +156,33 @@ describe('extend function', () => {
156
156
  const result = extend(target, source);
157
157
  expect(result).to.deep.equal({ a: [3, 4] });
158
158
  });
159
- });
160
159
 
160
+ it('should ignore inherited enumerable properties issue #499', () => {
161
+ const prototype = { inherited: 'unsafe' };
162
+ const source = Object.assign(Object.create(prototype), { own: 'safe' });
163
+
164
+ expect(extend({}, source)).to.deep.equal({ own: 'safe' });
165
+ });
166
+
167
+ [
168
+ '{"__proto__":{"monsterExtendPolluted":"yes"}}',
169
+ '{"nested":{"__proto__":{"monsterExtendPolluted":"yes"}}}',
170
+ '{"constructor":{"prototype":{"monsterExtendPolluted":"yes"}}}',
171
+ ].forEach((json) => {
172
+ it(`should not mutate Object.prototype for ${json} issue #499`, () => {
173
+ delete Object.prototype.monsterExtendPolluted;
174
+
175
+ try {
176
+ const result = extend({}, JSON.parse(json));
177
+
178
+ expect(Object.prototype.monsterExtendPolluted).to.equal(undefined);
179
+ expect(({}).monsterExtendPolluted).to.equal(undefined);
180
+ expect(result).to.be.an('object');
181
+ } finally {
182
+ delete Object.prototype.monsterExtendPolluted;
183
+ }
184
+ });
185
+ });
186
+ });
161
187
 
162
188
 
@@ -88,14 +88,20 @@ describe('DOM', function () {
88
88
 
89
89
  })
90
90
 
91
- it('should return undefined', function () {
91
+ it('should not reflect computed disabledness to the host attribute issue #500', function () {
92
92
  expect(element.formDisabledCallback()).to.be.undefined;
93
93
  expect(element.hasAttribute('disabled')).to.be.false;
94
94
  expect(element.formDisabledCallback(true)).to.be.undefined;
95
- expect(element.hasAttribute('disabled')).to.be.true;
96
- const d = element.getAttribute('disabled');
97
- expect(d).to.not.be.null;
98
-
95
+ expect(element.hasAttribute('disabled')).to.be.false;
96
+ expect(element.getOption('disabled')).to.be.false;
97
+ });
98
+
99
+ it('should preserve an author-provided form attribute issue #500', function () {
100
+ element.setAttribute('form', 'author-form');
101
+
102
+ element.formAssociatedCallback(null);
103
+
104
+ expect(element.getAttribute('form')).to.equal('author-form');
99
105
  });
100
106
 
101
107
  })
@@ -319,9 +325,185 @@ describe('DOM', function () {
319
325
  expect(d.value).to.equal('seed');
320
326
  });
321
327
 
328
+ it('formStateRestoreCallback() should restore through the value setter issue #501', function () {
329
+ const htmlTAG = 'monster-customcontrol-state-restore';
330
+
331
+ class StateRestoreComponent extends CustomControl {
332
+ static getTag() {
333
+ return htmlTAG;
334
+ }
335
+
336
+ get value() {
337
+ return this._value ?? '';
338
+ }
339
+
340
+ set value(value) {
341
+ this._value = value;
342
+ }
343
+ }
344
+
345
+ registerCustomElement(StateRestoreComponent);
346
+ const control = document.createElement(htmlTAG);
347
+ control.value = 'changed';
348
+
349
+ control.formStateRestoreCallback('restored', 'restore');
350
+
351
+ expect(control.value).to.equal('restored');
352
+ });
353
+
354
+ it('should normalize a removed value attribute to an empty string issue #501', async function () {
355
+ const htmlTAG = 'monster-customcontrol-value-removal';
356
+
357
+ class ValueRemovalComponent extends CustomControl {
358
+ static getTag() {
359
+ return htmlTAG;
360
+ }
361
+
362
+ get defaults() {
363
+ return Object.assign({}, super.defaults, {
364
+ shadowMode: false,
365
+ });
366
+ }
367
+
368
+ get value() {
369
+ return this._value ?? '';
370
+ }
371
+
372
+ set value(value) {
373
+ this._value = value;
374
+ this.lastAssignedValue = value;
375
+ }
376
+ }
377
+
378
+ registerCustomElement(ValueRemovalComponent);
379
+ const control = document.createElement(htmlTAG);
380
+ document.body.appendChild(control);
381
+ control.setAttribute('value', 'seed');
382
+ await new Promise((resolve) => setTimeout(resolve, 30));
383
+ control.removeAttribute('value');
384
+ await new Promise((resolve) => setTimeout(resolve, 30));
385
+
386
+ expect(control.lastAssignedValue).to.equal('');
387
+ expect(control.value).to.equal('');
388
+ control.remove();
389
+ });
390
+
391
+ it('should replay a pre-upgrade value once after assembly issue #501', async function () {
392
+ const htmlTAG = 'monster-customcontrol-pre-upgrade';
393
+ const control = document.createElement(htmlTAG);
394
+ control.value = 'assigned-before-definition';
395
+ document.body.appendChild(control);
396
+
397
+ class PreUpgradeComponent extends CustomControl {
398
+ static getTag() {
399
+ return htmlTAG;
400
+ }
401
+
402
+ get defaults() {
403
+ return Object.assign({}, super.defaults, {
404
+ shadowMode: false,
405
+ });
406
+ }
407
+
408
+ get value() {
409
+ return this._value ?? '';
410
+ }
411
+
412
+ set value(value) {
413
+ this._value = value;
414
+ this.setterCalls = (this.setterCalls ?? 0) + 1;
415
+ }
416
+ }
417
+
418
+ registerCustomElement(PreUpgradeComponent);
419
+ await customElements.whenDefined(htmlTAG);
420
+ await Promise.resolve();
421
+
422
+ expect(control).to.be.instanceof(PreUpgradeComponent);
423
+ expect(Object.prototype.hasOwnProperty.call(control, 'value')).to.be.false;
424
+ expect(control.value).to.equal('assigned-before-definition');
425
+ expect(control.setterCalls).to.equal(1);
426
+
427
+ control.remove();
428
+ document.body.appendChild(control);
429
+ await Promise.resolve();
430
+ expect(control.setterCalls).to.equal(1);
431
+ control.remove();
432
+ });
433
+
434
+ it('should preserve native fieldset and explicit form lifecycle issue #500', async function () {
435
+ if (window.navigator.userAgent.includes('jsdom')) {
436
+ this.skip();
437
+ }
438
+
439
+ const htmlTAG = 'monster-customcontrol-native-lifecycle';
440
+
441
+ class NativeLifecycleComponent extends CustomControl {
442
+ static getTag() {
443
+ return htmlTAG;
444
+ }
445
+
446
+ get defaults() {
447
+ return Object.assign({}, super.defaults, {
448
+ shadowMode: false,
449
+ });
450
+ }
451
+
452
+ get value() {
453
+ return this._value ?? '';
454
+ }
455
+
456
+ set value(value) {
457
+ this._value = value ?? '';
458
+ this.setFormValue(this._value);
459
+ }
460
+ }
461
+
462
+ registerCustomElement(NativeLifecycleComponent);
463
+
464
+ const externalForm = document.createElement('form');
465
+ externalForm.id = 'external-form-500';
466
+ document.body.appendChild(externalForm);
467
+
468
+ const externalControl = document.createElement(htmlTAG);
469
+ externalControl.setAttribute('name', 'external');
470
+ externalControl.setAttribute('form', externalForm.id);
471
+ document.body.appendChild(externalControl);
472
+ externalControl.value = 'submitted';
473
+
474
+ expect(new FormData(externalForm).get('external')).to.equal('submitted');
475
+ externalControl.remove();
476
+ expect(externalControl.getAttribute('form')).to.equal(externalForm.id);
477
+ document.body.appendChild(externalControl);
478
+ expect(externalControl.form).to.equal(externalForm);
479
+ expect(new FormData(externalForm).get('external')).to.equal('submitted');
480
+
481
+ const nestedForm = document.createElement('form');
482
+ const fieldset = document.createElement('fieldset');
483
+ const nestedControl = document.createElement(htmlTAG);
484
+ nestedControl.setAttribute('name', 'nested');
485
+ fieldset.appendChild(nestedControl);
486
+ nestedForm.appendChild(fieldset);
487
+ document.body.appendChild(nestedForm);
488
+ nestedControl.value = 'nested-value';
489
+
490
+ fieldset.disabled = true;
491
+ await Promise.resolve();
492
+ expect(nestedControl.hasAttribute('disabled')).to.be.false;
493
+ expect(new FormData(nestedForm).get('nested')).to.equal(null);
494
+
495
+ fieldset.disabled = false;
496
+ await Promise.resolve();
497
+ expect(nestedControl.hasAttribute('disabled')).to.be.false;
498
+ expect(new FormData(nestedForm).get('nested')).to.equal('nested-value');
499
+
500
+ externalControl.remove();
501
+ externalForm.remove();
502
+ nestedForm.remove();
503
+ });
504
+
322
505
 
323
506
  });
324
507
  });
325
508
 
326
509
  });
327
-
@@ -154,6 +154,68 @@ describe('DOM', function () {
154
154
  expect(control.getOption('test')).is.eql(3);
155
155
  expect(control.hasAttribute('data-monster-error')).is.false;
156
156
  });
157
+
158
+ it('should resolve a changed script host attribute issue #506', function () {
159
+ const mocks = document.getElementById('mocks');
160
+ mocks.innerHTML = `
161
+ <div id="call-back-host-a"></div>
162
+ <div id="call-back-host-b"></div>
163
+ <div id="container"></div>
164
+ `;
165
+ document.getElementById('call-back-host-a').resolveHost = () => 'a';
166
+ document.getElementById('call-back-host-b').resolveHost = () => 'b';
167
+
168
+ const control = document.createElement(randomTagNumber);
169
+ control.setAttribute('data-monster-script-host', 'call-back-host-a');
170
+ document.getElementById('container').appendChild(control);
171
+
172
+ expect(control.callCallback('resolveHost', [])).to.equal('a');
173
+ control.setAttribute('data-monster-script-host', 'call-back-host-b');
174
+ expect(control.callCallback('resolveHost', [])).to.equal('b');
175
+ });
176
+
177
+ it('should resolve a replacement host with the same id issue #506', function () {
178
+ const mocks = document.getElementById('mocks');
179
+ mocks.innerHTML = `
180
+ <div id="replaceable-host"></div>
181
+ <div id="container"></div>
182
+ `;
183
+ const original = document.getElementById('replaceable-host');
184
+ original.resolveHost = () => 'original';
185
+ const control = document.createElement(randomTagNumber);
186
+ control.setAttribute('data-monster-script-host', 'replaceable-host');
187
+ document.getElementById('container').appendChild(control);
188
+ expect(control.callCallback('resolveHost', [])).to.equal('original');
189
+
190
+ const replacement = document.createElement('div');
191
+ replacement.id = 'replaceable-host';
192
+ replacement.resolveHost = () => 'replacement';
193
+ original.replaceWith(replacement);
194
+
195
+ expect(control.callCallback('resolveHost', [])).to.equal('replacement');
196
+ });
197
+
198
+ it('should resolve the current ancestor after reparenting issue #506', function () {
199
+ const mocks = document.getElementById('mocks');
200
+ mocks.innerHTML = `
201
+ <div id="ancestor-host"><div id="first-container"></div></div>
202
+ <div><div id="second-container"></div></div>
203
+ `;
204
+ const firstHost = document.getElementById('ancestor-host');
205
+ firstHost.resolveHost = () => 'first';
206
+ const control = document.createElement(randomTagNumber);
207
+ control.setAttribute('data-monster-script-host', 'ancestor-host');
208
+ document.getElementById('first-container').appendChild(control);
209
+ expect(control.callCallback('resolveHost', [])).to.equal('first');
210
+
211
+ firstHost.removeAttribute('id');
212
+ const secondHost = document.getElementById('second-container').parentElement;
213
+ secondHost.id = 'ancestor-host';
214
+ secondHost.resolveHost = () => 'second';
215
+ document.getElementById('second-container').appendChild(control);
216
+
217
+ expect(control.callCallback('resolveHost', [])).to.equal('second');
218
+ });
157
219
  })
158
220
 
159
221
  });