jspsych 7.3.4 → 8.0.1
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/css/jspsych.css +18 -8
- package/dist/index.browser.js +3097 -4286
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +6 -2
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +2331 -4066
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +984 -6
- package/dist/index.js +2330 -4066
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/ExtensionManager.spec.ts +121 -0
- package/src/ExtensionManager.ts +100 -0
- package/src/JsPsych.ts +195 -690
- package/src/ProgressBar.spec.ts +60 -0
- package/src/ProgressBar.ts +60 -0
- package/src/index.scss +29 -8
- package/src/index.ts +4 -9
- package/src/modules/data/DataCollection.ts +1 -1
- package/src/modules/data/DataColumn.ts +12 -1
- package/src/modules/data/index.ts +92 -103
- package/src/modules/extensions.ts +4 -0
- package/src/modules/plugin-api/AudioPlayer.ts +101 -0
- package/src/modules/plugin-api/KeyboardListenerAPI.ts +1 -1
- package/src/modules/plugin-api/MediaAPI.ts +48 -106
- package/src/modules/plugin-api/__mocks__/AudioPlayer.ts +38 -0
- package/src/modules/plugin-api/index.ts +11 -14
- package/src/modules/plugins.ts +26 -27
- package/src/modules/randomization.ts +1 -1
- package/src/timeline/Timeline.spec.ts +921 -0
- package/src/timeline/Timeline.ts +342 -0
- package/src/timeline/TimelineNode.ts +174 -0
- package/src/timeline/Trial.spec.ts +897 -0
- package/src/timeline/Trial.ts +419 -0
- package/src/timeline/index.ts +232 -0
- package/src/timeline/util.spec.ts +124 -0
- package/src/timeline/util.ts +146 -0
- package/dist/JsPsych.d.ts +0 -112
- package/dist/TimelineNode.d.ts +0 -34
- package/dist/migration.d.ts +0 -3
- package/dist/modules/data/DataCollection.d.ts +0 -46
- package/dist/modules/data/DataColumn.d.ts +0 -15
- package/dist/modules/data/index.d.ts +0 -25
- package/dist/modules/data/utils.d.ts +0 -3
- package/dist/modules/extensions.d.ts +0 -22
- package/dist/modules/plugin-api/HardwareAPI.d.ts +0 -15
- package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +0 -34
- package/dist/modules/plugin-api/MediaAPI.d.ts +0 -32
- package/dist/modules/plugin-api/SimulationAPI.d.ts +0 -44
- package/dist/modules/plugin-api/TimeoutAPI.d.ts +0 -17
- package/dist/modules/plugin-api/index.d.ts +0 -8
- package/dist/modules/plugins.d.ts +0 -136
- package/dist/modules/randomization.d.ts +0 -42
- package/dist/modules/turk.d.ts +0 -40
- package/dist/modules/utils.d.ts +0 -13
- package/src/TimelineNode.ts +0 -544
- package/src/modules/plugin-api/HardwareAPI.ts +0 -32
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import { flushPromises } from "@jspsych/test-utils";
|
|
2
|
+
|
|
3
|
+
import { TimelineNodeDependenciesMock, createSnapshotUtils } from "../../tests/test-utils";
|
|
4
|
+
import TestPlugin from "../../tests/TestPlugin";
|
|
5
|
+
import { DataCollection } from "../modules/data/DataCollection";
|
|
6
|
+
import {
|
|
7
|
+
repeat,
|
|
8
|
+
sampleWithReplacement,
|
|
9
|
+
sampleWithoutReplacement,
|
|
10
|
+
shuffle,
|
|
11
|
+
shuffleAlternateGroups,
|
|
12
|
+
} from "../modules/randomization";
|
|
13
|
+
import { Timeline } from "./Timeline";
|
|
14
|
+
import { Trial } from "./Trial";
|
|
15
|
+
import {
|
|
16
|
+
SampleOptions,
|
|
17
|
+
TimelineArray,
|
|
18
|
+
TimelineDescription,
|
|
19
|
+
TimelineNodeStatus,
|
|
20
|
+
TimelineVariable,
|
|
21
|
+
} from ".";
|
|
22
|
+
|
|
23
|
+
jest.useFakeTimers();
|
|
24
|
+
|
|
25
|
+
jest.mock("../modules/randomization");
|
|
26
|
+
|
|
27
|
+
const exampleTimeline: TimelineDescription = {
|
|
28
|
+
timeline: [{ type: TestPlugin }, { type: TestPlugin }, { timeline: [{ type: TestPlugin }] }],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("Timeline", () => {
|
|
32
|
+
let dependencies: TimelineNodeDependenciesMock;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
dependencies = new TimelineNodeDependenciesMock();
|
|
36
|
+
TestPlugin.reset();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const createTimeline = (description: TimelineDescription | TimelineArray, parent?: Timeline) =>
|
|
40
|
+
new Timeline(dependencies, description, parent);
|
|
41
|
+
|
|
42
|
+
describe("run()", () => {
|
|
43
|
+
it("instantiates proper child nodes", async () => {
|
|
44
|
+
const timeline = createTimeline([
|
|
45
|
+
{ type: TestPlugin },
|
|
46
|
+
{ timeline: [{ type: TestPlugin }, { type: TestPlugin }] },
|
|
47
|
+
{ timeline: [{ type: TestPlugin }] },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
await timeline.run();
|
|
51
|
+
|
|
52
|
+
const children = timeline.children;
|
|
53
|
+
expect(children).toEqual([expect.any(Trial), expect.any(Timeline), expect.any(Timeline)]);
|
|
54
|
+
expect((children[1] as Timeline).children).toEqual([expect.any(Trial), expect.any(Trial)]);
|
|
55
|
+
expect((children[2] as Timeline).children).toEqual([expect.any(Trial)]);
|
|
56
|
+
|
|
57
|
+
expect(children.map((child) => child.index)).toEqual([0, 1, 3]);
|
|
58
|
+
expect((children[1] as Timeline).children.map((child) => child.index)).toEqual([1, 2]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("with `pause()` and `resume()` calls`", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
TestPlugin.setManualFinishTrialMode();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("pauses, resumes, and updates the results of getStatus()", async () => {
|
|
67
|
+
const timeline = createTimeline({
|
|
68
|
+
timeline: [
|
|
69
|
+
{ type: TestPlugin },
|
|
70
|
+
{ type: TestPlugin },
|
|
71
|
+
{ timeline: [{ type: TestPlugin }, { type: TestPlugin }] },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
const runPromise = timeline.run();
|
|
75
|
+
|
|
76
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
77
|
+
expect(timeline.children[0].getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
78
|
+
await TestPlugin.finishTrial();
|
|
79
|
+
|
|
80
|
+
expect(timeline.children[0].getStatus()).toBe(TimelineNodeStatus.COMPLETED);
|
|
81
|
+
expect(timeline.children[1].getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
82
|
+
timeline.pause();
|
|
83
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.PAUSED);
|
|
84
|
+
|
|
85
|
+
await TestPlugin.finishTrial();
|
|
86
|
+
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
|
+
|
|
92
|
+
expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.PENDING);
|
|
93
|
+
|
|
94
|
+
timeline.resume();
|
|
95
|
+
await flushPromises();
|
|
96
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
97
|
+
expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
98
|
+
|
|
99
|
+
// The child timeline is running. Let's pause the parent timeline to check whether the child
|
|
100
|
+
// gets paused too
|
|
101
|
+
timeline.pause();
|
|
102
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.PAUSED);
|
|
103
|
+
expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.PAUSED);
|
|
104
|
+
|
|
105
|
+
await TestPlugin.finishTrial();
|
|
106
|
+
timeline.resume();
|
|
107
|
+
await flushPromises();
|
|
108
|
+
expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
109
|
+
|
|
110
|
+
await TestPlugin.finishTrial();
|
|
111
|
+
|
|
112
|
+
expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.COMPLETED);
|
|
113
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.COMPLETED);
|
|
114
|
+
|
|
115
|
+
await runPromise;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// https://www.jspsych.org/7.1/reference/jspsych/#jspsychresumeexperiment
|
|
119
|
+
it("doesn't affect `post_trial_gap`", async () => {
|
|
120
|
+
const timeline = createTimeline([{ type: TestPlugin, post_trial_gap: 200 }]);
|
|
121
|
+
const runPromise = timeline.run();
|
|
122
|
+
let hasTimelineCompleted = false;
|
|
123
|
+
runPromise.then(() => {
|
|
124
|
+
hasTimelineCompleted = true;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(hasTimelineCompleted).toBe(false);
|
|
128
|
+
await TestPlugin.finishTrial();
|
|
129
|
+
expect(hasTimelineCompleted).toBe(false);
|
|
130
|
+
|
|
131
|
+
timeline.pause();
|
|
132
|
+
jest.advanceTimersByTime(100);
|
|
133
|
+
timeline.resume();
|
|
134
|
+
await flushPromises();
|
|
135
|
+
expect(hasTimelineCompleted).toBe(false);
|
|
136
|
+
|
|
137
|
+
jest.advanceTimersByTime(100);
|
|
138
|
+
await flushPromises();
|
|
139
|
+
expect(hasTimelineCompleted).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("abort()", () => {
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
TestPlugin.setManualFinishTrialMode();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("aborts the timeline after the current trial ends, updating the result of getStatus()", () => {
|
|
149
|
+
test("when the timeline is running", async () => {
|
|
150
|
+
const timeline = createTimeline(exampleTimeline);
|
|
151
|
+
const runPromise = timeline.run();
|
|
152
|
+
|
|
153
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
154
|
+
timeline.abort();
|
|
155
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
156
|
+
await TestPlugin.finishTrial();
|
|
157
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.ABORTED);
|
|
158
|
+
await runPromise;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("when the timeline is paused", async () => {
|
|
162
|
+
const timeline = createTimeline(exampleTimeline);
|
|
163
|
+
timeline.run();
|
|
164
|
+
|
|
165
|
+
timeline.pause();
|
|
166
|
+
await TestPlugin.finishTrial();
|
|
167
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.PAUSED);
|
|
168
|
+
timeline.abort();
|
|
169
|
+
await flushPromises();
|
|
170
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.ABORTED);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("aborts child timelines too", async () => {
|
|
175
|
+
const timeline = createTimeline({
|
|
176
|
+
timeline: [{ timeline: [{ type: TestPlugin }, { type: TestPlugin }] }],
|
|
177
|
+
});
|
|
178
|
+
const runPromise = timeline.run();
|
|
179
|
+
|
|
180
|
+
expect(timeline.children[0].getStatus()).toBe(TimelineNodeStatus.RUNNING);
|
|
181
|
+
timeline.abort();
|
|
182
|
+
await TestPlugin.finishTrial();
|
|
183
|
+
expect(timeline.children[0].getStatus()).toBe(TimelineNodeStatus.ABORTED);
|
|
184
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.ABORTED);
|
|
185
|
+
await runPromise;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("doesn't affect the timeline when it is neither running nor paused", async () => {
|
|
189
|
+
const timeline = createTimeline([{ type: TestPlugin }]);
|
|
190
|
+
|
|
191
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.PENDING);
|
|
192
|
+
timeline.abort();
|
|
193
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.PENDING);
|
|
194
|
+
|
|
195
|
+
// Complete the timeline
|
|
196
|
+
const runPromise = timeline.run();
|
|
197
|
+
await TestPlugin.finishTrial();
|
|
198
|
+
await runPromise;
|
|
199
|
+
|
|
200
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.COMPLETED);
|
|
201
|
+
timeline.abort();
|
|
202
|
+
expect(timeline.getStatus()).toBe(TimelineNodeStatus.COMPLETED);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("repeats a timeline according to `repetitions`", async () => {
|
|
207
|
+
const timeline = createTimeline({ ...exampleTimeline, repetitions: 2 });
|
|
208
|
+
|
|
209
|
+
await timeline.run();
|
|
210
|
+
|
|
211
|
+
expect(timeline.children.length).toEqual(6);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("repeats a timeline according to `loop_function`", async () => {
|
|
215
|
+
const loopFunction = jest.fn();
|
|
216
|
+
loopFunction.mockReturnValue(false);
|
|
217
|
+
loopFunction.mockReturnValueOnce(true);
|
|
218
|
+
|
|
219
|
+
const timeline = createTimeline({ ...exampleTimeline, loop_function: loopFunction });
|
|
220
|
+
|
|
221
|
+
await timeline.run();
|
|
222
|
+
expect(loopFunction).toHaveBeenCalledTimes(2);
|
|
223
|
+
|
|
224
|
+
for (const call of loopFunction.mock.calls) {
|
|
225
|
+
expect(call[0]).toBeInstanceOf(DataCollection);
|
|
226
|
+
expect((call[0] as DataCollection).values()).toEqual(
|
|
227
|
+
Array(3).fill(expect.objectContaining({ my: "result" }))
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(timeline.children.length).toEqual(6);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("repeats a timeline according to `repetitions` and `loop_function`", async () => {
|
|
235
|
+
const loopFunction = jest.fn();
|
|
236
|
+
loopFunction.mockReturnValue(false);
|
|
237
|
+
loopFunction.mockReturnValueOnce(true);
|
|
238
|
+
loopFunction.mockReturnValueOnce(false);
|
|
239
|
+
loopFunction.mockReturnValueOnce(true);
|
|
240
|
+
|
|
241
|
+
const timeline = createTimeline({
|
|
242
|
+
...exampleTimeline,
|
|
243
|
+
repetitions: 2,
|
|
244
|
+
loop_function: loopFunction,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await timeline.run();
|
|
248
|
+
expect(loopFunction).toHaveBeenCalledTimes(4);
|
|
249
|
+
expect(timeline.children.length).toEqual(12);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("skips execution if `conditional_function` returns `false`", async () => {
|
|
253
|
+
const timeline = createTimeline({
|
|
254
|
+
...exampleTimeline,
|
|
255
|
+
conditional_function: jest.fn(() => false),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await timeline.run();
|
|
259
|
+
expect(timeline.children.length).toEqual(0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("executes regularly if `conditional_function` returns `true`", async () => {
|
|
263
|
+
const timeline = createTimeline({
|
|
264
|
+
...exampleTimeline,
|
|
265
|
+
conditional_function: jest.fn(() => true),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await timeline.run();
|
|
269
|
+
expect(timeline.children.length).toEqual(3);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("invokes `on_timeline_start` and `on_timeline_finished` callbacks at the beginning and at the end of the timeline, respectively", async () => {
|
|
273
|
+
TestPlugin.setManualFinishTrialMode();
|
|
274
|
+
|
|
275
|
+
const onTimelineStart = jest.fn();
|
|
276
|
+
const onTimelineFinish = jest.fn();
|
|
277
|
+
|
|
278
|
+
const timeline = createTimeline({
|
|
279
|
+
timeline: [{ type: TestPlugin }],
|
|
280
|
+
on_timeline_start: onTimelineStart,
|
|
281
|
+
on_timeline_finish: onTimelineFinish,
|
|
282
|
+
repetitions: 2,
|
|
283
|
+
});
|
|
284
|
+
timeline.run();
|
|
285
|
+
expect(onTimelineStart).toHaveBeenCalledTimes(1);
|
|
286
|
+
expect(onTimelineFinish).toHaveBeenCalledTimes(0);
|
|
287
|
+
|
|
288
|
+
await TestPlugin.finishTrial();
|
|
289
|
+
await TestPlugin.finishTrial();
|
|
290
|
+
|
|
291
|
+
expect(onTimelineStart).toHaveBeenCalledTimes(1);
|
|
292
|
+
expect(onTimelineFinish).toHaveBeenCalledTimes(1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("loop function ignores data from trials where `record_data` is false", async () => {
|
|
296
|
+
const loopFunction = jest.fn();
|
|
297
|
+
loopFunction.mockReturnValue(false);
|
|
298
|
+
|
|
299
|
+
const timeline = createTimeline({
|
|
300
|
+
timeline: [{ type: TestPlugin, record_data: false }, { type: TestPlugin }],
|
|
301
|
+
loop_function: loopFunction,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await timeline.run();
|
|
305
|
+
expect((loopFunction.mock.calls[0][0] as DataCollection).count()).toBe(1);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("with timeline variables", () => {
|
|
309
|
+
it("repeats all trials for each set of variables", async () => {
|
|
310
|
+
const xValues = [];
|
|
311
|
+
TestPlugin.trial = async () => {
|
|
312
|
+
xValues.push(timeline.evaluateTimelineVariable(new TimelineVariable("x")));
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const timeline = createTimeline({
|
|
316
|
+
timeline: [{ type: TestPlugin }],
|
|
317
|
+
timeline_variables: [{ x: 0 }, { x: 1 }, { x: 2 }, { x: 3 }],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await timeline.run();
|
|
321
|
+
expect(timeline.children.length).toEqual(4);
|
|
322
|
+
expect(xValues).toEqual([0, 1, 2, 3]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("respects the `randomize_order` and `sample` options", async () => {
|
|
326
|
+
let xValues: number[];
|
|
327
|
+
|
|
328
|
+
const createSampleTimeline = (sample: SampleOptions, randomize_order?: boolean) => {
|
|
329
|
+
xValues = [];
|
|
330
|
+
const timeline = createTimeline({
|
|
331
|
+
timeline: [{ type: TestPlugin }],
|
|
332
|
+
timeline_variables: [{ x: 0 }, { x: 1 }],
|
|
333
|
+
sample,
|
|
334
|
+
randomize_order,
|
|
335
|
+
});
|
|
336
|
+
TestPlugin.trial = async () => {
|
|
337
|
+
xValues.push(timeline.evaluateTimelineVariable(new TimelineVariable("x")));
|
|
338
|
+
};
|
|
339
|
+
return timeline;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// `randomize_order`
|
|
343
|
+
jest.mocked(shuffle).mockReturnValue([1, 0]);
|
|
344
|
+
await createSampleTimeline(undefined, true).run();
|
|
345
|
+
expect(shuffle).toHaveBeenCalledWith([0, 1]);
|
|
346
|
+
expect(xValues).toEqual([1, 0]);
|
|
347
|
+
|
|
348
|
+
// with-replacement
|
|
349
|
+
jest.mocked(sampleWithReplacement).mockReturnValue([0, 0]);
|
|
350
|
+
await createSampleTimeline({ type: "with-replacement", size: 2, weights: [1, 1] }).run();
|
|
351
|
+
expect(sampleWithReplacement).toHaveBeenCalledWith([0, 1], 2, [1, 1]);
|
|
352
|
+
expect(xValues).toEqual([0, 0]);
|
|
353
|
+
|
|
354
|
+
// without-replacement
|
|
355
|
+
jest.mocked(sampleWithoutReplacement).mockReturnValue([1, 0]);
|
|
356
|
+
await createSampleTimeline({ type: "without-replacement", size: 2 }).run();
|
|
357
|
+
expect(sampleWithoutReplacement).toHaveBeenCalledWith([0, 1], 2);
|
|
358
|
+
expect(xValues).toEqual([1, 0]);
|
|
359
|
+
|
|
360
|
+
// fixed-repetitions
|
|
361
|
+
jest.mocked(repeat).mockReturnValue([0, 0, 1, 1]);
|
|
362
|
+
await createSampleTimeline({ type: "fixed-repetitions", size: 2 }).run();
|
|
363
|
+
expect(repeat).toHaveBeenCalledWith([0, 1], 2);
|
|
364
|
+
expect(xValues).toEqual([0, 0, 1, 1]);
|
|
365
|
+
|
|
366
|
+
// alternate-groups
|
|
367
|
+
jest.mocked(shuffleAlternateGroups).mockReturnValue([1, 0]);
|
|
368
|
+
await createSampleTimeline({
|
|
369
|
+
type: "alternate-groups",
|
|
370
|
+
groups: [[0], [1]],
|
|
371
|
+
randomize_group_order: true,
|
|
372
|
+
}).run();
|
|
373
|
+
expect(shuffleAlternateGroups).toHaveBeenCalledWith([[0], [1]], true);
|
|
374
|
+
expect(xValues).toEqual([1, 0]);
|
|
375
|
+
|
|
376
|
+
// custom function
|
|
377
|
+
const sampleFunction = jest.fn(() => [0]);
|
|
378
|
+
await createSampleTimeline({ type: "custom", fn: sampleFunction }).run();
|
|
379
|
+
expect(sampleFunction).toHaveBeenCalledTimes(1);
|
|
380
|
+
expect(xValues).toEqual([0]);
|
|
381
|
+
|
|
382
|
+
await expect(
|
|
383
|
+
// @ts-expect-error non-existing type
|
|
384
|
+
createSampleTimeline({ type: "invalid" }).run()
|
|
385
|
+
).rejects.toThrow('Invalid type "invalid" in timeline sample parameters.');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("samples on each loop iteration (be it via `repetitions` or `loop_function`)", async () => {
|
|
389
|
+
const sampleFunction = jest.fn(() => [0]);
|
|
390
|
+
|
|
391
|
+
await createTimeline({
|
|
392
|
+
timeline: [{ type: TestPlugin }],
|
|
393
|
+
timeline_variables: [{ x: 0 }],
|
|
394
|
+
sample: { type: "custom", fn: sampleFunction },
|
|
395
|
+
repetitions: 2,
|
|
396
|
+
loop_function: jest.fn().mockReturnValue(false).mockReturnValueOnce(true),
|
|
397
|
+
}).run();
|
|
398
|
+
|
|
399
|
+
// 2 repetitions + 1 loop in the first repitition = 3 sample function calls
|
|
400
|
+
expect(sampleFunction).toHaveBeenCalledTimes(3);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("makes variables available to callbacks", async () => {
|
|
404
|
+
const variableResults: Record<string, any> = {};
|
|
405
|
+
const makeCallback = (resultName: string, callbackReturnValue?: any) => () => {
|
|
406
|
+
variableResults[resultName] = timeline.evaluateTimelineVariable(
|
|
407
|
+
new TimelineVariable("x")
|
|
408
|
+
);
|
|
409
|
+
return callbackReturnValue;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const timeline = createTimeline({
|
|
413
|
+
timeline: [{ type: TestPlugin }],
|
|
414
|
+
timeline_variables: [{ x: 0 }],
|
|
415
|
+
on_timeline_start: jest.fn().mockImplementation(makeCallback("on_timeline_start")),
|
|
416
|
+
on_timeline_finish: jest.fn().mockImplementation(makeCallback("on_timeline_finish")),
|
|
417
|
+
conditional_function: jest
|
|
418
|
+
.fn()
|
|
419
|
+
.mockImplementation(makeCallback("conditional_function", true)),
|
|
420
|
+
loop_function: jest.fn().mockImplementation(makeCallback("loop_function", false)),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await timeline.run();
|
|
424
|
+
expect(variableResults).toEqual({
|
|
425
|
+
on_timeline_start: 0,
|
|
426
|
+
on_timeline_finish: 0,
|
|
427
|
+
conditional_function: 0,
|
|
428
|
+
loop_function: 0,
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe("getAllTimelineVariables()", () => {
|
|
435
|
+
it("returns the current values of all timeline variables, including those from parent timelines", async () => {
|
|
436
|
+
const timeline = createTimeline({
|
|
437
|
+
timeline: [{ timeline: [{ type: TestPlugin }], timeline_variables: [{ y: 1, z: 1 }] }],
|
|
438
|
+
timeline_variables: [{ x: 0, y: 0 }],
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await timeline.run();
|
|
442
|
+
|
|
443
|
+
expect(timeline.getAllTimelineVariables()).toEqual({ x: 0, y: 0 });
|
|
444
|
+
expect((timeline.children[0] as Timeline).getAllTimelineVariables()).toEqual({
|
|
445
|
+
x: 0,
|
|
446
|
+
y: 1,
|
|
447
|
+
z: 1,
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe("evaluateTimelineVariable()", () => {
|
|
453
|
+
describe("if a local timeline variable exists", () => {
|
|
454
|
+
it("returns the local timeline variable", async () => {
|
|
455
|
+
const timeline = createTimeline({
|
|
456
|
+
timeline: [{ type: TestPlugin }],
|
|
457
|
+
timeline_variables: [{ x: 0 }],
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
await timeline.run();
|
|
461
|
+
expect(timeline.evaluateTimelineVariable(new TimelineVariable("x"))).toEqual(0);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe("if a timeline variable is not defined locally", () => {
|
|
466
|
+
it("falls back to parent timeline variables", async () => {
|
|
467
|
+
const timeline = createTimeline({
|
|
468
|
+
timeline: [{ timeline: [{ type: TestPlugin }], timeline_variables: [{ x: undefined }] }],
|
|
469
|
+
timeline_variables: [{ x: 0, y: 0 }],
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await timeline.run();
|
|
473
|
+
expect(timeline.evaluateTimelineVariable(new TimelineVariable("x"))).toEqual(0);
|
|
474
|
+
expect(timeline.evaluateTimelineVariable(new TimelineVariable("y"))).toEqual(0);
|
|
475
|
+
|
|
476
|
+
const childTimeline = timeline.children[0] as Timeline;
|
|
477
|
+
expect(childTimeline.evaluateTimelineVariable(new TimelineVariable("x"))).toBeUndefined();
|
|
478
|
+
expect(childTimeline.evaluateTimelineVariable(new TimelineVariable("y"))).toEqual(0);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("throws an exception if there are no parents or none of them has a value for the variable", async () => {
|
|
482
|
+
const timeline = createTimeline({
|
|
483
|
+
timeline: [{ timeline: [{ type: TestPlugin }] }],
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const variable = new TimelineVariable("x");
|
|
487
|
+
|
|
488
|
+
await timeline.run();
|
|
489
|
+
expect(() => timeline.evaluateTimelineVariable(variable)).toThrowError("");
|
|
490
|
+
expect(() =>
|
|
491
|
+
(timeline.children[0] as Timeline).evaluateTimelineVariable(variable)
|
|
492
|
+
).toThrowError("");
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
describe("getParameterValue()", () => {
|
|
498
|
+
// Note: This includes test cases for the implementation provided by `BaseTimelineNode`.
|
|
499
|
+
|
|
500
|
+
it("returns the local parameter value, if it exists", async () => {
|
|
501
|
+
const timeline = createTimeline({ timeline: [], my_parameter: "test" });
|
|
502
|
+
|
|
503
|
+
expect(timeline.getParameterValue("my_parameter")).toEqual("test");
|
|
504
|
+
expect(timeline.getParameterValue("other_parameter")).toBeUndefined();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("falls back to parent parameter values if `recursive` is not `false`", async () => {
|
|
508
|
+
const parentTimeline = createTimeline({
|
|
509
|
+
timeline: [],
|
|
510
|
+
first_parameter: "test",
|
|
511
|
+
second_parameter: "test",
|
|
512
|
+
});
|
|
513
|
+
const childTimeline = createTimeline(
|
|
514
|
+
{ timeline: [], first_parameter: undefined },
|
|
515
|
+
parentTimeline
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
expect(childTimeline.getParameterValue("second_parameter", { cacheResult: false })).toEqual(
|
|
519
|
+
"test"
|
|
520
|
+
);
|
|
521
|
+
expect(
|
|
522
|
+
childTimeline.getParameterValue("second_parameter", { recursive: false })
|
|
523
|
+
).toBeUndefined();
|
|
524
|
+
|
|
525
|
+
expect(childTimeline.getParameterValue("first_parameter")).toBeUndefined();
|
|
526
|
+
expect(childTimeline.getParameterValue("other_parameter")).toBeUndefined();
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("evaluates timeline variables", async () => {
|
|
530
|
+
const timeline = createTimeline({
|
|
531
|
+
timeline: [{ timeline: [], child_parameter: new TimelineVariable("x") }],
|
|
532
|
+
timeline_variables: [{ x: 0 }],
|
|
533
|
+
parent_parameter: new TimelineVariable("x"),
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
await timeline.run();
|
|
537
|
+
|
|
538
|
+
expect(timeline.children[0].getParameterValue("child_parameter")).toEqual(0);
|
|
539
|
+
expect(timeline.children[0].getParameterValue("parent_parameter")).toEqual(0);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("evaluates functions unless `evaluateFunctions` is set to `false`", async () => {
|
|
543
|
+
const timeline = createTimeline({
|
|
544
|
+
timeline: [],
|
|
545
|
+
function_parameter: jest.fn(() => "result"),
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
expect(timeline.getParameterValue("function_parameter", { cacheResult: false })).toEqual(
|
|
549
|
+
"result"
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
expect(
|
|
553
|
+
timeline.getParameterValue("function_parameter", {
|
|
554
|
+
evaluateFunctions: true,
|
|
555
|
+
cacheResult: false,
|
|
556
|
+
})
|
|
557
|
+
).toEqual("result");
|
|
558
|
+
|
|
559
|
+
expect(
|
|
560
|
+
typeof timeline.getParameterValue("function_parameter", { evaluateFunctions: false })
|
|
561
|
+
).toEqual("function");
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("considers nested properties if `parameterName` is an array", async () => {
|
|
565
|
+
const timeline = createTimeline({
|
|
566
|
+
timeline: [],
|
|
567
|
+
object: {
|
|
568
|
+
childString: "foo",
|
|
569
|
+
childObject: {
|
|
570
|
+
childString: "bar",
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
expect(timeline.getParameterValue(["object", "childString"])).toEqual("foo");
|
|
576
|
+
expect(timeline.getParameterValue(["object", "childObject"])).toEqual({ childString: "bar" });
|
|
577
|
+
expect(timeline.getParameterValue(["object", "childObject", "childString"])).toEqual("bar");
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("respects the `replaceResult` function", () => {
|
|
581
|
+
const timeline = createTimeline({ timeline: [] });
|
|
582
|
+
|
|
583
|
+
expect(timeline.getParameterValue("key", { replaceResult: () => "value" })).toBe("value");
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("caches results and uses them for nested lookups", async () => {
|
|
587
|
+
const timeline = createTimeline({ timeline: [], object: () => ({ child: "foo" }) });
|
|
588
|
+
|
|
589
|
+
expect(
|
|
590
|
+
timeline.getParameterValue("object", {
|
|
591
|
+
replaceResult: () => ({ child: "bar" }),
|
|
592
|
+
})
|
|
593
|
+
).toEqual({ child: "bar" });
|
|
594
|
+
expect(timeline.getParameterValue(["object", "child"])).toEqual("bar");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("does not cache results when `cacheResult` is set to false", async () => {
|
|
598
|
+
const timeline = createTimeline({ timeline: [], object: { child: "foo" } });
|
|
599
|
+
|
|
600
|
+
expect(
|
|
601
|
+
timeline.getParameterValue("object", {
|
|
602
|
+
replaceResult: () => ({ child: "bar" }),
|
|
603
|
+
cacheResult: false,
|
|
604
|
+
})
|
|
605
|
+
).toEqual({ child: "bar" });
|
|
606
|
+
expect(timeline.getParameterValue(["object", "child"])).toEqual("foo");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("all result caches are reset after every trial", async () => {
|
|
610
|
+
TestPlugin.setManualFinishTrialMode();
|
|
611
|
+
|
|
612
|
+
const timeline = createTimeline({
|
|
613
|
+
timeline: [
|
|
614
|
+
{
|
|
615
|
+
timeline: [{ type: TestPlugin }, { type: TestPlugin }],
|
|
616
|
+
object1: jest.fn().mockReturnValueOnce({ child: "foo" }),
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
object2: jest.fn().mockReturnValueOnce({ child: "foo" }),
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
timeline.run();
|
|
623
|
+
const childTimeline = timeline.children[0];
|
|
624
|
+
|
|
625
|
+
// First trial
|
|
626
|
+
for (const parameter of ["object1", "object2"]) {
|
|
627
|
+
expect(childTimeline.getParameterValue(parameter)).toEqual({ child: "foo" });
|
|
628
|
+
expect(childTimeline.getParameterValue([parameter, "child"])).toEqual("foo");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
await TestPlugin.finishTrial();
|
|
632
|
+
|
|
633
|
+
// Second trial, caches should have been reset
|
|
634
|
+
for (const parameter of ["object1", "object2"]) {
|
|
635
|
+
expect(childTimeline.getParameterValue(parameter)).toBeUndefined();
|
|
636
|
+
expect(childTimeline.getParameterValue([parameter, "child"])).toBeUndefined();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
describe("getDataParameter()", () => {
|
|
642
|
+
it("works when the `data` parameter is a function", async () => {
|
|
643
|
+
const timeline = createTimeline({ timeline: [], data: () => ({ custom: "value" }) });
|
|
644
|
+
expect(timeline.getDataParameter()).toEqual({ custom: "value" });
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("evaluates nested functions and timeline variables", async () => {
|
|
648
|
+
const timeline = createTimeline({
|
|
649
|
+
timeline: [],
|
|
650
|
+
timeline_variables: [{ x: 1 }],
|
|
651
|
+
data: {
|
|
652
|
+
custom: () => "value",
|
|
653
|
+
variable: new TimelineVariable("x"),
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
await timeline.run(); // required to properly evaluate timeline variables
|
|
658
|
+
|
|
659
|
+
expect(timeline.getDataParameter()).toEqual({ custom: "value", variable: 1 });
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("merges in all parent node `data` parameters", async () => {
|
|
663
|
+
const timeline = createTimeline({
|
|
664
|
+
timeline: [{ timeline: [], data: { custom: "value" } }],
|
|
665
|
+
data: { other: "value" },
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
await timeline.run();
|
|
669
|
+
|
|
670
|
+
expect((timeline.children[0] as Timeline).getDataParameter()).toEqual({
|
|
671
|
+
custom: "value",
|
|
672
|
+
other: "value",
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
describe("getResults()", () => {
|
|
678
|
+
it("recursively returns all results", async () => {
|
|
679
|
+
const timeline = createTimeline(exampleTimeline);
|
|
680
|
+
await timeline.run();
|
|
681
|
+
expect(timeline.getResults()).toEqual(
|
|
682
|
+
Array(3).fill(expect.objectContaining({ my: "result" }))
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("does not include `undefined` results", async () => {
|
|
687
|
+
const timeline = createTimeline(exampleTimeline);
|
|
688
|
+
await timeline.run();
|
|
689
|
+
|
|
690
|
+
jest.spyOn(timeline.children[0] as Trial, "getResult").mockReturnValue(undefined);
|
|
691
|
+
expect(timeline.getResults()).toEqual(
|
|
692
|
+
Array(2).fill(expect.objectContaining({ my: "result" }))
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe("getNaiveProgress()", () => {
|
|
698
|
+
it("returns the progress of a timeline at any time", async () => {
|
|
699
|
+
TestPlugin.setManualFinishTrialMode();
|
|
700
|
+
const { snapshots, createSnapshotCallback } = createSnapshotUtils(() =>
|
|
701
|
+
timeline.getNaiveProgress()
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const timeline = createTimeline({
|
|
705
|
+
on_timeline_start: createSnapshotCallback("mainTimelineStart"),
|
|
706
|
+
on_timeline_finish: createSnapshotCallback("mainTimelineFinish"),
|
|
707
|
+
timeline: [
|
|
708
|
+
{
|
|
709
|
+
type: TestPlugin,
|
|
710
|
+
on_start: createSnapshotCallback("trial1Start"),
|
|
711
|
+
on_finish: createSnapshotCallback("trial1Finish"),
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
on_timeline_start: createSnapshotCallback("nestedTimelineStart"),
|
|
715
|
+
on_timeline_finish: createSnapshotCallback("nestedTimelineFinish"),
|
|
716
|
+
timeline: [{ type: TestPlugin }, { type: TestPlugin }],
|
|
717
|
+
repetitions: 2,
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
});
|
|
721
|
+
expect(timeline.getNaiveProgress()).toEqual(0);
|
|
722
|
+
|
|
723
|
+
const runPromise = timeline.run();
|
|
724
|
+
expect(timeline.getNaiveProgress()).toEqual(0);
|
|
725
|
+
expect(snapshots.mainTimelineStart).toEqual(0);
|
|
726
|
+
expect(snapshots.trial1Start).toEqual(0);
|
|
727
|
+
|
|
728
|
+
await TestPlugin.finishTrial();
|
|
729
|
+
expect(timeline.getNaiveProgress()).toEqual(0.2);
|
|
730
|
+
expect(snapshots.trial1Finish).toEqual(0.2);
|
|
731
|
+
expect(snapshots.nestedTimelineStart).toEqual(0.2);
|
|
732
|
+
|
|
733
|
+
await TestPlugin.finishTrial();
|
|
734
|
+
expect(timeline.getNaiveProgress()).toEqual(0.4);
|
|
735
|
+
|
|
736
|
+
await TestPlugin.finishTrial();
|
|
737
|
+
expect(timeline.getNaiveProgress()).toEqual(0.6);
|
|
738
|
+
|
|
739
|
+
await TestPlugin.finishTrial();
|
|
740
|
+
expect(timeline.getNaiveProgress()).toEqual(0.8);
|
|
741
|
+
|
|
742
|
+
await TestPlugin.finishTrial();
|
|
743
|
+
expect(timeline.getNaiveProgress()).toEqual(1);
|
|
744
|
+
expect(snapshots.nestedTimelineFinish).toEqual(1);
|
|
745
|
+
expect(snapshots.mainTimelineFinish).toEqual(1);
|
|
746
|
+
|
|
747
|
+
await runPromise;
|
|
748
|
+
expect(timeline.getNaiveProgress()).toEqual(1);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("does not return values above 1", async () => {
|
|
752
|
+
const timeline = createTimeline({
|
|
753
|
+
timeline: [{ type: TestPlugin }],
|
|
754
|
+
loop_function: jest.fn().mockReturnValue(false).mockReturnValueOnce(true),
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
await timeline.run();
|
|
758
|
+
expect(timeline.getNaiveProgress()).toEqual(1);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
describe("getNaiveTrialCount()", () => {
|
|
763
|
+
it("correctly estimates the length of a timeline (including nested timelines)", async () => {
|
|
764
|
+
const timeline = createTimeline({
|
|
765
|
+
timeline: [
|
|
766
|
+
{ type: TestPlugin },
|
|
767
|
+
{ timeline: [{ type: TestPlugin }], repetitions: 2, timeline_variables: [] },
|
|
768
|
+
{ timeline: [{ type: TestPlugin }], repetitions: 5 },
|
|
769
|
+
],
|
|
770
|
+
repetitions: 3,
|
|
771
|
+
timeline_variables: [{ x: 1 }, { x: 2 }],
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
const estimate = (1 + 1 * 2 + 1 * 5) * 3 * 2;
|
|
775
|
+
expect(timeline.getNaiveTrialCount()).toEqual(estimate);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe("when the `sample` option is used", () => {
|
|
779
|
+
it("handles `with-replacement` sampling", async () => {
|
|
780
|
+
expect(
|
|
781
|
+
createTimeline({
|
|
782
|
+
timeline: [{ type: TestPlugin }],
|
|
783
|
+
timeline_variables: [{}, {}],
|
|
784
|
+
sample: { type: "with-replacement", size: 5 },
|
|
785
|
+
}).getNaiveTrialCount()
|
|
786
|
+
).toEqual(5);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("handles `without-replacement` sampling", async () => {
|
|
790
|
+
expect(
|
|
791
|
+
createTimeline({
|
|
792
|
+
timeline: [{ type: TestPlugin }],
|
|
793
|
+
timeline_variables: [{}, {}],
|
|
794
|
+
sample: { type: "without-replacement", size: 5 },
|
|
795
|
+
}).getNaiveTrialCount()
|
|
796
|
+
).toEqual(5);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("handles `fixed-repetitions` sampling", async () => {
|
|
800
|
+
expect(
|
|
801
|
+
createTimeline({
|
|
802
|
+
timeline: [{ type: TestPlugin }],
|
|
803
|
+
timeline_variables: [{}, {}],
|
|
804
|
+
sample: { type: "fixed-repetitions", size: 5 },
|
|
805
|
+
}).getNaiveTrialCount()
|
|
806
|
+
).toEqual(10);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("handles `alternate-groups` sampling", async () => {
|
|
810
|
+
expect(
|
|
811
|
+
createTimeline({
|
|
812
|
+
timeline: [{ type: TestPlugin }],
|
|
813
|
+
timeline_variables: [{}, {}, {}, {}],
|
|
814
|
+
sample: {
|
|
815
|
+
type: "alternate-groups",
|
|
816
|
+
groups: [
|
|
817
|
+
[0, 1],
|
|
818
|
+
[2, 3],
|
|
819
|
+
],
|
|
820
|
+
},
|
|
821
|
+
}).getNaiveTrialCount()
|
|
822
|
+
).toEqual(4);
|
|
823
|
+
|
|
824
|
+
expect(
|
|
825
|
+
createTimeline({
|
|
826
|
+
timeline: [{ type: TestPlugin }],
|
|
827
|
+
timeline_variables: [{}, {}, {}, {}],
|
|
828
|
+
sample: {
|
|
829
|
+
type: "alternate-groups",
|
|
830
|
+
groups: [[0, 1], [2]],
|
|
831
|
+
},
|
|
832
|
+
}).getNaiveTrialCount()
|
|
833
|
+
).toEqual(3);
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
describe("getLatestNode()", () => {
|
|
839
|
+
it("returns the latest `TimelineNode` or `undefined` when no node is active", async () => {
|
|
840
|
+
TestPlugin.setManualFinishTrialMode();
|
|
841
|
+
const { snapshots, createSnapshotCallback } = createSnapshotUtils(() =>
|
|
842
|
+
timeline.getLatestNode()
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
const timeline = createTimeline({
|
|
846
|
+
timeline: [
|
|
847
|
+
{ type: TestPlugin },
|
|
848
|
+
{
|
|
849
|
+
timeline: [{ type: TestPlugin }],
|
|
850
|
+
on_timeline_start: createSnapshotCallback("innerTimelineStart"),
|
|
851
|
+
on_timeline_finish: createSnapshotCallback("innerTimelineFinish"),
|
|
852
|
+
},
|
|
853
|
+
],
|
|
854
|
+
on_timeline_start: createSnapshotCallback("outerTimelineStart"),
|
|
855
|
+
on_timeline_finish: createSnapshotCallback("outerTimelineFinish"),
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
expect(timeline.getLatestNode()).toBe(timeline);
|
|
859
|
+
|
|
860
|
+
timeline.run();
|
|
861
|
+
|
|
862
|
+
expect(snapshots.outerTimelineStart).toBe(timeline);
|
|
863
|
+
expect(timeline.getLatestNode()).toBeInstanceOf(Trial);
|
|
864
|
+
expect(timeline.getLatestNode()).toBe(timeline.children[0]);
|
|
865
|
+
|
|
866
|
+
await TestPlugin.finishTrial();
|
|
867
|
+
expect(snapshots.innerTimelineStart).toBeInstanceOf(Timeline);
|
|
868
|
+
expect(snapshots.innerTimelineStart).toBe(timeline.children[1]);
|
|
869
|
+
|
|
870
|
+
const nestedTrial = (timeline.children[1] as Timeline).children[0];
|
|
871
|
+
expect(timeline.getLatestNode()).toBeInstanceOf(Trial);
|
|
872
|
+
expect(timeline.getLatestNode()).toBe(nestedTrial);
|
|
873
|
+
|
|
874
|
+
await TestPlugin.finishTrial();
|
|
875
|
+
expect(snapshots.innerTimelineFinish).toBe(nestedTrial);
|
|
876
|
+
expect(snapshots.outerTimelineFinish).toBe(nestedTrial);
|
|
877
|
+
expect(timeline.getLatestNode()).toBe(nestedTrial);
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
describe("getActiveTimelineByName()", () => {
|
|
882
|
+
it("returns the timeline with the given name", async () => {
|
|
883
|
+
TestPlugin.setManualFinishTrialMode();
|
|
884
|
+
|
|
885
|
+
const timeline = createTimeline({
|
|
886
|
+
timeline: [{ timeline: [{ type: TestPlugin }], name: "innerTimeline" }],
|
|
887
|
+
name: "outerTimeline",
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
timeline.run();
|
|
891
|
+
|
|
892
|
+
expect(timeline.getActiveTimelineByName("outerTimeline")).toBe(timeline);
|
|
893
|
+
expect(timeline.getActiveTimelineByName("innerTimeline")).toBe(
|
|
894
|
+
timeline.children[0] as Timeline
|
|
895
|
+
);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it("returns only active timelines", async () => {
|
|
899
|
+
TestPlugin.setManualFinishTrialMode();
|
|
900
|
+
|
|
901
|
+
const timeline = createTimeline({
|
|
902
|
+
timeline: [
|
|
903
|
+
{ type: TestPlugin },
|
|
904
|
+
{ timeline: [{ type: TestPlugin }], name: "innerTimeline" },
|
|
905
|
+
],
|
|
906
|
+
name: "outerTimeline",
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
timeline.run();
|
|
910
|
+
|
|
911
|
+
expect(timeline.getActiveTimelineByName("outerTimeline")).toBe(timeline);
|
|
912
|
+
expect(timeline.getActiveTimelineByName("innerTimeline")).toBeUndefined();
|
|
913
|
+
|
|
914
|
+
await TestPlugin.finishTrial();
|
|
915
|
+
|
|
916
|
+
expect(timeline.getActiveTimelineByName("innerTimeline")).toBe(
|
|
917
|
+
timeline.children[1] as Timeline
|
|
918
|
+
);
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
});
|