@schukai/monster 4.148.3 → 4.148.5
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/package.json +1 -1
- package/source/components/datatable/constants.mjs +18 -0
- package/source/components/datatable/datasource/rest.mjs +27 -3
- package/source/components/datatable/save-button.mjs +16 -5
- package/source/components/form/select.mjs +104 -16
- package/test/cases/components/datatable/datasource-rest-lifecycle.mjs +299 -0
- package/test/cases/components/form/select.mjs +275 -0
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.148.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.148.5"}
|
|
@@ -20,6 +20,22 @@ import { ATTRIBUTE_PREFIX } from "../../dom/constants.mjs";
|
|
|
20
20
|
*/
|
|
21
21
|
const STYLE_DISPLAY_MODE_BLOCK = "block";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Datasource lifecycle operation for read requests.
|
|
25
|
+
*
|
|
26
|
+
* @since 4.148.5
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
const DATASOURCE_OPERATION_READ = "read";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Datasource lifecycle operation for write requests.
|
|
33
|
+
*
|
|
34
|
+
* @since 4.148.5
|
|
35
|
+
* @type {string}
|
|
36
|
+
*/
|
|
37
|
+
const DATASOURCE_OPERATION_WRITE = "write";
|
|
38
|
+
|
|
23
39
|
/**
|
|
24
40
|
* This attribute `data-monster-datasource` can be used to pass a datasource.
|
|
25
41
|
*
|
|
@@ -105,6 +121,8 @@ const ATTRIBUTE_DATATABLE_MODE_HIDDEN = "hidden";
|
|
|
105
121
|
const ATTRIBUTE_DATATABLE_MODE_VISIBLE = "visible";
|
|
106
122
|
|
|
107
123
|
export {
|
|
124
|
+
DATASOURCE_OPERATION_READ,
|
|
125
|
+
DATASOURCE_OPERATION_WRITE,
|
|
108
126
|
ATTRIBUTE_DATASOURCE,
|
|
109
127
|
ATTRIBUTE_DATASOURCE_SELECTOR,
|
|
110
128
|
ATTRIBUTE_DATASOURCE_ARGUMENTS,
|
|
@@ -31,6 +31,10 @@ import { Observer } from "../../../types/observer.mjs";
|
|
|
31
31
|
import { Pathfinder } from "../../../data/pathfinder.mjs";
|
|
32
32
|
import { fireCustomEvent } from "../../../dom/events.mjs";
|
|
33
33
|
import { isArray, isFunction, isObject, isString } from "../../../types/is.mjs";
|
|
34
|
+
import {
|
|
35
|
+
DATASOURCE_OPERATION_READ,
|
|
36
|
+
DATASOURCE_OPERATION_WRITE,
|
|
37
|
+
} from "../constants.mjs";
|
|
34
38
|
|
|
35
39
|
export { Rest };
|
|
36
40
|
|
|
@@ -318,6 +322,9 @@ class Rest extends Datasource {
|
|
|
318
322
|
* @fires monster-datasource-fetch
|
|
319
323
|
* @fires monster-datasource-fetched
|
|
320
324
|
* @fires monster-datasource-error
|
|
325
|
+
* @fires monster-datasource-validation
|
|
326
|
+
* @description Lifecycle event details contain `datasource` and
|
|
327
|
+
* `operation: "read"` for this request.
|
|
321
328
|
*/
|
|
322
329
|
read() {
|
|
323
330
|
const opt = clone(this.getOption("read"));
|
|
@@ -372,6 +379,7 @@ class Rest extends Datasource {
|
|
|
372
379
|
return new Promise((resolve, reject) => {
|
|
373
380
|
fireCustomEvent(this, "monster-datasource-fetch", {
|
|
374
381
|
datasource: this,
|
|
382
|
+
operation: DATASOURCE_OPERATION_READ,
|
|
375
383
|
});
|
|
376
384
|
|
|
377
385
|
queueMicrotask(() => {
|
|
@@ -381,15 +389,18 @@ class Rest extends Datasource {
|
|
|
381
389
|
if (this[readRequestIdSymbol] === requestId) {
|
|
382
390
|
fireCustomEvent(this, "monster-datasource-fetched", {
|
|
383
391
|
datasource: this,
|
|
392
|
+
operation: DATASOURCE_OPERATION_READ,
|
|
384
393
|
});
|
|
385
394
|
}
|
|
386
395
|
|
|
387
396
|
resolve(response);
|
|
388
397
|
})
|
|
389
398
|
.catch((error) => {
|
|
390
|
-
handleValidationError.call(this, error);
|
|
399
|
+
handleValidationError.call(this, error, DATASOURCE_OPERATION_READ);
|
|
391
400
|
if (this[readRequestIdSymbol] === requestId) {
|
|
392
401
|
fireCustomEvent(this, "monster-datasource-error", {
|
|
402
|
+
datasource: this,
|
|
403
|
+
operation: DATASOURCE_OPERATION_READ,
|
|
393
404
|
error: error,
|
|
394
405
|
});
|
|
395
406
|
|
|
@@ -404,6 +415,12 @@ class Rest extends Datasource {
|
|
|
404
415
|
/**
|
|
405
416
|
* Writes the data to the rest api.
|
|
406
417
|
* @return {Promise}
|
|
418
|
+
* @fires monster-datasource-fetch
|
|
419
|
+
* @fires monster-datasource-fetched
|
|
420
|
+
* @fires monster-datasource-error
|
|
421
|
+
* @fires monster-datasource-validation
|
|
422
|
+
* @description Lifecycle event details contain `datasource` and
|
|
423
|
+
* `operation: "write"` for this request.
|
|
407
424
|
*/
|
|
408
425
|
write() {
|
|
409
426
|
const opt = clone(this.getOption("write"));
|
|
@@ -435,6 +452,7 @@ class Rest extends Datasource {
|
|
|
435
452
|
return new Promise((resolve, reject) => {
|
|
436
453
|
fireCustomEvent(this, "monster-datasource-fetch", {
|
|
437
454
|
datasource: this,
|
|
455
|
+
operation: DATASOURCE_OPERATION_WRITE,
|
|
438
456
|
});
|
|
439
457
|
|
|
440
458
|
queueMicrotask(() => {
|
|
@@ -444,15 +462,18 @@ class Rest extends Datasource {
|
|
|
444
462
|
if (this[writeRequestIdSymbol] === requestId) {
|
|
445
463
|
fireCustomEvent(this, "monster-datasource-fetched", {
|
|
446
464
|
datasource: this,
|
|
465
|
+
operation: DATASOURCE_OPERATION_WRITE,
|
|
447
466
|
});
|
|
448
467
|
}
|
|
449
468
|
|
|
450
469
|
resolve(response);
|
|
451
470
|
})
|
|
452
471
|
.catch((error) => {
|
|
453
|
-
handleValidationError.call(this, error);
|
|
472
|
+
handleValidationError.call(this, error, DATASOURCE_OPERATION_WRITE);
|
|
454
473
|
if (this[writeRequestIdSymbol] === requestId) {
|
|
455
474
|
fireCustomEvent(this, "monster-datasource-error", {
|
|
475
|
+
datasource: this,
|
|
476
|
+
operation: DATASOURCE_OPERATION_WRITE,
|
|
456
477
|
error: error,
|
|
457
478
|
});
|
|
458
479
|
|
|
@@ -662,9 +683,10 @@ function initIntersectionObserver() {
|
|
|
662
683
|
/**
|
|
663
684
|
* @private
|
|
664
685
|
* @param {Error} error
|
|
686
|
+
* @param {string} operation
|
|
665
687
|
* @return {Promise<void>}
|
|
666
688
|
*/
|
|
667
|
-
function handleValidationError(error) {
|
|
689
|
+
function handleValidationError(error, operation) {
|
|
668
690
|
const path = this.getOption("response.path.validationErrors");
|
|
669
691
|
if (!path) {
|
|
670
692
|
return Promise.resolve();
|
|
@@ -723,6 +745,8 @@ function handleValidationError(error) {
|
|
|
723
745
|
new CustomEvent("monster-datasource-validation", {
|
|
724
746
|
bubbles: true,
|
|
725
747
|
detail: {
|
|
748
|
+
datasource: this,
|
|
749
|
+
operation,
|
|
726
750
|
errors: normalizedErrors,
|
|
727
751
|
raw: rawErrors,
|
|
728
752
|
message,
|
|
@@ -37,7 +37,10 @@ import { Observer } from "../../types/observer.mjs";
|
|
|
37
37
|
import { TokenList } from "../../types/tokenlist.mjs";
|
|
38
38
|
import { clone } from "../../util/clone.mjs";
|
|
39
39
|
import { State } from "../form/types/state.mjs";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
ATTRIBUTE_DATASOURCE_SELECTOR,
|
|
42
|
+
DATASOURCE_OPERATION_WRITE,
|
|
43
|
+
} from "./constants.mjs";
|
|
41
44
|
import { Datasource } from "./datasource.mjs";
|
|
42
45
|
import { Rest as RestDatasource } from "./datasource/rest.mjs";
|
|
43
46
|
import { BadgeStyleSheet } from "../stylesheet/badge.mjs";
|
|
@@ -203,7 +206,10 @@ class SaveButton extends CustomElement {
|
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
if (element instanceof RestDatasource) {
|
|
206
|
-
element.addEventListener("monster-datasource-fetch", () => {
|
|
209
|
+
element.addEventListener("monster-datasource-fetch", (event) => {
|
|
210
|
+
if (event?.detail?.operation === DATASOURCE_OPERATION_WRITE) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
207
213
|
if (self[saveInFlightSymbol]) {
|
|
208
214
|
self[pendingResetSymbol] = true;
|
|
209
215
|
return;
|
|
@@ -211,15 +217,20 @@ class SaveButton extends CustomElement {
|
|
|
211
217
|
self[fetchInFlightSymbol] = true;
|
|
212
218
|
clearOriginValues.call(self);
|
|
213
219
|
});
|
|
214
|
-
element.addEventListener("monster-datasource-fetched", () => {
|
|
215
|
-
|
|
220
|
+
element.addEventListener("monster-datasource-fetched", (event) => {
|
|
221
|
+
if (event?.detail?.operation !== DATASOURCE_OPERATION_WRITE) {
|
|
222
|
+
self[fetchInFlightSymbol] = false;
|
|
223
|
+
}
|
|
216
224
|
setOriginValues.call(
|
|
217
225
|
self,
|
|
218
226
|
clone(self[datasourceLinkedElementSymbol].data),
|
|
219
227
|
);
|
|
220
228
|
updateChangesState.call(self);
|
|
221
229
|
});
|
|
222
|
-
element.addEventListener("monster-datasource-error", () => {
|
|
230
|
+
element.addEventListener("monster-datasource-error", (event) => {
|
|
231
|
+
if (event?.detail?.operation === DATASOURCE_OPERATION_WRITE) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
223
234
|
self[fetchInFlightSymbol] = false;
|
|
224
235
|
});
|
|
225
236
|
}
|
|
@@ -113,6 +113,18 @@ const keyFilterEventSymbol = Symbol("keyFilterEvent");
|
|
|
113
113
|
*/
|
|
114
114
|
const lazyLoadDoneSymbol = Symbol("lazyLoadDone");
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @private
|
|
118
|
+
* @type {Symbol}
|
|
119
|
+
*/
|
|
120
|
+
const lazyLoadRequestSymbol = Symbol("lazyLoadRequest");
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @private
|
|
124
|
+
* @type {Symbol}
|
|
125
|
+
*/
|
|
126
|
+
const lazyLoadErrorSymbol = Symbol("lazyLoadError");
|
|
127
|
+
|
|
116
128
|
/**
|
|
117
129
|
* @private
|
|
118
130
|
* @type {Symbol}
|
|
@@ -300,6 +312,7 @@ const optionsMapVersionSnapshotSymbol = Symbol("optionsMapVersionSnapshot");
|
|
|
300
312
|
const selectionVersionSymbol = Symbol("selectionVersion");
|
|
301
313
|
const closeOnSelectAutoSymbol = Symbol("closeOnSelectAuto");
|
|
302
314
|
const lastFilterValueSymbol = Symbol("lastFilterValue");
|
|
315
|
+
const filterInputVersionSymbol = Symbol("filterInputVersion");
|
|
303
316
|
|
|
304
317
|
/**
|
|
305
318
|
* @private
|
|
@@ -2051,6 +2064,7 @@ function invalidatePendingRemoteWork() {
|
|
|
2051
2064
|
}
|
|
2052
2065
|
this[isLoadingSymbol] = false;
|
|
2053
2066
|
this[remoteInfoRequestSymbol] = null;
|
|
2067
|
+
delete this[lazyLoadRequestSymbol];
|
|
2054
2068
|
|
|
2055
2069
|
if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) {
|
|
2056
2070
|
try {
|
|
@@ -4014,6 +4028,7 @@ function handleFilterInputEvents() {
|
|
|
4014
4028
|
}
|
|
4015
4029
|
|
|
4016
4030
|
this[lastFilterValueSymbol] = filterValue;
|
|
4031
|
+
bumpFilterInputVersion.call(this);
|
|
4017
4032
|
|
|
4018
4033
|
if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) {
|
|
4019
4034
|
try {
|
|
@@ -4061,6 +4076,16 @@ function getCurrentFilterInputValue() {
|
|
|
4061
4076
|
return undefined;
|
|
4062
4077
|
}
|
|
4063
4078
|
|
|
4079
|
+
function getFilterInputVersion() {
|
|
4080
|
+
return Number.isInteger(this[filterInputVersionSymbol])
|
|
4081
|
+
? this[filterInputVersionSymbol]
|
|
4082
|
+
: 0;
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
function bumpFilterInputVersion() {
|
|
4086
|
+
this[filterInputVersionSymbol] = getFilterInputVersion.call(this) + 1;
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4064
4089
|
/**
|
|
4065
4090
|
* @private
|
|
4066
4091
|
*/
|
|
@@ -4601,7 +4626,15 @@ function areOptionsAvailableAndInitInternal() {
|
|
|
4601
4626
|
options === null ||
|
|
4602
4627
|
(isArray(options) && options.length === 0)
|
|
4603
4628
|
) {
|
|
4604
|
-
|
|
4629
|
+
const lazyLoadPending = this[lazyLoadRequestSymbol] instanceof Promise;
|
|
4630
|
+
const lazyLoadError = isString(this[lazyLoadErrorSymbol]);
|
|
4631
|
+
if (lazyLoadError) {
|
|
4632
|
+
setStatusOrRemoveBadges.call(this, "error");
|
|
4633
|
+
} else if (lazyLoadPending) {
|
|
4634
|
+
setStatusOrRemoveBadges.call(this, "loading");
|
|
4635
|
+
} else {
|
|
4636
|
+
setStatusOrRemoveBadges.call(this, "empty");
|
|
4637
|
+
}
|
|
4605
4638
|
if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
|
|
4606
4639
|
if (this[isLoadingSymbol] !== true) {
|
|
4607
4640
|
if (isInteger(this.getOption("total"))) {
|
|
@@ -4615,7 +4648,9 @@ function areOptionsAvailableAndInitInternal() {
|
|
|
4615
4648
|
|
|
4616
4649
|
let msg = this.getOption("labels.no-options-available");
|
|
4617
4650
|
|
|
4618
|
-
if (
|
|
4651
|
+
if (lazyLoadError) {
|
|
4652
|
+
msg = this.getOption("labels.cannot-be-loaded");
|
|
4653
|
+
} else if (
|
|
4619
4654
|
this.getOption("url") !== null &&
|
|
4620
4655
|
this.getOption("features.lazyLoad") === true &&
|
|
4621
4656
|
this[lazyLoadDoneSymbol] !== true
|
|
@@ -4651,7 +4686,11 @@ function areOptionsAvailableAndInitInternal() {
|
|
|
4651
4686
|
if (this.getOption("features.emptyValueIfNoOptions") === true) {
|
|
4652
4687
|
this.value = "";
|
|
4653
4688
|
}
|
|
4654
|
-
if (
|
|
4689
|
+
if (
|
|
4690
|
+
this[isLoadingSymbol] !== true &&
|
|
4691
|
+
lazyLoadPending === false &&
|
|
4692
|
+
lazyLoadError === false
|
|
4693
|
+
) {
|
|
4655
4694
|
addErrorAttribute(this, "No options available.");
|
|
4656
4695
|
}
|
|
4657
4696
|
return false;
|
|
@@ -5087,24 +5126,65 @@ function show() {
|
|
|
5087
5126
|
this.getOption("features.lazyLoad") && this[lazyLoadDoneSymbol] !== true;
|
|
5088
5127
|
|
|
5089
5128
|
if (lazyLoadFlag === true) {
|
|
5090
|
-
this[
|
|
5129
|
+
if (this[lazyLoadRequestSymbol] instanceof Promise) {
|
|
5130
|
+
return;
|
|
5131
|
+
}
|
|
5132
|
+
|
|
5133
|
+
if (isString(this[lazyLoadErrorSymbol])) {
|
|
5134
|
+
removeErrorAttribute(this, this[lazyLoadErrorSymbol]);
|
|
5135
|
+
delete this[lazyLoadErrorSymbol];
|
|
5136
|
+
}
|
|
5137
|
+
removeErrorAttribute(this, "No options available.");
|
|
5091
5138
|
setStatusOrRemoveBadges.call(this, "loading");
|
|
5092
5139
|
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5140
|
+
const lifecycleVersion = getRemoteLifecycleVersion.call(this);
|
|
5141
|
+
const lazyLoadRequest = new Processing(200, () => {
|
|
5142
|
+
if (
|
|
5143
|
+
!isRemoteLifecycleCurrent.call(this, lifecycleVersion) ||
|
|
5144
|
+
this.isConnected !== true
|
|
5145
|
+
) {
|
|
5146
|
+
return;
|
|
5147
|
+
}
|
|
5148
|
+
|
|
5149
|
+
return this.fetch();
|
|
5150
|
+
}).run();
|
|
5151
|
+
this[lazyLoadRequestSymbol] = lazyLoadRequest;
|
|
5152
|
+
|
|
5153
|
+
lazyLoadRequest
|
|
5154
|
+
.then(() => {
|
|
5155
|
+
if (
|
|
5156
|
+
this[lazyLoadRequestSymbol] !== lazyLoadRequest ||
|
|
5157
|
+
!isRemoteLifecycleCurrent.call(this, lifecycleVersion) ||
|
|
5158
|
+
this.isConnected !== true
|
|
5159
|
+
) {
|
|
5160
|
+
return;
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
delete this[lazyLoadRequestSymbol];
|
|
5164
|
+
this[lazyLoadDoneSymbol] = true;
|
|
5165
|
+
delete this[lazyLoadErrorSymbol];
|
|
5166
|
+
checkOptionState.call(this);
|
|
5167
|
+
requestAnimationFrame(() => {
|
|
5168
|
+
if (
|
|
5169
|
+
isRemoteLifecycleCurrent.call(this, lifecycleVersion) &&
|
|
5170
|
+
this.isConnected === true
|
|
5171
|
+
) {
|
|
5098
5172
|
show.call(this);
|
|
5099
|
-
}
|
|
5100
|
-
})
|
|
5101
|
-
.catch((e) => {
|
|
5102
|
-
addErrorAttribute(this, e);
|
|
5103
|
-
setStatusOrRemoveBadges.call(this, "error");
|
|
5173
|
+
}
|
|
5104
5174
|
});
|
|
5105
|
-
|
|
5106
|
-
.run()
|
|
5175
|
+
})
|
|
5107
5176
|
.catch((e) => {
|
|
5177
|
+
if (
|
|
5178
|
+
this[lazyLoadRequestSymbol] !== lazyLoadRequest ||
|
|
5179
|
+
!isRemoteLifecycleCurrent.call(this, lifecycleVersion)
|
|
5180
|
+
) {
|
|
5181
|
+
return;
|
|
5182
|
+
}
|
|
5183
|
+
|
|
5184
|
+
delete this[lazyLoadRequestSymbol];
|
|
5185
|
+
this[lazyLoadDoneSymbol] = false;
|
|
5186
|
+
this[lazyLoadErrorSymbol] =
|
|
5187
|
+
e instanceof Error ? e.message : String(e);
|
|
5108
5188
|
addErrorAttribute(this, e);
|
|
5109
5189
|
setStatusOrRemoveBadges.call(this, "error");
|
|
5110
5190
|
});
|
|
@@ -5141,6 +5221,8 @@ function show() {
|
|
|
5141
5221
|
addAttributeToken(this[controlElementSymbol], "class", "open");
|
|
5142
5222
|
registerWithHost.call(this);
|
|
5143
5223
|
|
|
5224
|
+
const filterInputVersion = getFilterInputVersion.call(this);
|
|
5225
|
+
|
|
5144
5226
|
new Processing(() => {
|
|
5145
5227
|
const shouldLoadRemoteOptions =
|
|
5146
5228
|
getFilterMode.call(self) === FILTER_MODE_REMOTE &&
|
|
@@ -5152,12 +5234,18 @@ function show() {
|
|
|
5152
5234
|
|
|
5153
5235
|
if (shouldLoadDefaultOptions) {
|
|
5154
5236
|
setTimeout(() => {
|
|
5237
|
+
if (getFilterInputVersion.call(this) !== filterInputVersion) {
|
|
5238
|
+
return;
|
|
5239
|
+
}
|
|
5155
5240
|
loadDefaultOptionsFromUrl.call(self).catch((e) => {
|
|
5156
5241
|
addErrorAttribute(self, e);
|
|
5157
5242
|
});
|
|
5158
5243
|
}, 0);
|
|
5159
5244
|
} else if (shouldLoadRemoteOptions) {
|
|
5160
5245
|
setTimeout(() => {
|
|
5246
|
+
if (getFilterInputVersion.call(this) !== filterInputVersion) {
|
|
5247
|
+
return;
|
|
5248
|
+
}
|
|
5161
5249
|
self[cleanupOptionsListSymbol] = true;
|
|
5162
5250
|
filterFromRemote.call(self).catch((e) => {
|
|
5163
5251
|
addErrorAttribute(self, e);
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import * as chai from "chai";
|
|
2
|
+
import { chaiDom } from "../../../util/chai-dom.mjs";
|
|
3
|
+
import { initJSDOM } from "../../../util/jsdom.mjs";
|
|
4
|
+
|
|
5
|
+
const expect = chai.expect;
|
|
6
|
+
chai.use(chaiDom);
|
|
7
|
+
|
|
8
|
+
let DataFetchError;
|
|
9
|
+
|
|
10
|
+
const waitForUpdates = () => new Promise((resolve) => setTimeout(resolve, 30));
|
|
11
|
+
|
|
12
|
+
function createDatasource(id) {
|
|
13
|
+
const datasource = document.createElement("monster-datasource-rest");
|
|
14
|
+
datasource.id = id;
|
|
15
|
+
datasource.setOption("features.autoInit", false);
|
|
16
|
+
datasource.setOption("read.url", "/records/1");
|
|
17
|
+
datasource.setOption("write.url", "/records/1");
|
|
18
|
+
return datasource;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function listenForLifecycle(datasource) {
|
|
22
|
+
const events = [];
|
|
23
|
+
for (const type of [
|
|
24
|
+
"monster-datasource-fetch",
|
|
25
|
+
"monster-datasource-fetched",
|
|
26
|
+
"monster-datasource-error",
|
|
27
|
+
"monster-datasource-validation",
|
|
28
|
+
]) {
|
|
29
|
+
datasource.addEventListener(type, (event) => {
|
|
30
|
+
events.push({ type, detail: event.detail });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return events;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createSaveButton(datasourceSelector) {
|
|
37
|
+
const button = document.createElement("monster-datasource-save-button");
|
|
38
|
+
button.setOption("datasource.selector", datasourceSelector);
|
|
39
|
+
button.setOption("disableWhenNoChanges", true);
|
|
40
|
+
return button;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getStateButton(button) {
|
|
44
|
+
return button.shadowRoot?.querySelector("monster-state-button");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function loadDatasource(datasource, data) {
|
|
48
|
+
datasource.datasource.read = () => {
|
|
49
|
+
datasource.data = data;
|
|
50
|
+
return Promise.resolve({ ok: true });
|
|
51
|
+
};
|
|
52
|
+
await datasource.read();
|
|
53
|
+
await waitForUpdates();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clickSaveButton(button) {
|
|
57
|
+
const action = getStateButton(button)?.getOption("actions.click");
|
|
58
|
+
expect(action).to.be.a("function");
|
|
59
|
+
action();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function expectPendingChanges(button) {
|
|
63
|
+
expect(Number(button.getOption("changes"))).to.be.greaterThan(0);
|
|
64
|
+
expect(getStateButton(button)?.getOption("disabled")).to.equal(false);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expectNoPendingChanges(button) {
|
|
68
|
+
expect(button.getOption("changes")).to.equal("");
|
|
69
|
+
expect(getStateButton(button)?.getOption("disabled")).to.equal(true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("REST datasource lifecycle", function () {
|
|
73
|
+
before(async function () {
|
|
74
|
+
await initJSDOM();
|
|
75
|
+
globalThis.location = window.location;
|
|
76
|
+
await import("element-internals-polyfill").catch(() => {});
|
|
77
|
+
await import("../../../../source/components/datatable/datasource/rest.mjs");
|
|
78
|
+
await import("../../../../source/components/datatable/save-button.mjs");
|
|
79
|
+
({ DataFetchError } = await import(
|
|
80
|
+
"../../../../source/data/datasource/server/restapi/data-fetch-error.mjs"
|
|
81
|
+
));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
document.getElementById("mocks").innerHTML = "";
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
document.getElementById("mocks").innerHTML = "";
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("identifies read and write lifecycle events without changing event names", async function () {
|
|
93
|
+
const datasource = createDatasource("lifecycle-datasource");
|
|
94
|
+
const events = listenForLifecycle(datasource);
|
|
95
|
+
document.getElementById("mocks").appendChild(datasource);
|
|
96
|
+
|
|
97
|
+
datasource.datasource.read = () => Promise.resolve({ ok: true });
|
|
98
|
+
await datasource.read();
|
|
99
|
+
|
|
100
|
+
datasource.datasource.write = () => Promise.resolve({ ok: true });
|
|
101
|
+
await datasource.write();
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
events.map(({ type, detail }) => [type, detail.operation]),
|
|
105
|
+
).to.deep.equal([
|
|
106
|
+
["monster-datasource-fetch", "read"],
|
|
107
|
+
["monster-datasource-fetched", "read"],
|
|
108
|
+
["monster-datasource-fetch", "write"],
|
|
109
|
+
["monster-datasource-fetched", "write"],
|
|
110
|
+
]);
|
|
111
|
+
events.forEach(({ detail }) => {
|
|
112
|
+
expect(detail.datasource).to.equal(datasource);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("identifies write validation and error events", async function () {
|
|
117
|
+
const datasource = createDatasource("validation-datasource");
|
|
118
|
+
datasource.setOption(
|
|
119
|
+
"response.path.validationErrors",
|
|
120
|
+
"meta.validationErrors",
|
|
121
|
+
);
|
|
122
|
+
datasource.setOption("response.path.validationMessage", "meta.message");
|
|
123
|
+
const events = listenForLifecycle(datasource);
|
|
124
|
+
document.getElementById("mocks").appendChild(datasource);
|
|
125
|
+
|
|
126
|
+
const response = new Response(
|
|
127
|
+
JSON.stringify({
|
|
128
|
+
meta: {
|
|
129
|
+
message: "Input data is invalid",
|
|
130
|
+
validationErrors: {
|
|
131
|
+
parentId: { code: "VAL_INVALID", message: "Invalid parent" },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
{ status: 400, headers: { "content-type": "application/json" } },
|
|
136
|
+
);
|
|
137
|
+
datasource.datasource.write = () =>
|
|
138
|
+
Promise.reject(new DataFetchError("Input data is invalid", response));
|
|
139
|
+
|
|
140
|
+
await datasource.write().catch(() => {});
|
|
141
|
+
await waitForUpdates();
|
|
142
|
+
|
|
143
|
+
const validation = events.find(
|
|
144
|
+
({ type }) => type === "monster-datasource-validation",
|
|
145
|
+
);
|
|
146
|
+
const error = events.find(
|
|
147
|
+
({ type }) => type === "monster-datasource-error",
|
|
148
|
+
);
|
|
149
|
+
expect(validation?.detail.operation).to.equal("write");
|
|
150
|
+
expect(validation?.detail.datasource).to.equal(datasource);
|
|
151
|
+
expect(validation?.detail.errors.parentId).to.deep.equal({
|
|
152
|
+
code: "VAL_INVALID",
|
|
153
|
+
message: "Invalid parent",
|
|
154
|
+
});
|
|
155
|
+
expect(error?.detail.operation).to.equal("write");
|
|
156
|
+
expect(error?.detail.datasource).to.equal(datasource);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("keeps every save button on the loaded baseline after a rejected write", async function () {
|
|
160
|
+
const datasource = createDatasource("rejected-write-datasource");
|
|
161
|
+
const loaded = {
|
|
162
|
+
data: [{ id: "area-1", name: "Inbound", parentAreaId: "" }],
|
|
163
|
+
};
|
|
164
|
+
const invalid = {
|
|
165
|
+
data: [{ id: "area-1", name: "Inbound changed", parentAreaId: "area-1" }],
|
|
166
|
+
};
|
|
167
|
+
const corrected = {
|
|
168
|
+
data: [{ id: "area-1", name: "Inbound changed", parentAreaId: "" }],
|
|
169
|
+
};
|
|
170
|
+
const first = createSaveButton("#rejected-write-datasource");
|
|
171
|
+
const second = createSaveButton("#rejected-write-datasource");
|
|
172
|
+
const mocks = document.getElementById("mocks");
|
|
173
|
+
mocks.append(datasource, first, second);
|
|
174
|
+
await loadDatasource(datasource, loaded);
|
|
175
|
+
expectNoPendingChanges(first);
|
|
176
|
+
expectNoPendingChanges(second);
|
|
177
|
+
|
|
178
|
+
datasource.data = invalid;
|
|
179
|
+
await waitForUpdates();
|
|
180
|
+
expectPendingChanges(first);
|
|
181
|
+
expectPendingChanges(second);
|
|
182
|
+
|
|
183
|
+
datasource.datasource.write = () =>
|
|
184
|
+
Promise.reject(new Error("Rejected write"));
|
|
185
|
+
clickSaveButton(first);
|
|
186
|
+
await waitForUpdates();
|
|
187
|
+
expectPendingChanges(first);
|
|
188
|
+
expectPendingChanges(second);
|
|
189
|
+
|
|
190
|
+
datasource.data = corrected;
|
|
191
|
+
await waitForUpdates();
|
|
192
|
+
expectPendingChanges(first);
|
|
193
|
+
expectPendingChanges(second);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("establishes the current data as every save button baseline after a successful write", async function () {
|
|
197
|
+
const datasource = createDatasource("successful-write-datasource");
|
|
198
|
+
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
199
|
+
const edited = { data: [{ id: "area-1", name: "Inbound changed" }] };
|
|
200
|
+
const first = createSaveButton("#successful-write-datasource");
|
|
201
|
+
const second = createSaveButton("#successful-write-datasource");
|
|
202
|
+
const mocks = document.getElementById("mocks");
|
|
203
|
+
mocks.append(datasource, first, second);
|
|
204
|
+
await loadDatasource(datasource, loaded);
|
|
205
|
+
datasource.data = edited;
|
|
206
|
+
await waitForUpdates();
|
|
207
|
+
expectPendingChanges(first);
|
|
208
|
+
expectPendingChanges(second);
|
|
209
|
+
|
|
210
|
+
datasource.datasource.write = () => Promise.resolve({ ok: true });
|
|
211
|
+
clickSaveButton(first);
|
|
212
|
+
await waitForUpdates();
|
|
213
|
+
|
|
214
|
+
expectNoPendingChanges(first);
|
|
215
|
+
expectNoPendingChanges(second);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("replaces every save button baseline after a read", async function () {
|
|
219
|
+
const datasource = createDatasource("read-baseline-datasource");
|
|
220
|
+
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
221
|
+
const edited = { data: [{ id: "area-1", name: "Inbound changed" }] };
|
|
222
|
+
const refreshed = { data: [{ id: "area-1", name: "Inbound from server" }] };
|
|
223
|
+
const first = createSaveButton("#read-baseline-datasource");
|
|
224
|
+
const second = createSaveButton("#read-baseline-datasource");
|
|
225
|
+
const mocks = document.getElementById("mocks");
|
|
226
|
+
mocks.append(datasource, first, second);
|
|
227
|
+
await loadDatasource(datasource, loaded);
|
|
228
|
+
datasource.data = edited;
|
|
229
|
+
await waitForUpdates();
|
|
230
|
+
expectPendingChanges(first);
|
|
231
|
+
expectPendingChanges(second);
|
|
232
|
+
|
|
233
|
+
datasource.datasource.read = () => {
|
|
234
|
+
datasource.data = refreshed;
|
|
235
|
+
return Promise.resolve({ ok: true });
|
|
236
|
+
};
|
|
237
|
+
await datasource.read();
|
|
238
|
+
await waitForUpdates();
|
|
239
|
+
|
|
240
|
+
expectNoPendingChanges(first);
|
|
241
|
+
expectNoPendingChanges(second);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("does not let write completion end an in-flight read", async function () {
|
|
245
|
+
const datasource = createDatasource("concurrent-datasource");
|
|
246
|
+
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
247
|
+
const firstReadValue = {
|
|
248
|
+
data: [{ id: "area-1", name: "Inbound from server" }],
|
|
249
|
+
};
|
|
250
|
+
const finalReadValue = {
|
|
251
|
+
data: [{ id: "area-1", name: "Inbound from server (final)" }],
|
|
252
|
+
};
|
|
253
|
+
const first = createSaveButton("#concurrent-datasource");
|
|
254
|
+
const second = createSaveButton("#concurrent-datasource");
|
|
255
|
+
const mocks = document.getElementById("mocks");
|
|
256
|
+
mocks.append(datasource, first, second);
|
|
257
|
+
await loadDatasource(datasource, loaded);
|
|
258
|
+
expectNoPendingChanges(first);
|
|
259
|
+
expectNoPendingChanges(second);
|
|
260
|
+
|
|
261
|
+
let resolveRead;
|
|
262
|
+
datasource.datasource.read = () =>
|
|
263
|
+
new Promise((resolve) => {
|
|
264
|
+
resolveRead = resolve;
|
|
265
|
+
});
|
|
266
|
+
const read = datasource.read();
|
|
267
|
+
await waitForUpdates();
|
|
268
|
+
expectNoPendingChanges(first);
|
|
269
|
+
expectNoPendingChanges(second);
|
|
270
|
+
|
|
271
|
+
datasource.datasource.write = () => Promise.resolve({ ok: true });
|
|
272
|
+
await datasource.write();
|
|
273
|
+
expectNoPendingChanges(first);
|
|
274
|
+
expectNoPendingChanges(second);
|
|
275
|
+
|
|
276
|
+
datasource.data = firstReadValue;
|
|
277
|
+
await waitForUpdates();
|
|
278
|
+
expectNoPendingChanges(first);
|
|
279
|
+
expectNoPendingChanges(second);
|
|
280
|
+
|
|
281
|
+
datasource.datasource.write = () =>
|
|
282
|
+
Promise.reject(new Error("Rejected concurrent write"));
|
|
283
|
+
await datasource.write().catch(() => {});
|
|
284
|
+
expectNoPendingChanges(first);
|
|
285
|
+
expectNoPendingChanges(second);
|
|
286
|
+
|
|
287
|
+
datasource.data = finalReadValue;
|
|
288
|
+
await waitForUpdates();
|
|
289
|
+
|
|
290
|
+
expectNoPendingChanges(first);
|
|
291
|
+
expectNoPendingChanges(second);
|
|
292
|
+
|
|
293
|
+
resolveRead({ ok: true });
|
|
294
|
+
await read;
|
|
295
|
+
await waitForUpdates();
|
|
296
|
+
expectNoPendingChanges(first);
|
|
297
|
+
expectNoPendingChanges(second);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -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(() => {
|