jspsych 8.2.1 → 8.2.3
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 +1 -1
- package/dist/index.browser.js +95 -3
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +7 -7
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +94 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +94 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/plugin-api/MediaAPI.ts +28 -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.2.
|
|
3
|
+
"version": "8.2.3",
|
|
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.
|
|
51
|
+
"@jspsych/config": "^3.3.0",
|
|
52
52
|
"@types/dom-mediacapture-record": "^1.0.11",
|
|
53
53
|
"base64-inline-loader": "^2.0.1",
|
|
54
54
|
"css-loader": "^6.6.0",
|
|
@@ -284,11 +284,38 @@ export class MediaAPI {
|
|
|
284
284
|
private camera_recorder: MediaRecorder = null;
|
|
285
285
|
|
|
286
286
|
initializeCameraRecorder(stream: MediaStream, opts?: MediaRecorderOptions) {
|
|
287
|
+
let mimeType = this.getCompatibleMimeType() || "video/webm";
|
|
288
|
+
const recorderOptions: MediaRecorderOptions = {
|
|
289
|
+
...opts,
|
|
290
|
+
mimeType
|
|
291
|
+
}
|
|
292
|
+
|
|
287
293
|
this.camera_stream = stream;
|
|
288
|
-
const recorder = new MediaRecorder(stream,
|
|
294
|
+
const recorder = new MediaRecorder(stream, recorderOptions);
|
|
289
295
|
this.camera_recorder = recorder;
|
|
290
296
|
}
|
|
291
297
|
|
|
298
|
+
// mimetype checking code adapted from https://github.com/lookit/lookit-jspsych/blob/develop/packages/record/src/videoConfig.ts#L673-L699
|
|
299
|
+
/** returns a compatible mimetype string, or null if none from the array are supported. */
|
|
300
|
+
private getCompatibleMimeType(): string {
|
|
301
|
+
const types = [
|
|
302
|
+
// chrome firefox edge
|
|
303
|
+
"video/webm;codecs=vp9,opus",
|
|
304
|
+
"video/webm;codecs=vp8,opus",
|
|
305
|
+
// general
|
|
306
|
+
"video/mp4;codecs=avc1.42E01E,mp4a.40.2",
|
|
307
|
+
// safari
|
|
308
|
+
"video/mp4;codecs=h264,aac",
|
|
309
|
+
"video/mp4;codecs=hevc,aac",
|
|
310
|
+
]
|
|
311
|
+
for (const mimeType of types) {
|
|
312
|
+
if (MediaRecorder.isTypeSupported(mimeType)) {
|
|
313
|
+
return mimeType;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
292
319
|
getCameraStream(): MediaStream {
|
|
293
320
|
return this.camera_stream;
|
|
294
321
|
}
|
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(
|