jspsych 8.0.2 → 8.1.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/dist/index.browser.js +9 -10
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +3 -3
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +8 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +20 -7
- package/dist/index.js +8 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/randomization.ts +29 -10
- package/src/timeline/Timeline.spec.ts +105 -5
- package/src/timeline/Timeline.ts +12 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jspsych",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.1.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.0.1",
|
|
52
52
|
"@types/dom-mediacapture-record": "^1.0.11",
|
|
53
53
|
"base64-inline-loader": "^2.0.1",
|
|
54
54
|
"css-loader": "^6.6.0",
|
|
@@ -12,7 +12,7 @@ export function setSeed(seed: string = Math.random().toString()) {
|
|
|
12
12
|
return seed;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function repeat(array, repetitions, unpack = false) {
|
|
15
|
+
export function repeat(array: any, repetitions: any, unpack = false) {
|
|
16
16
|
const arr_isArray = Array.isArray(array);
|
|
17
17
|
const rep_isArray = Array.isArray(repetitions);
|
|
18
18
|
|
|
@@ -77,7 +77,7 @@ export function repeat(array, repetitions, unpack = false) {
|
|
|
77
77
|
return out;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export function shuffle(array: Array<
|
|
80
|
+
export function shuffle<T>(array: Array<T>) {
|
|
81
81
|
if (!Array.isArray(array)) {
|
|
82
82
|
console.error("Argument to shuffle() must be an array.");
|
|
83
83
|
}
|
|
@@ -101,7 +101,7 @@ export function shuffle(array: Array<any>) {
|
|
|
101
101
|
return copy_array;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export function shuffleNoRepeats(arr: Array<
|
|
104
|
+
export function shuffleNoRepeats<T>(arr: Array<T>, equalityTest: (a: T, b: T) => boolean) {
|
|
105
105
|
if (!Array.isArray(arr)) {
|
|
106
106
|
console.error("First argument to shuffleNoRepeats() must be an array.");
|
|
107
107
|
}
|
|
@@ -143,7 +143,10 @@ export function shuffleNoRepeats(arr: Array<any>, equalityTest: (a: any, b: any)
|
|
|
143
143
|
return random_shuffle;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
export function shuffleAlternateGroups
|
|
146
|
+
export function shuffleAlternateGroups<T extends any[]>(
|
|
147
|
+
arr_groups: Array<T>,
|
|
148
|
+
random_group_order = false
|
|
149
|
+
) {
|
|
147
150
|
const n_groups = arr_groups.length;
|
|
148
151
|
if (n_groups == 1) {
|
|
149
152
|
console.warn(
|
|
@@ -178,7 +181,7 @@ export function shuffleAlternateGroups(arr_groups, random_group_order = false) {
|
|
|
178
181
|
return out;
|
|
179
182
|
}
|
|
180
183
|
|
|
181
|
-
export function sampleWithoutReplacement(arr
|
|
184
|
+
export function sampleWithoutReplacement<T>(arr: Array<T>, size: number) {
|
|
182
185
|
if (!Array.isArray(arr)) {
|
|
183
186
|
console.error("First argument to sampleWithoutReplacement() must be an array");
|
|
184
187
|
}
|
|
@@ -189,7 +192,7 @@ export function sampleWithoutReplacement(arr, size) {
|
|
|
189
192
|
return shuffle(arr).slice(0, size);
|
|
190
193
|
}
|
|
191
194
|
|
|
192
|
-
export function sampleWithReplacement(arr
|
|
195
|
+
export function sampleWithReplacement<T>(arr: Array<T>, size: number, weights?: number[]) {
|
|
193
196
|
if (!Array.isArray(arr)) {
|
|
194
197
|
console.error("First argument to sampleWithReplacement() must be an array");
|
|
195
198
|
}
|
|
@@ -301,6 +304,21 @@ export function sampleExGaussian(
|
|
|
301
304
|
return s;
|
|
302
305
|
}
|
|
303
306
|
|
|
307
|
+
type RandomWordsOptions = {
|
|
308
|
+
min?: number;
|
|
309
|
+
max?: number;
|
|
310
|
+
exactly?: number;
|
|
311
|
+
maxLength?: number;
|
|
312
|
+
wordsPerString?: number;
|
|
313
|
+
seperator?: string;
|
|
314
|
+
formatter?: (word: string, index: number) => string;
|
|
315
|
+
join?: string;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
type RandomWordsResult<T extends RandomWordsOptions> = T extends { join: string }
|
|
319
|
+
? string
|
|
320
|
+
: string[];
|
|
321
|
+
|
|
304
322
|
/**
|
|
305
323
|
* Generate one or more random words.
|
|
306
324
|
*
|
|
@@ -311,8 +329,9 @@ export function sampleExGaussian(
|
|
|
311
329
|
*
|
|
312
330
|
* @returns An array of words or a single string, depending on parameter choices.
|
|
313
331
|
*/
|
|
314
|
-
export function randomWords(opts) {
|
|
315
|
-
|
|
332
|
+
export function randomWords<T extends RandomWordsOptions>(opts: T) {
|
|
333
|
+
// there is a type incompatibility here because `random-words` uses overloads rather than generics
|
|
334
|
+
return rw(opts) as RandomWordsResult<T>;
|
|
316
335
|
}
|
|
317
336
|
|
|
318
337
|
// Box-Muller transformation for a random sample from normal distribution with mean = 0, std = 1
|
|
@@ -325,8 +344,8 @@ function randn_bm() {
|
|
|
325
344
|
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
326
345
|
}
|
|
327
346
|
|
|
328
|
-
function unpackArray(array) {
|
|
329
|
-
const out = {};
|
|
347
|
+
function unpackArray(array: object[]) {
|
|
348
|
+
const out: Record<string, any> = {};
|
|
330
349
|
|
|
331
350
|
for (const x of array) {
|
|
332
351
|
for (const key of Object.keys(x)) {
|
|
@@ -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>;
|