jspsych 8.0.3 → 8.2.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 +1 -1
- package/dist/index.browser.js +1350 -400
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +7 -6
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +251 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +28 -40
- package/dist/index.js +251 -115
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/ExtensionManager.spec.ts +1 -1
- package/src/JsPsych.ts +46 -0
- package/src/modules/extensions.ts +1 -0
- package/src/modules/plugins.ts +1 -0
- package/src/modules/randomization.ts +1 -1
- package/src/timeline/Timeline.spec.ts +105 -5
- package/src/timeline/Timeline.ts +12 -9
- package/src/timeline/Trial.spec.ts +15 -1
- package/src/timeline/Trial.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jspsych",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.2.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.0
|
|
51
|
+
"@jspsych/config": "^3.2.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Class } from "type-fest";
|
|
2
2
|
|
|
3
|
-
import { TestExtension } from "../tests/extensions/
|
|
3
|
+
import { TestExtension } from "../tests/extensions/TestExtension";
|
|
4
4
|
import { ExtensionManager, ExtensionManagerDependencies } from "./ExtensionManager";
|
|
5
5
|
import { JsPsych } from "./JsPsych";
|
|
6
6
|
import { JsPsychExtension } from "./modules/extensions";
|
package/src/JsPsych.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import autoBind from "auto-bind";
|
|
2
|
+
// To work with citations
|
|
3
|
+
import { Class } from "type-fest";
|
|
2
4
|
|
|
3
5
|
import { version } from "../package.json";
|
|
4
6
|
import { ExtensionManager, ExtensionManagerDependencies } from "./ExtensionManager";
|
|
5
7
|
import { JsPsychData, JsPsychDataDependencies } from "./modules/data";
|
|
8
|
+
import { JsPsychExtension } from "./modules/extensions";
|
|
6
9
|
import { PluginAPI, createJointPluginAPIObject } from "./modules/plugin-api";
|
|
10
|
+
import { JsPsychPlugin } from "./modules/plugins";
|
|
7
11
|
import * as randomization from "./modules/randomization";
|
|
8
12
|
import * as turk from "./modules/turk";
|
|
9
13
|
import * as utils from "./modules/utils";
|
|
@@ -257,6 +261,48 @@ export class JsPsych {
|
|
|
257
261
|
return this.timeline?.description.timeline;
|
|
258
262
|
}
|
|
259
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Prints out a string containing citations for the jsPsych library and all input plugins/extensions in the specified format.
|
|
266
|
+
* If called without input, prints citation for jsPsych library.
|
|
267
|
+
*
|
|
268
|
+
* @param plugins The plugins/extensions to generate citations for. Always prints the citation for the jsPsych library at the top.
|
|
269
|
+
* @param format The desired output citation format. Currently supports "apa" and "bibtex".
|
|
270
|
+
* @returns String containing citations separated with newline character.
|
|
271
|
+
*/
|
|
272
|
+
getCitations(
|
|
273
|
+
plugins: Array<Class<JsPsychPlugin<any>> | Class<JsPsychExtension>> = [],
|
|
274
|
+
format: "apa" | "bibtex" = "apa"
|
|
275
|
+
) {
|
|
276
|
+
const formatOptions = ["apa", "bibtex"];
|
|
277
|
+
// prettier-ignore
|
|
278
|
+
const jsPsychCitations: any = '__CITATIONS__';
|
|
279
|
+
format = format.toLowerCase() as "apa" | "bibtex";
|
|
280
|
+
// Check if plugins is an array
|
|
281
|
+
if (!Array.isArray(plugins)) {
|
|
282
|
+
throw new Error("Expected array of plugins/extensions");
|
|
283
|
+
}
|
|
284
|
+
// Check if format is supported
|
|
285
|
+
else if (!formatOptions.includes(format)) {
|
|
286
|
+
throw new Error("Unsupported citation format");
|
|
287
|
+
}
|
|
288
|
+
// Print citations
|
|
289
|
+
else {
|
|
290
|
+
const jsPsychCitation = jsPsychCitations[format];
|
|
291
|
+
const citationSet = new Set([jsPsychCitation]);
|
|
292
|
+
|
|
293
|
+
for (const plugin of plugins) {
|
|
294
|
+
try {
|
|
295
|
+
const pluginCitation = plugin["info"].citations[format];
|
|
296
|
+
citationSet.add(pluginCitation);
|
|
297
|
+
} catch {
|
|
298
|
+
console.error(`${plugin} does not have citation in ${format} format.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const citationList = Array.from(citationSet).join("\n");
|
|
302
|
+
return citationList;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
260
306
|
get extensions() {
|
|
261
307
|
return this.extensionManager?.extensions ?? {};
|
|
262
308
|
}
|
package/src/modules/plugins.ts
CHANGED
|
@@ -58,6 +58,109 @@ describe("Timeline", () => {
|
|
|
58
58
|
expect((children[1] as Timeline).children.map((child) => child.index)).toEqual([1, 2]);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
it("respects dynamically added child node descriptions", async () => {
|
|
62
|
+
TestPlugin.setManualFinishTrialMode();
|
|
63
|
+
|
|
64
|
+
const timelineDescription: TimelineArray = [{ type: TestPlugin }];
|
|
65
|
+
const timeline = createTimeline(timelineDescription);
|
|
66
|
+
|
|
67
|
+
const runPromise = timeline.run();
|
|
68
|
+
expect(timeline.children.length).toEqual(1);
|
|
69
|
+
|
|
70
|
+
timelineDescription.push({ timeline: [{ type: TestPlugin }] });
|
|
71
|
+
await TestPlugin.finishTrial();
|
|
72
|
+
await TestPlugin.finishTrial();
|
|
73
|
+
await runPromise;
|
|
74
|
+
|
|
75
|
+
expect(timeline.children.length).toEqual(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("dynamically added child node descriptions before a node after it has been run", async () => {
|
|
79
|
+
TestPlugin.setManualFinishTrialMode();
|
|
80
|
+
|
|
81
|
+
const timelineDescription: TimelineArray = [{ type: TestPlugin }];
|
|
82
|
+
const timeline = createTimeline(timelineDescription);
|
|
83
|
+
|
|
84
|
+
const runPromise = timeline.run();
|
|
85
|
+
expect(timeline.children.length).toEqual(1);
|
|
86
|
+
|
|
87
|
+
await TestPlugin.finishTrial();
|
|
88
|
+
timelineDescription.splice(0, 0, { timeline: [{ type: TestPlugin }] });
|
|
89
|
+
await TestPlugin.finishTrial();
|
|
90
|
+
await runPromise;
|
|
91
|
+
|
|
92
|
+
expect(timeline.children.length).toEqual(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("respects dynamically removed end child node descriptions", async () => {
|
|
96
|
+
TestPlugin.setManualFinishTrialMode();
|
|
97
|
+
|
|
98
|
+
const timelineDescription: TimelineArray = [
|
|
99
|
+
{ type: TestPlugin },
|
|
100
|
+
{ timeline: [{ type: TestPlugin }] },
|
|
101
|
+
{ type: TestPlugin },
|
|
102
|
+
];
|
|
103
|
+
const timeline = createTimeline(timelineDescription);
|
|
104
|
+
|
|
105
|
+
const runPromise = timeline.run();
|
|
106
|
+
expect(timeline.children.length).toEqual(1); // Only the first child is instantiated because they are incrementally instantiated now
|
|
107
|
+
|
|
108
|
+
timelineDescription.pop();
|
|
109
|
+
await TestPlugin.finishTrial();
|
|
110
|
+
await TestPlugin.finishTrial();
|
|
111
|
+
await runPromise;
|
|
112
|
+
|
|
113
|
+
expect(timeline.children.length).toEqual(2);
|
|
114
|
+
expect(timeline.children).toEqual([expect.any(Trial), expect.any(Timeline)]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("respects dynamically removed middle child node descriptions", async () => {
|
|
118
|
+
TestPlugin.setManualFinishTrialMode();
|
|
119
|
+
|
|
120
|
+
const timelineDescription: TimelineArray = [
|
|
121
|
+
{ type: TestPlugin },
|
|
122
|
+
{ timeline: [{ type: TestPlugin }] },
|
|
123
|
+
{ type: TestPlugin },
|
|
124
|
+
];
|
|
125
|
+
const timeline = createTimeline(timelineDescription);
|
|
126
|
+
|
|
127
|
+
const runPromise = timeline.run();
|
|
128
|
+
expect(timeline.children.length).toEqual(1);
|
|
129
|
+
|
|
130
|
+
timelineDescription.splice(1, 1);
|
|
131
|
+
await TestPlugin.finishTrial();
|
|
132
|
+
await TestPlugin.finishTrial();
|
|
133
|
+
await runPromise;
|
|
134
|
+
|
|
135
|
+
expect(timeline.children.length).toEqual(2);
|
|
136
|
+
expect(timeline.children).toEqual([expect.any(Trial), expect.any(Trial)]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("dynamically remove first node after running it", async () => {
|
|
140
|
+
TestPlugin.setManualFinishTrialMode();
|
|
141
|
+
|
|
142
|
+
const timelineDescription: TimelineArray = [
|
|
143
|
+
{ type: TestPlugin, data: { I: 0 } },
|
|
144
|
+
{ timeline: [{ type: TestPlugin, data: { I: 1 } }] },
|
|
145
|
+
{ type: TestPlugin, data: { I: 2 } },
|
|
146
|
+
{ type: TestPlugin, data: { I: 3 } },
|
|
147
|
+
];
|
|
148
|
+
const timeline = createTimeline(timelineDescription);
|
|
149
|
+
|
|
150
|
+
const runPromise = timeline.run();
|
|
151
|
+
await TestPlugin.finishTrial();
|
|
152
|
+
timelineDescription.shift();
|
|
153
|
+
await TestPlugin.finishTrial();
|
|
154
|
+
await TestPlugin.finishTrial();
|
|
155
|
+
await runPromise;
|
|
156
|
+
|
|
157
|
+
expect(timeline.children.length).toEqual(3);
|
|
158
|
+
expect(timeline.children[0].getDataParameter().I).toEqual(0);
|
|
159
|
+
const secondChildDescription = timeline.children[1].description as TimelineDescription;
|
|
160
|
+
expect(secondChildDescription["timeline"][0]).toHaveProperty("data.I", 1);
|
|
161
|
+
expect(timeline.children[2].getDataParameter().I).toEqual(3);
|
|
162
|
+
});
|
|
163
|
+
|
|
61
164
|
describe("with `pause()` and `resume()` calls`", () => {
|
|
62
165
|
beforeEach(() => {
|
|
63
166
|
TestPlugin.setManualFinishTrialMode();
|
|
@@ -84,12 +187,9 @@ describe("Timeline", () => {
|
|
|
84
187
|
|
|
85
188
|
await TestPlugin.finishTrial();
|
|
86
189
|
expect(timeline.children[1].getStatus()).toBe(TimelineNodeStatus.COMPLETED);
|
|
87
|
-
expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.PENDING);
|
|
88
|
-
|
|
89
|
-
// Resolving the next trial promise shouldn't continue the experiment since no trial should be running.
|
|
90
|
-
await TestPlugin.finishTrial();
|
|
91
190
|
|
|
92
|
-
|
|
191
|
+
// The timeline is paused, so it shouldn't have instantiated the next child node yet.
|
|
192
|
+
expect(timeline.children.length).toEqual(2);
|
|
93
193
|
|
|
94
194
|
timeline.resume();
|
|
95
195
|
await flushPromises();
|
package/src/timeline/Timeline.ts
CHANGED
|
@@ -75,7 +75,9 @@ export class Timeline extends TimelineNode {
|
|
|
75
75
|
for (const timelineVariableIndex of timelineVariableOrder) {
|
|
76
76
|
this.setCurrentTimelineVariablesByIndex(timelineVariableIndex);
|
|
77
77
|
|
|
78
|
-
for (const
|
|
78
|
+
for (const childNodeDescription of this.description.timeline) {
|
|
79
|
+
const childNode = this.instantiateChildNode(childNodeDescription);
|
|
80
|
+
|
|
79
81
|
const previousChild = this.currentChild;
|
|
80
82
|
this.currentChild = childNode;
|
|
81
83
|
childNode.index = previousChild
|
|
@@ -151,14 +153,15 @@ export class Timeline extends TimelineNode {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
private instantiateChildNode(
|
|
157
|
+
childDescription: TimelineDescription | TimelineArray | TrialDescription
|
|
158
|
+
) {
|
|
159
|
+
const newChildNode = isTimelineDescription(childDescription)
|
|
160
|
+
? new Timeline(this.dependencies, childDescription, this)
|
|
161
|
+
: new Trial(this.dependencies, childDescription, this);
|
|
162
|
+
|
|
163
|
+
this.children.push(newChildNode);
|
|
164
|
+
return newChildNode;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
private currentTimelineVariables: Record<string, any>;
|
|
@@ -36,6 +36,16 @@ describe("Trial", () => {
|
|
|
36
36
|
return trial;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
it("throws an error upon construction when the `type` parameter or plugin info object is undefined", () => {
|
|
40
|
+
for (const description of [{}, { type: {} }] as TrialDescription[]) {
|
|
41
|
+
expect(
|
|
42
|
+
() => new Trial(dependencies, description, timeline)
|
|
43
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
44
|
+
"\"Plugin not recognized. Please provide a valid plugin using the 'type' parameter.\""
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
39
49
|
describe("run()", () => {
|
|
40
50
|
it("instantiates the corresponding plugin", async () => {
|
|
41
51
|
const trial = createTrial({ type: TestPlugin });
|
|
@@ -436,6 +446,8 @@ describe("Trial", () => {
|
|
|
436
446
|
return TestPlugin;
|
|
437
447
|
case "x":
|
|
438
448
|
return "foo";
|
|
449
|
+
case "y":
|
|
450
|
+
return ["foo", "bar"];
|
|
439
451
|
default:
|
|
440
452
|
return undefined;
|
|
441
453
|
}
|
|
@@ -444,17 +456,19 @@ describe("Trial", () => {
|
|
|
444
456
|
const trial = createTrial({
|
|
445
457
|
type: new TimelineVariable("t"),
|
|
446
458
|
requiredString: new TimelineVariable("x"),
|
|
459
|
+
stringArray: new TimelineVariable("y"),
|
|
447
460
|
requiredComplexNested: { requiredChild: () => new TimelineVariable("x") },
|
|
448
461
|
requiredComplexNestedArray: [{ requiredChild: () => new TimelineVariable("x") }],
|
|
449
462
|
});
|
|
450
463
|
|
|
451
464
|
await trial.run();
|
|
452
465
|
|
|
453
|
-
// The `x` timeline variables should have been
|
|
466
|
+
// The `x` and `y` timeline variables should have been evaluated
|
|
454
467
|
expect(trial.pluginInstance.trial).toHaveBeenCalledWith(
|
|
455
468
|
expect.anything(),
|
|
456
469
|
expect.objectContaining({
|
|
457
470
|
requiredString: "foo",
|
|
471
|
+
stringArray: ["foo", "bar"],
|
|
458
472
|
requiredComplexNested: expect.objectContaining({ requiredChild: "foo" }),
|
|
459
473
|
requiredComplexNestedArray: [expect.objectContaining({ requiredChild: "foo" })],
|
|
460
474
|
}),
|
package/src/timeline/Trial.ts
CHANGED
|
@@ -35,7 +35,12 @@ export class Trial extends TimelineNode {
|
|
|
35
35
|
|
|
36
36
|
this.trialObject = deepCopy(description);
|
|
37
37
|
this.pluginClass = this.getParameterValue("type", { evaluateFunctions: false });
|
|
38
|
-
this.pluginInfo = this.pluginClass["info"];
|
|
38
|
+
this.pluginInfo = this.pluginClass?.["info"];
|
|
39
|
+
if (!this.pluginInfo) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Plugin not recognized. Please provide a valid plugin using the 'type' parameter."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
39
44
|
|
|
40
45
|
if (!("version" in this.pluginInfo) && !("data" in this.pluginInfo)) {
|
|
41
46
|
console.warn(
|