jspsych 8.2.2 → 8.3.0
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/README.md +10 -2
- package/css/jspsych.css +20 -0
- package/dist/index.browser.js +124 -4
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +4 -4
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +123 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +123 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.scss +21 -0
- package/src/modules/plugin-api/KeyboardListenerAPI.ts +90 -1
- package/src/modules/plugins.ts +1 -0
- package/src/timeline/Trial.spec.ts +105 -1
- package/src/timeline/Trial.ts +78 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jspsych",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "Behavioral experiments in a browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@fontsource/open-sans": "4.5.3",
|
|
51
|
-
"@jspsych/config": "^3.3.
|
|
51
|
+
"@jspsych/config": "^3.3.4",
|
|
52
52
|
"@types/dom-mediacapture-record": "^1.0.11",
|
|
53
53
|
"base64-inline-loader": "^2.0.1",
|
|
54
54
|
"css-loader": "^6.6.0",
|
package/src/index.scss
CHANGED
|
@@ -112,6 +112,27 @@
|
|
|
112
112
|
cursor: not-allowed;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/* Keyboard keys, e.g., press <kbd>space</kbd> in instructions */
|
|
116
|
+
|
|
117
|
+
.jspsych-display-element kbd,
|
|
118
|
+
.jspsych-display-element .jspsych-key {
|
|
119
|
+
display: inline-block;
|
|
120
|
+
min-width: 1em;
|
|
121
|
+
padding: 0.15em 0.5em;
|
|
122
|
+
margin: 0 0.1em 2px;
|
|
123
|
+
font-family: "Open Sans", "Arial", sans-serif;
|
|
124
|
+
font-size: 0.8em;
|
|
125
|
+
line-height: 1.4;
|
|
126
|
+
text-align: center;
|
|
127
|
+
white-space: nowrap;
|
|
128
|
+
vertical-align: middle;
|
|
129
|
+
color: #333;
|
|
130
|
+
background-color: #f7f7f7;
|
|
131
|
+
border: 1px solid #ccc;
|
|
132
|
+
border-radius: 4px;
|
|
133
|
+
box-shadow: 0 2px 0 #ccc;
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
/* custom style for input[type="range] (slider) to improve alignment between positions and labels */
|
|
116
137
|
|
|
117
138
|
.jspsych-slider {
|
|
@@ -13,6 +13,18 @@ export interface GetKeyboardResponseOptions {
|
|
|
13
13
|
audio_context_start_time?: number;
|
|
14
14
|
allow_held_key?: boolean;
|
|
15
15
|
minimum_valid_rt?: number;
|
|
16
|
+
wait_for_key_release?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PendingRelease {
|
|
20
|
+
/**
|
|
21
|
+
* physical key (`KeyboardEvent.code`) whose release is being awaited; falls back to the
|
|
22
|
+
* case-normalized key value for synthetic events that do not set `code`.
|
|
23
|
+
*/
|
|
24
|
+
code: string;
|
|
25
|
+
key: string;
|
|
26
|
+
rt: number;
|
|
27
|
+
callback_function: any;
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
export class KeyboardListenerAPI {
|
|
@@ -27,6 +39,8 @@ export class KeyboardListenerAPI {
|
|
|
27
39
|
|
|
28
40
|
private listeners = new Set<KeyboardListener>();
|
|
29
41
|
private heldKeys = new Set<string>();
|
|
42
|
+
private keyDownTimestamps = new Map<string, number>();
|
|
43
|
+
private pendingReleases = new Map<KeyboardListener, PendingRelease>();
|
|
30
44
|
|
|
31
45
|
private areRootListenersRegistered = false;
|
|
32
46
|
|
|
@@ -40,12 +54,21 @@ export class KeyboardListenerAPI {
|
|
|
40
54
|
if (rootElement) {
|
|
41
55
|
rootElement.addEventListener("keydown", this.rootKeydownListener);
|
|
42
56
|
rootElement.addEventListener("keyup", this.rootKeyupListener);
|
|
57
|
+
// When the window loses focus the browser stops delivering keyup events, so a key held at
|
|
58
|
+
// that moment would otherwise never be seen as released. Treat a blur as releasing all keys.
|
|
59
|
+
window.addEventListener("blur", this.rootBlurListener);
|
|
43
60
|
this.areRootListenersRegistered = true;
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
private rootKeydownListener(e: KeyboardEvent) {
|
|
66
|
+
const physicalKey = this.getPhysicalKey(e);
|
|
67
|
+
// Record the press timestamp only for the initial keydown, not for key-repeat events of a key
|
|
68
|
+
// that is already held down (the timestamp is only removed again by the key's keyup).
|
|
69
|
+
if (!this.keyDownTimestamps.has(physicalKey)) {
|
|
70
|
+
this.keyDownTimestamps.set(physicalKey, performance.now());
|
|
71
|
+
}
|
|
49
72
|
// Iterate over a static copy of the listeners set because listeners might add other listeners
|
|
50
73
|
// that we do not want to be included in the loop
|
|
51
74
|
for (const listener of [...this.listeners]) {
|
|
@@ -58,10 +81,53 @@ export class KeyboardListenerAPI {
|
|
|
58
81
|
return this.areResponsesCaseSensitive ? string : string.toLowerCase();
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
/**
|
|
85
|
+
* identifies physical key of a keyboard event, for matching a keydown event with
|
|
86
|
+
* its corresponding keyup event, in the case of a change of shift state while
|
|
87
|
+
* the key is being held.
|
|
88
|
+
*/
|
|
89
|
+
private getPhysicalKey(e: KeyboardEvent) {
|
|
90
|
+
return e.code || this.toLowerCaseIfInsensitive(e.key);
|
|
91
|
+
}
|
|
92
|
+
|
|
61
93
|
private rootKeyupListener(e: KeyboardEvent) {
|
|
94
|
+
const physicalKey = this.getPhysicalKey(e);
|
|
95
|
+
|
|
96
|
+
// match pending releases by physical key so that a change in shift state while the key is held
|
|
97
|
+
// (which changes `e.key`, but not `e.code`) cannot orphan a pending release.
|
|
98
|
+
for (const [listener, pending] of this.pendingReleases) {
|
|
99
|
+
if (pending.code === physicalKey) {
|
|
100
|
+
const pressTimestamp = this.keyDownTimestamps.get(physicalKey);
|
|
101
|
+
const rt_key_duration =
|
|
102
|
+
pressTimestamp === undefined ? null : Math.round(performance.now() - pressTimestamp);
|
|
103
|
+
this.pendingReleases.delete(listener);
|
|
104
|
+
// report the key as it was at keydown, so that the deferred and immediate paths record
|
|
105
|
+
// the same response for the same key press
|
|
106
|
+
pending.callback_function({ key: pending.key, rt: pending.rt, rt_key_duration });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.keyDownTimestamps.delete(physicalKey);
|
|
62
111
|
this.heldKeys.delete(this.toLowerCaseIfInsensitive(e.key));
|
|
63
112
|
}
|
|
64
113
|
|
|
114
|
+
/**
|
|
115
|
+
* When the window loses focus (`blur`), the browser will not deliver the `keyup` events for keys
|
|
116
|
+
* that are currently held, which would otherwise leave stale entries in `heldKeys` and
|
|
117
|
+
* `keyDownTimestamps` (making a key look permanently held) and orphan any pending releases. Treat
|
|
118
|
+
* the blur as a release of every held key: resolve each pending release with
|
|
119
|
+
* `rt_key_duration: null`, since the true hold duration cannot be measured once the release is
|
|
120
|
+
* never observed, then clear the tracked key state.
|
|
121
|
+
*/
|
|
122
|
+
private rootBlurListener() {
|
|
123
|
+
for (const [listener, pending] of this.pendingReleases) {
|
|
124
|
+
this.pendingReleases.delete(listener);
|
|
125
|
+
pending.callback_function({ key: pending.key, rt: pending.rt, rt_key_duration: null });
|
|
126
|
+
}
|
|
127
|
+
this.heldKeys.clear();
|
|
128
|
+
this.keyDownTimestamps.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
65
131
|
private isResponseValid(validResponses: ValidResponses, allowHeldKey: boolean, key: string) {
|
|
66
132
|
// check if key was already held down
|
|
67
133
|
if (!allowHeldKey && this.heldKeys.has(key)) {
|
|
@@ -87,6 +153,7 @@ export class KeyboardListenerAPI {
|
|
|
87
153
|
audio_context_start_time,
|
|
88
154
|
allow_held_key = false,
|
|
89
155
|
minimum_valid_rt = this.minimumValidRt,
|
|
156
|
+
wait_for_key_release = false,
|
|
90
157
|
}: GetKeyboardResponseOptions) {
|
|
91
158
|
if (rt_method !== "performance" && rt_method !== "audio") {
|
|
92
159
|
console.log(
|
|
@@ -125,7 +192,26 @@ export class KeyboardListenerAPI {
|
|
|
125
192
|
this.cancelKeyboardResponse(listener);
|
|
126
193
|
}
|
|
127
194
|
|
|
128
|
-
|
|
195
|
+
if (wait_for_key_release) {
|
|
196
|
+
// defer the callback until this key is released, keyed by the listener handle so that
|
|
197
|
+
// the pending release is cancelled together with the listener (see cancelKeyboardResponse
|
|
198
|
+
// and cancelAllKeyboardResponses).
|
|
199
|
+
//
|
|
200
|
+
// Because the pending release is keyed by listener, each listener tracks only one pending
|
|
201
|
+
// release at a time. With persist: true, if a second valid key is pressed before the first
|
|
202
|
+
// is released, this overwrites the earlier pending release: the superseded press is
|
|
203
|
+
// dropped and only the most recent press's release fires the callback. This is a
|
|
204
|
+
// deliberate trade-off of the single-slot design — note that it differs from the
|
|
205
|
+
// non-deferred persist path, which fires the callback for every valid press.
|
|
206
|
+
this.pendingReleases.set(listener, {
|
|
207
|
+
code: this.getPhysicalKey(e),
|
|
208
|
+
key: e.key,
|
|
209
|
+
rt,
|
|
210
|
+
callback_function,
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
callback_function({ key: e.key, rt });
|
|
214
|
+
}
|
|
129
215
|
}
|
|
130
216
|
};
|
|
131
217
|
|
|
@@ -136,10 +222,13 @@ export class KeyboardListenerAPI {
|
|
|
136
222
|
cancelKeyboardResponse(listener: KeyboardListener) {
|
|
137
223
|
// remove the listener from the set of listeners if it is contained
|
|
138
224
|
this.listeners.delete(listener);
|
|
225
|
+
// also drop any pending release so a deferred callback never fires after cancellation
|
|
226
|
+
this.pendingReleases.delete(listener);
|
|
139
227
|
}
|
|
140
228
|
|
|
141
229
|
cancelAllKeyboardResponses() {
|
|
142
230
|
this.listeners.clear();
|
|
231
|
+
this.pendingReleases.clear();
|
|
143
232
|
}
|
|
144
233
|
|
|
145
234
|
compareKeys(key1: string | null, key2: string | null) {
|
package/src/modules/plugins.ts
CHANGED
|
@@ -235,7 +235,7 @@ describe("Trial", () => {
|
|
|
235
235
|
});
|
|
236
236
|
|
|
237
237
|
it("respects the `save_trial_parameters` parameter", async () => {
|
|
238
|
-
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
238
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
239
239
|
|
|
240
240
|
TestPlugin.setParameterInfos({
|
|
241
241
|
stringParameter1: { type: ParameterType.STRING },
|
|
@@ -560,6 +560,110 @@ describe("Trial", () => {
|
|
|
560
560
|
});
|
|
561
561
|
});
|
|
562
562
|
|
|
563
|
+
describe("with parameter type mismatches", () => {
|
|
564
|
+
//TODO: redo these to expect errors on v9!
|
|
565
|
+
let consoleSpy: jest.SpyInstance;
|
|
566
|
+
|
|
567
|
+
beforeEach(() => {
|
|
568
|
+
consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
afterEach(() => {
|
|
572
|
+
consoleSpy.mockRestore();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("errors on non-boolean values for boolean parameters", async () => {
|
|
576
|
+
TestPlugin.setParameterInfos({
|
|
577
|
+
boolParameter: { type: ParameterType.BOOL },
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// this should work:
|
|
581
|
+
await createTrial({ type: TestPlugin, boolParameter: true }).run();
|
|
582
|
+
|
|
583
|
+
// this shouldn't:
|
|
584
|
+
await createTrial({ type: TestPlugin, boolParameter: "foo" }).run();
|
|
585
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
586
|
+
'A non-boolean value (`foo`) was provided for the boolean parameter "boolParameter" in the "test" plugin.'
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("errors on non-string values for string parameters", async () => {
|
|
591
|
+
TestPlugin.setParameterInfos({
|
|
592
|
+
stringParameter: { type: ParameterType.STRING },
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// this should work:
|
|
596
|
+
await createTrial({ type: TestPlugin, stringParameter: "foo" }).run();
|
|
597
|
+
|
|
598
|
+
// this shouldn't:
|
|
599
|
+
await createTrial({ type: TestPlugin, stringParameter: 1 }).run();
|
|
600
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
601
|
+
'A non-string value (`1`) was provided for the parameter "stringParameter" in the "test" plugin.'
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("errors on non-numeric values for numeric parameters", async () => {
|
|
606
|
+
TestPlugin.setParameterInfos({
|
|
607
|
+
intParameter: { type: ParameterType.INT },
|
|
608
|
+
floatParameter: { type: ParameterType.FLOAT },
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// this should work:
|
|
612
|
+
await createTrial({ type: TestPlugin, intParameter: 1, floatParameter: 1.5 }).run();
|
|
613
|
+
|
|
614
|
+
// this shouldn't:
|
|
615
|
+
await createTrial({ type: TestPlugin, intParameter: "foo", floatParameter: 1.5 }).run();
|
|
616
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
617
|
+
'A non-numeric value (`foo`) was provided for the numeric parameter "intParameter" in the "test" plugin.'
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
await createTrial({ type: TestPlugin, intParameter: 1, floatParameter: "foo" }).run();
|
|
621
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
622
|
+
'A non-numeric value (`foo`) was provided for the numeric parameter "floatParameter" in the "test" plugin.'
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
// this should warn but not error (behavior in v9):
|
|
626
|
+
await createTrial({ type: TestPlugin, intParameter: 1.5, floatParameter: 1.5 }).run();
|
|
627
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
628
|
+
`A float value (\`1.5\`) was provided for the integer parameter "intParameter" in the "test" plugin. The value will be truncated to an integer.`
|
|
629
|
+
);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("errors on non-function values for function parameters", async () => {
|
|
633
|
+
TestPlugin.setParameterInfos({
|
|
634
|
+
functionParameter: { type: ParameterType.FUNCTION },
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// this should work:
|
|
638
|
+
await createTrial({ type: TestPlugin, functionParameter: () => {} }).run();
|
|
639
|
+
|
|
640
|
+
// this shouldn't:
|
|
641
|
+
await createTrial({ type: TestPlugin, functionParameter: "foo" }).run();
|
|
642
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
643
|
+
'A non-function value (`foo`) was provided for the function parameter "functionParameter" in the "test" plugin.'
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("errors on select parameters with values not in the options array", async () => {
|
|
648
|
+
TestPlugin.setParameterInfos({
|
|
649
|
+
selectParameter: {
|
|
650
|
+
type: ParameterType.SELECT,
|
|
651
|
+
options: ["foo", "bar"],
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// this should work:
|
|
656
|
+
await createTrial({ type: TestPlugin, selectParameter: "foo" }).run();
|
|
657
|
+
|
|
658
|
+
// this shouldn't:
|
|
659
|
+
|
|
660
|
+
await createTrial({ type: TestPlugin, selectParameter: "baz" }).run();
|
|
661
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
662
|
+
'The value "baz" is not a valid option for the parameter "selectParameter" in the "test" plugin. Valid options are: foo, bar.'
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
563
667
|
it("respects `default_iti` and `post_trial_gap``", async () => {
|
|
564
668
|
dependencies.getDefaultIti.mockReturnValue(100);
|
|
565
669
|
TestPlugin.setManualFinishTrialMode();
|
package/src/timeline/Trial.ts
CHANGED
|
@@ -360,6 +360,7 @@ export class Trial extends TimelineNode {
|
|
|
360
360
|
for (const [parameterName, parameterConfig] of Object.entries(parameterInfos)) {
|
|
361
361
|
const parameterPath = [...parentParameterPath, parameterName];
|
|
362
362
|
|
|
363
|
+
// evaluate parameter and validate required parameter
|
|
363
364
|
let parameterValue = this.getParameterValue(parameterPath, {
|
|
364
365
|
evaluateFunctions: parameterConfig.type !== ParameterType.FUNCTION,
|
|
365
366
|
replaceResult: (originalResult) => {
|
|
@@ -379,6 +380,83 @@ export class Trial extends TimelineNode {
|
|
|
379
380
|
},
|
|
380
381
|
});
|
|
381
382
|
|
|
383
|
+
// TODO: ensure that this throws an error in v9!
|
|
384
|
+
// major parameter type validation
|
|
385
|
+
if (!parameterConfig.array && parameterValue !== null) {
|
|
386
|
+
switch (parameterConfig.type) {
|
|
387
|
+
case ParameterType.BOOL:
|
|
388
|
+
if (typeof parameterValue !== "boolean") {
|
|
389
|
+
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
390
|
+
console.warn(
|
|
391
|
+
`A non-boolean value (\`${parameterValue}\`) was provided for the boolean parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin.`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
// @ts-ignore falls through
|
|
396
|
+
case ParameterType.KEYS: // "ALL_KEYS", "NO_KEYS", and single key strings are checked here
|
|
397
|
+
if (Array.isArray(parameterValue)) break;
|
|
398
|
+
case ParameterType.STRING:
|
|
399
|
+
case ParameterType.HTML_STRING:
|
|
400
|
+
case ParameterType.KEY:
|
|
401
|
+
case ParameterType.AUDIO:
|
|
402
|
+
case ParameterType.VIDEO:
|
|
403
|
+
case ParameterType.IMAGE:
|
|
404
|
+
if (typeof parameterValue !== "string") {
|
|
405
|
+
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
406
|
+
console.warn(
|
|
407
|
+
`A non-string value (\`${parameterValue}\`) was provided for the parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case ParameterType.FLOAT:
|
|
412
|
+
case ParameterType.INT:
|
|
413
|
+
if (typeof parameterValue !== "number") {
|
|
414
|
+
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
415
|
+
console.warn(
|
|
416
|
+
`A non-numeric value (\`${parameterValue}\`) was provided for the numeric parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin.`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
case ParameterType.FUNCTION:
|
|
421
|
+
if (typeof parameterValue !== "function") {
|
|
422
|
+
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
423
|
+
console.warn(
|
|
424
|
+
`A non-function value (\`${parameterValue}\`) was provided for the function parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin.`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
case ParameterType.SELECT:
|
|
429
|
+
if (!parameterConfig.options) {
|
|
430
|
+
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
431
|
+
console.warn(
|
|
432
|
+
`The "options" array is required for the "select" parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin.`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// truncate floats to integers if the parameter type is INT
|
|
438
|
+
if (parameterConfig.type === ParameterType.INT && parameterValue % 1 !== 0) {
|
|
439
|
+
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
440
|
+
console.warn(
|
|
441
|
+
`A float value (\`${parameterValue}\`) was provided for the integer parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin. The value will be truncated to an integer.`
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
parameterValue = Math.trunc(parameterValue);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (parameterConfig.type === ParameterType.SELECT) {
|
|
449
|
+
if (!parameterConfig.options.includes(parameterValue)) {
|
|
450
|
+
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
451
|
+
console.warn(
|
|
452
|
+
`The value "${parameterValue}" is not a valid option for the parameter "${parameterPathString}" in the "${
|
|
453
|
+
this.pluginInfo.name
|
|
454
|
+
}" plugin. Valid options are: ${parameterConfig.options.join(", ")}.`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// array validation
|
|
382
460
|
if (parameterConfig.array && !Array.isArray(parameterValue)) {
|
|
383
461
|
const parameterPathString = parameterPathArrayToString(parameterPath);
|
|
384
462
|
throw new Error(
|