@lookit/record 4.0.0 → 5.0.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 +199 -8
- package/dist/index.browser.js +257 -39
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +16 -16
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +256 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +185 -14
- package/dist/index.js +256 -38
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/consentVideo.spec.ts +9 -1
- package/src/consentVideo.ts +10 -2
- package/src/errors.ts +28 -0
- package/src/index.spec.ts +835 -17
- package/src/recorder.spec.ts +677 -73
- package/src/recorder.ts +191 -35
- package/src/start.ts +51 -5
- package/src/stop.ts +56 -5
- package/src/trial.ts +128 -16
- package/src/types.ts +21 -0
- package/src/utils.spec.ts +129 -0
- package/src/utils.ts +45 -0
package/src/recorder.spec.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
// explicit any needed for media recorder mocks
|
|
1
3
|
import Data from "@lookit/data";
|
|
2
4
|
import { LookitWindow } from "@lookit/data/dist/types";
|
|
3
5
|
import Handlebars from "handlebars";
|
|
@@ -9,6 +11,7 @@ import play_icon from "../img/play-icon.svg";
|
|
|
9
11
|
import record_icon from "../img/record-icon.svg";
|
|
10
12
|
import {
|
|
11
13
|
CreateURLError,
|
|
14
|
+
NoFileNameError,
|
|
12
15
|
NoStopPromiseError,
|
|
13
16
|
NoWebCamElementError,
|
|
14
17
|
RecorderInitializeError,
|
|
@@ -16,6 +19,7 @@ import {
|
|
|
16
19
|
StreamActiveOnResetError,
|
|
17
20
|
StreamDataInitializeError,
|
|
18
21
|
StreamInactiveInitializeError,
|
|
22
|
+
TimeoutError,
|
|
19
23
|
} from "./errors";
|
|
20
24
|
import Recorder from "./recorder";
|
|
21
25
|
import { CSSWidthHeight } from "./types";
|
|
@@ -29,36 +33,123 @@ window.chs = {
|
|
|
29
33
|
response: {
|
|
30
34
|
id: "456",
|
|
31
35
|
},
|
|
32
|
-
|
|
36
|
+
pendingUploads: [],
|
|
37
|
+
} as unknown as typeof window.chs;
|
|
33
38
|
|
|
34
39
|
let originalDate: DateConstructor;
|
|
35
40
|
|
|
41
|
+
let consoleLogSpy: jest.SpyInstance<
|
|
42
|
+
void,
|
|
43
|
+
[message?: unknown, ...optionalParams: unknown[]],
|
|
44
|
+
unknown
|
|
45
|
+
>;
|
|
46
|
+
let consoleWarnSpy: jest.SpyInstance<
|
|
47
|
+
void,
|
|
48
|
+
[message?: unknown, ...optionalParams: unknown[]],
|
|
49
|
+
unknown
|
|
50
|
+
>;
|
|
51
|
+
let consoleErrorSpy: jest.SpyInstance<
|
|
52
|
+
void,
|
|
53
|
+
[message?: unknown, ...optionalParams: unknown[]],
|
|
54
|
+
unknown
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
type MockStream = {
|
|
58
|
+
getTracks: jest.Mock<Array<{ stop: jest.Mock<void, []> }>, []>;
|
|
59
|
+
clone: () => MockStream;
|
|
60
|
+
readonly active: boolean;
|
|
61
|
+
/** Utility for tests - manually force stream to stop (inactive state). */
|
|
62
|
+
__forceStop: () => void;
|
|
63
|
+
/** Utility for tests - manually force stream to start (active state). */
|
|
64
|
+
__forceStart: () => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
36
67
|
jest.mock("@lookit/data");
|
|
37
|
-
jest.mock("jspsych", () =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
jest.mock("jspsych", () => {
|
|
69
|
+
/**
|
|
70
|
+
* Helper to create a mock stream. The mock for
|
|
71
|
+
* jsPsych.pluginAPI.getCameraRecorder().stream will use this so that it
|
|
72
|
+
* dynamically returns streams that are active/inactive based on whether or
|
|
73
|
+
* not they've been stopped.
|
|
74
|
+
*
|
|
75
|
+
* @returns Mocked stream
|
|
76
|
+
*/
|
|
77
|
+
const createMockStream = (): MockStream => {
|
|
78
|
+
let stopped = false;
|
|
79
|
+
|
|
80
|
+
const stream: MockStream = {
|
|
81
|
+
// need to mock 'active', 'clone()', and 'getTracks()`
|
|
82
|
+
getTracks: jest.fn(() => {
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
stop: jest.fn(() => {
|
|
86
|
+
stopped = true;
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
];
|
|
58
90
|
}),
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
clone: jest.fn(() => createMockStream()),
|
|
92
|
+
/**
|
|
93
|
+
* Getter for stream's active property
|
|
94
|
+
*
|
|
95
|
+
* @returns Boolean indicating whether or not the stream is active.
|
|
96
|
+
*/
|
|
97
|
+
get active() {
|
|
98
|
+
return !stopped;
|
|
99
|
+
},
|
|
100
|
+
/** Utility for tests - manually force stream to stop (inactive state). */
|
|
101
|
+
__forceStop: () => {
|
|
102
|
+
stopped = true;
|
|
103
|
+
},
|
|
104
|
+
/** Utility for tests - maually force stream to start (active state). */
|
|
105
|
+
__forceStart: () => {
|
|
106
|
+
stopped = false;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return stream;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Persistent recorder that always gets returned
|
|
115
|
+
*
|
|
116
|
+
* @returns Mock recorder object
|
|
117
|
+
*/
|
|
118
|
+
const createMockRecorder = () => {
|
|
119
|
+
return {
|
|
120
|
+
addEventListener: jest.fn(),
|
|
121
|
+
mimeType: "video/webm",
|
|
122
|
+
start: jest.fn(),
|
|
123
|
+
stop: jest.fn(),
|
|
124
|
+
stream: createMockStream(),
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...jest.requireActual("jspsych"),
|
|
130
|
+
// factories for tests to call
|
|
131
|
+
__createMockRecorder: createMockRecorder,
|
|
132
|
+
__createMockStream: createMockStream,
|
|
133
|
+
initJsPsych: jest.fn().mockImplementation(() => {
|
|
134
|
+
const recorder = createMockRecorder();
|
|
135
|
+
return {
|
|
136
|
+
pluginAPI: {
|
|
137
|
+
initializeCameraRecorder: jest.fn((stream) => {
|
|
138
|
+
recorder.stream = stream;
|
|
139
|
+
}),
|
|
140
|
+
getCameraRecorder: jest.fn(() => recorder),
|
|
141
|
+
},
|
|
142
|
+
data: {
|
|
143
|
+
getLastTrialData: jest.fn().mockReturnValue({
|
|
144
|
+
values: jest
|
|
145
|
+
.fn()
|
|
146
|
+
.mockReturnValue([{ trial_type: "test-type", trial_index: 0 }]),
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}),
|
|
151
|
+
};
|
|
152
|
+
});
|
|
62
153
|
|
|
63
154
|
/**
|
|
64
155
|
* Remove new lines, indents (tabs or spaces), and empty HTML property values.
|
|
@@ -82,8 +173,23 @@ const cleanHTML = (html: string) => {
|
|
|
82
173
|
);
|
|
83
174
|
};
|
|
84
175
|
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
jest.useFakeTimers();
|
|
178
|
+
window.chs.pendingUploads = [];
|
|
179
|
+
|
|
180
|
+
// Hide the console output during tests. Tests can still assert on these spies to check console calls.
|
|
181
|
+
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
182
|
+
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
183
|
+
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
184
|
+
});
|
|
185
|
+
|
|
85
186
|
afterEach(() => {
|
|
86
187
|
jest.clearAllMocks();
|
|
188
|
+
jest.useRealTimers();
|
|
189
|
+
|
|
190
|
+
consoleLogSpy.mockRestore();
|
|
191
|
+
consoleWarnSpy.mockRestore();
|
|
192
|
+
consoleErrorSpy.mockRestore();
|
|
87
193
|
});
|
|
88
194
|
|
|
89
195
|
test("Recorder start", async () => {
|
|
@@ -99,32 +205,415 @@ test("Recorder start", async () => {
|
|
|
99
205
|
test("Recorder stop", async () => {
|
|
100
206
|
const jsPsych = initJsPsych();
|
|
101
207
|
const rec = new Recorder(jsPsych);
|
|
102
|
-
const stopPromise = Promise.resolve();
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
//
|
|
208
|
+
const stopPromise = Promise.resolve("url");
|
|
209
|
+
const uploadPromise = Promise.resolve();
|
|
210
|
+
|
|
211
|
+
// capture the actual mocked recorder and stream so that we can assert on the same instance
|
|
212
|
+
const recorderInstance = jsPsych.pluginAPI.getCameraRecorder();
|
|
213
|
+
const streamInstance = recorderInstance.stream;
|
|
214
|
+
|
|
215
|
+
// spy on Recorder helper functions
|
|
216
|
+
const preStopCheckSpy = jest.spyOn(rec as any, "preStopCheck");
|
|
217
|
+
const clearWebcamFeedSpy = jest.spyOn(rec as any, "clearWebcamFeed");
|
|
218
|
+
const stopTracksSpy = jest.spyOn(rec as any, "stopTracks");
|
|
219
|
+
const resetSpy = jest.spyOn(rec as any, "reset");
|
|
220
|
+
const getTracksSpy = jest.spyOn(streamInstance, "getTracks");
|
|
221
|
+
|
|
222
|
+
// set the s3 completeUpload function to resolve
|
|
223
|
+
rec["_s3"] = { completeUpload: jest.fn() } as any;
|
|
224
|
+
jest.spyOn(rec["_s3"] as any, "completeUpload").mockResolvedValue(undefined);
|
|
225
|
+
|
|
226
|
+
// manual mocks to simulate having started recording
|
|
227
|
+
rec["filename"] = "fakename";
|
|
106
228
|
rec["stopPromise"] = stopPromise;
|
|
107
229
|
|
|
108
|
-
|
|
109
|
-
|
|
230
|
+
expect(window.chs.pendingUploads).toStrictEqual([]);
|
|
231
|
+
|
|
232
|
+
const { stopped, uploaded } = rec.stop();
|
|
233
|
+
await stopped;
|
|
110
234
|
|
|
111
|
-
|
|
235
|
+
// calls recorder.preStopCheck()
|
|
236
|
+
expect(preStopCheckSpy).toHaveBeenCalledTimes(1);
|
|
237
|
+
// calls recorder.clearWebcamFeed(maintain_container_size)
|
|
238
|
+
expect(clearWebcamFeedSpy).toHaveBeenCalledTimes(1);
|
|
239
|
+
// calls recorder.stopTracks(), which calls stop() and stream.getTracks()
|
|
240
|
+
expect(stopTracksSpy).toHaveBeenCalledTimes(1);
|
|
241
|
+
expect(jsPsych.pluginAPI.getCameraRecorder().stop).toHaveBeenCalledTimes(1);
|
|
242
|
+
expect(getTracksSpy).toHaveBeenCalledTimes(1);
|
|
243
|
+
// calls recorder.reset
|
|
244
|
+
expect(resetSpy).toHaveBeenCalledTimes(1);
|
|
112
245
|
|
|
113
|
-
|
|
114
|
-
|
|
246
|
+
await uploaded;
|
|
247
|
+
|
|
248
|
+
expect(rec["s3"].completeUpload).toHaveBeenCalledTimes(1);
|
|
249
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
250
|
+
"Upload for fakename-uploaded completed.",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// check that the stop and upload promises are returned on stop
|
|
254
|
+
expect({ stopped, uploaded }).toStrictEqual({
|
|
255
|
+
stopped: stopPromise,
|
|
256
|
+
uploaded: uploadPromise,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// adds promise to window.chs.pendingUploads
|
|
260
|
+
expect(window.chs.pendingUploads.length).toBe(1);
|
|
261
|
+
expect(window.chs.pendingUploads[0].promise).toBeInstanceOf(Promise);
|
|
262
|
+
expect(window.chs.pendingUploads).toStrictEqual([
|
|
263
|
+
{ promise: uploadPromise, file: "fakename" },
|
|
264
|
+
]);
|
|
115
265
|
});
|
|
116
266
|
|
|
117
|
-
test("Recorder no stop promise", () => {
|
|
267
|
+
test("Recorder stop with no stop promise", () => {
|
|
118
268
|
const jsPsych = initJsPsych();
|
|
119
269
|
const rec = new Recorder(jsPsych);
|
|
120
270
|
|
|
121
271
|
// no stop promise
|
|
122
272
|
rec["stopPromise"] = undefined;
|
|
123
273
|
|
|
124
|
-
|
|
274
|
+
// throws immediately - no need to await the returned promises
|
|
275
|
+
expect(rec.stop).toThrow(NoStopPromiseError);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("Recorder stop and upload promises resolve", async () => {
|
|
279
|
+
const jsPsych = initJsPsych();
|
|
280
|
+
const rec = new Recorder(jsPsych);
|
|
281
|
+
|
|
282
|
+
// stop promise will resolve
|
|
283
|
+
rec["stopPromise"] = Promise.resolve("url");
|
|
284
|
+
|
|
285
|
+
// completeUpload will resolve
|
|
286
|
+
rec["_s3"] = { completeUpload: jest.fn(() => Promise.resolve()) } as any;
|
|
287
|
+
|
|
288
|
+
// manual mocks to simulate having started recording
|
|
289
|
+
rec["filename"] = "fakename";
|
|
290
|
+
|
|
291
|
+
const { stopped, uploaded } = rec.stop({
|
|
292
|
+
stop_timeout_ms: 100,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await jest.advanceTimersByTimeAsync(101);
|
|
296
|
+
|
|
297
|
+
await expect(stopped).resolves.toBe("url");
|
|
298
|
+
await expect(uploaded).resolves.toBeUndefined();
|
|
299
|
+
// make sure timeouts are cleared
|
|
300
|
+
expect(jest.getTimerCount()).toEqual(0);
|
|
301
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
302
|
+
"Upload for fakename-uploaded completed.",
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("Recorder stop promise times out", async () => {
|
|
307
|
+
const jsPsych = initJsPsych();
|
|
308
|
+
const rec = new Recorder(jsPsych);
|
|
309
|
+
|
|
310
|
+
// create a stop promise that never resolves
|
|
311
|
+
rec["stopPromise"] = new Promise<string>(() => {});
|
|
312
|
+
|
|
313
|
+
// manual mocks to simulate having started recording
|
|
314
|
+
rec["_s3"] = {
|
|
315
|
+
completeUpload: jest.fn().mockResolvedValue(undefined),
|
|
316
|
+
} as any;
|
|
317
|
+
rec["filename"] = "fakename";
|
|
318
|
+
|
|
319
|
+
const { stopped, uploaded } = rec.stop({
|
|
320
|
+
stop_timeout_ms: 100,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const stoppedObserved = stopped.catch((e) => e);
|
|
324
|
+
const uploadedObserved = uploaded.catch((e) => e);
|
|
325
|
+
|
|
326
|
+
await jest.advanceTimersByTimeAsync(101);
|
|
327
|
+
|
|
328
|
+
await expect(stoppedObserved).resolves.toBe("timeout");
|
|
329
|
+
await expect(uploadedObserved).resolves.toThrow(TimeoutError);
|
|
330
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
331
|
+
"Recorder stop timed out: fakename",
|
|
332
|
+
);
|
|
333
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
334
|
+
"Upload failed because recorder stop timed out",
|
|
335
|
+
);
|
|
336
|
+
const settled = await Promise.race([
|
|
337
|
+
Promise.allSettled(window.chs.pendingUploads.map((u) => u.promise)),
|
|
338
|
+
Promise.resolve("still-pending"),
|
|
339
|
+
]);
|
|
340
|
+
// The uploaded promise race is settled, but the upload promise in window.chs.pendingUploads should NOT be resolved - it should still be pending
|
|
341
|
+
expect(settled).toBe("still-pending");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("Recorder upload timeout with default duration", async () => {
|
|
345
|
+
const jsPsych = initJsPsych();
|
|
346
|
+
const rec = new Recorder(jsPsych);
|
|
347
|
+
|
|
348
|
+
// stop promise will resolve
|
|
349
|
+
rec["stopPromise"] = Promise.resolve("url");
|
|
350
|
+
rec["filename"] = "fakename";
|
|
351
|
+
|
|
352
|
+
// completeUpload never resolves - upload promise will timeout
|
|
353
|
+
const never = new Promise<void>(() => {});
|
|
354
|
+
rec["_s3"] = { completeUpload: jest.fn(() => never) } as any;
|
|
355
|
+
|
|
356
|
+
// default upload_timeout_ms is 10000
|
|
357
|
+
const { stopped, uploaded } = rec.stop();
|
|
358
|
+
|
|
359
|
+
// stop promise should resolve with the url
|
|
360
|
+
const url = await stopped;
|
|
361
|
+
expect(url).toBe("url");
|
|
362
|
+
|
|
363
|
+
// advance time by 10000 ms to trigger timeout
|
|
364
|
+
await jest.advanceTimersByTimeAsync(10000);
|
|
365
|
+
|
|
366
|
+
await expect(uploaded).resolves.toBe("timeout");
|
|
367
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
368
|
+
"Recorder upload timed out: fakename",
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Assert background upload is still pending
|
|
372
|
+
expect(window.chs.pendingUploads).toHaveLength(1);
|
|
373
|
+
let settled = false;
|
|
374
|
+
window.chs.pendingUploads[0].promise.finally(() => {
|
|
375
|
+
settled = true;
|
|
376
|
+
});
|
|
377
|
+
await Promise.resolve(); // flush microtasks
|
|
378
|
+
expect(settled).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("Recorder upload promise times out with duration parameter", async () => {
|
|
382
|
+
const jsPsych = initJsPsych();
|
|
383
|
+
const rec = new Recorder(jsPsych);
|
|
384
|
+
|
|
385
|
+
// stop promise will resolve
|
|
386
|
+
rec["stopPromise"] = Promise.resolve("url");
|
|
387
|
+
rec["filename"] = "fakename";
|
|
388
|
+
|
|
389
|
+
// completeUpload never resolves - upload promise will timeout
|
|
390
|
+
const never = new Promise<void>(() => {});
|
|
391
|
+
rec["_s3"] = { completeUpload: jest.fn(() => never) } as any;
|
|
392
|
+
|
|
393
|
+
const { stopped, uploaded } = rec.stop({
|
|
394
|
+
upload_timeout_ms: 100,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// stop promise should resolve with the url
|
|
398
|
+
await expect(stopped).resolves.toBe("url");
|
|
399
|
+
|
|
400
|
+
// advance fake timers so that the timeout triggers
|
|
401
|
+
await jest.advanceTimersByTimeAsync(100);
|
|
402
|
+
await expect(uploaded).resolves.toBe("timeout");
|
|
403
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
404
|
+
"Recorder upload timed out: fakename",
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// Assert background upload is still pending
|
|
408
|
+
expect(window.chs.pendingUploads).toHaveLength(1);
|
|
409
|
+
let settled = false;
|
|
410
|
+
window.chs.pendingUploads[0].promise.finally(() => {
|
|
411
|
+
settled = true;
|
|
412
|
+
});
|
|
413
|
+
await Promise.resolve(); // flush microtasks
|
|
414
|
+
expect(settled).toBe(false);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("Recorder upload promise with no timeout", async () => {
|
|
418
|
+
const jsPsych = initJsPsych();
|
|
419
|
+
const rec = new Recorder(jsPsych);
|
|
420
|
+
|
|
421
|
+
// stop promise will resolve
|
|
422
|
+
rec["stopPromise"] = Promise.resolve("url");
|
|
423
|
+
rec["filename"] = "fakename";
|
|
424
|
+
|
|
425
|
+
// completeUpload never resolves
|
|
426
|
+
const never = new Promise<void>(() => {});
|
|
427
|
+
rec["_s3"] = { completeUpload: jest.fn(() => never) } as any;
|
|
428
|
+
|
|
429
|
+
const { stopped, uploaded } = rec.stop({
|
|
430
|
+
upload_timeout_ms: null,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// stop promise should resolve with the url
|
|
434
|
+
await expect(stopped).resolves.toBe("url");
|
|
435
|
+
|
|
436
|
+
// advance fake timers to make sure that the default timeout does not trigger resolution
|
|
437
|
+
await jest.advanceTimersByTimeAsync(10000);
|
|
438
|
+
expect(uploaded).toStrictEqual(never);
|
|
439
|
+
|
|
440
|
+
// Assert background upload is original upload promise
|
|
441
|
+
expect(window.chs.pendingUploads).toHaveLength(1);
|
|
442
|
+
expect(window.chs.pendingUploads[0].promise).toStrictEqual(uploaded);
|
|
443
|
+
// Promise should still be pending
|
|
444
|
+
let settled = false;
|
|
445
|
+
window.chs.pendingUploads[0].promise.finally(() => {
|
|
446
|
+
settled = true;
|
|
447
|
+
});
|
|
448
|
+
await Promise.resolve(); // flush microtasks
|
|
449
|
+
expect(settled).toBe(false);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("Recorder stop with local download", async () => {
|
|
453
|
+
const jsPsych = initJsPsych();
|
|
454
|
+
const rec = new Recorder(jsPsych);
|
|
455
|
+
const stopPromise = Promise.resolve("url");
|
|
456
|
+
const uploadPromise = Promise.resolve();
|
|
457
|
+
|
|
458
|
+
// Download the file locally
|
|
459
|
+
rec["localDownload"] = true;
|
|
460
|
+
|
|
461
|
+
// manual mocks to simulate having started recording
|
|
462
|
+
// s3 is not defined when localDownload is true
|
|
463
|
+
rec["filename"] = "fakename";
|
|
464
|
+
rec["stopPromise"] = stopPromise;
|
|
465
|
+
const download = jest.fn();
|
|
466
|
+
rec["download"] = download;
|
|
467
|
+
|
|
468
|
+
expect(window.chs.pendingUploads).toStrictEqual([]);
|
|
469
|
+
|
|
470
|
+
const { stopped, uploaded } = rec.stop();
|
|
471
|
+
|
|
472
|
+
await uploaded;
|
|
473
|
+
|
|
474
|
+
// upload promise should call download
|
|
475
|
+
expect(download).toHaveBeenCalledTimes(1);
|
|
476
|
+
expect(download).toHaveBeenCalledWith(rec["filename"], "url");
|
|
477
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
478
|
+
"Upload for fakename-uploaded completed.",
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// check that the stop and upload promises are returned from recorder.stop
|
|
482
|
+
expect({ stopped, uploaded }).toStrictEqual({
|
|
483
|
+
stopped: stopPromise,
|
|
484
|
+
uploaded: uploadPromise,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// adds promise to window.chs.pendingUploads
|
|
488
|
+
expect(window.chs.pendingUploads.length).toBe(1);
|
|
489
|
+
expect(window.chs.pendingUploads[0].promise).toBeInstanceOf(Promise);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("Recorder stop with no filename", () => {
|
|
493
|
+
const jsPsych = initJsPsych();
|
|
494
|
+
const rec = new Recorder(jsPsych);
|
|
495
|
+
const stopPromise = Promise.resolve("url");
|
|
496
|
+
|
|
497
|
+
// manual mocks to simulate having started recording
|
|
498
|
+
rec["stopPromise"] = stopPromise;
|
|
499
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
500
|
+
|
|
501
|
+
// no filename
|
|
502
|
+
rec["filename"] = undefined;
|
|
503
|
+
|
|
504
|
+
expect(rec.stop).toThrow(NoFileNameError);
|
|
125
505
|
});
|
|
126
506
|
|
|
127
|
-
test("Recorder
|
|
507
|
+
test("Recorder stop throws with inactive stream", () => {
|
|
508
|
+
const jsPsych = initJsPsych();
|
|
509
|
+
const rec = new Recorder(jsPsych);
|
|
510
|
+
const stopPromise = Promise.resolve("url");
|
|
511
|
+
|
|
512
|
+
// manual mocks to simulate having started recording
|
|
513
|
+
rec["stopPromise"] = stopPromise;
|
|
514
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
515
|
+
rec["filename"] = "filename";
|
|
516
|
+
|
|
517
|
+
// de-activate stream
|
|
518
|
+
(
|
|
519
|
+
jsPsych.pluginAPI.getCameraRecorder().stream as unknown as MockStream
|
|
520
|
+
).__forceStop();
|
|
521
|
+
|
|
522
|
+
expect(rec.stop).toThrow(StreamInactiveInitializeError);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("Recorder stop catches error in local download", async () => {
|
|
526
|
+
const jsPsych = initJsPsych();
|
|
527
|
+
const rec = new Recorder(jsPsych);
|
|
528
|
+
const stopPromise = Promise.resolve("url");
|
|
529
|
+
|
|
530
|
+
// Download the file locally
|
|
531
|
+
rec["localDownload"] = true;
|
|
532
|
+
|
|
533
|
+
// manual mocks to simulate having started recording
|
|
534
|
+
// s3 is not defined when localDownload is true
|
|
535
|
+
rec["filename"] = "fakename";
|
|
536
|
+
rec["stopPromise"] = stopPromise;
|
|
537
|
+
const download = jest.fn().mockImplementation(() => {
|
|
538
|
+
throw new Error("Something went wrong.");
|
|
539
|
+
});
|
|
540
|
+
rec["download"] = download;
|
|
541
|
+
|
|
542
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
543
|
+
const { stopped, uploaded } = rec.stop();
|
|
544
|
+
|
|
545
|
+
await expect(uploaded).rejects.toThrow("Something went wrong.");
|
|
546
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
547
|
+
"Local download failed: ",
|
|
548
|
+
Error("Something went wrong."),
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("Recorder stop catches error in upload", async () => {
|
|
553
|
+
const jsPsych = initJsPsych();
|
|
554
|
+
const rec = new Recorder(jsPsych);
|
|
555
|
+
const stopPromise = Promise.resolve("url");
|
|
556
|
+
|
|
557
|
+
// set the s3 completeUpload function to resolve
|
|
558
|
+
rec["_s3"] = { completeUpload: jest.fn() } as any;
|
|
559
|
+
jest.spyOn(rec["_s3"] as any, "completeUpload").mockImplementation(() => {
|
|
560
|
+
throw new Error("Something broke.");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// manual mocks to simulate having started recording
|
|
564
|
+
rec["filename"] = "fakename";
|
|
565
|
+
rec["stopPromise"] = stopPromise;
|
|
566
|
+
|
|
567
|
+
expect(window.chs.pendingUploads).toStrictEqual([]);
|
|
568
|
+
|
|
569
|
+
const { stopped, uploaded } = rec.stop();
|
|
570
|
+
|
|
571
|
+
await stopped;
|
|
572
|
+
|
|
573
|
+
await expect(uploaded).rejects.toThrow("Something broke.");
|
|
574
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
575
|
+
"Upload failed: ",
|
|
576
|
+
Error("Something broke."),
|
|
577
|
+
);
|
|
578
|
+
expect(window.chs.pendingUploads.length).toBe(1);
|
|
579
|
+
expect(window.chs.pendingUploads[0].promise).toBeInstanceOf(Promise);
|
|
580
|
+
await expect(
|
|
581
|
+
Promise.allSettled(window.chs.pendingUploads.map((u) => u.promise)),
|
|
582
|
+
).resolves.toStrictEqual([
|
|
583
|
+
{
|
|
584
|
+
status: "rejected",
|
|
585
|
+
reason: new Error("Something broke."),
|
|
586
|
+
},
|
|
587
|
+
]);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("Recorder stop tries to reset after stopping and handles error", async () => {
|
|
591
|
+
const jsPsych = initJsPsych();
|
|
592
|
+
const rec = new Recorder(jsPsych);
|
|
593
|
+
const stopPromise = Promise.resolve("url");
|
|
594
|
+
|
|
595
|
+
// manual mocks to simulate having started recording
|
|
596
|
+
rec["filename"] = "fakename";
|
|
597
|
+
rec["stopPromise"] = stopPromise;
|
|
598
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
599
|
+
|
|
600
|
+
const reset = jest.fn().mockImplementation(() => {
|
|
601
|
+
throw new Error("Reset failed.");
|
|
602
|
+
});
|
|
603
|
+
rec["reset"] = reset;
|
|
604
|
+
|
|
605
|
+
const { stopped } = rec.stop();
|
|
606
|
+
|
|
607
|
+
await stopped;
|
|
608
|
+
|
|
609
|
+
expect(reset).toHaveBeenCalledTimes(1);
|
|
610
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
611
|
+
"Error while resetting recorder after stop: ",
|
|
612
|
+
Error("Reset failed."),
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("Recorder initialize error throws from recorder start", () => {
|
|
128
617
|
const jsPsych = initJsPsych();
|
|
129
618
|
const rec = new Recorder(jsPsych);
|
|
130
619
|
const getCameraRecorder = jsPsych.pluginAPI.getCameraRecorder;
|
|
@@ -142,37 +631,91 @@ test("Recorder initialize error", () => {
|
|
|
142
631
|
jsPsych.pluginAPI.getCameraRecorder = getCameraRecorder;
|
|
143
632
|
});
|
|
144
633
|
|
|
145
|
-
test("Recorder
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const handleStop = rec["handleStop"](resolve);
|
|
634
|
+
test("Recorder initialize error throws from recorder stop", () => {
|
|
635
|
+
const jsPsych = initJsPsych();
|
|
636
|
+
const rec = new Recorder(jsPsych);
|
|
637
|
+
const getCameraRecorder = jsPsych.pluginAPI.getCameraRecorder;
|
|
150
638
|
|
|
151
|
-
//
|
|
152
|
-
rec["
|
|
153
|
-
rec["
|
|
154
|
-
|
|
639
|
+
// fy other requirements for recorder.stop
|
|
640
|
+
rec["stopPromise"] = new Promise<string>(() => {});
|
|
641
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
642
|
+
rec["filename"] = "fakename";
|
|
155
643
|
|
|
156
|
-
//
|
|
157
|
-
|
|
644
|
+
// no recorder
|
|
645
|
+
jsPsych.pluginAPI.getCameraRecorder = jest.fn().mockReturnValue(undefined);
|
|
646
|
+
jsPsych.pluginAPI.getMicrophoneRecorder = jest
|
|
647
|
+
.fn()
|
|
648
|
+
.mockReturnValue(undefined);
|
|
649
|
+
|
|
650
|
+
expect(rec.stop).toThrow(RecorderInitializeError);
|
|
158
651
|
|
|
159
|
-
|
|
652
|
+
jsPsych.pluginAPI.getCameraRecorder = getCameraRecorder;
|
|
653
|
+
expect(window.chs.pendingUploads.length).toBe(0);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("S3 undefined error throws from recorder stop", () => {
|
|
657
|
+
const jsPsych = initJsPsych();
|
|
658
|
+
const rec = new Recorder(jsPsych);
|
|
659
|
+
|
|
660
|
+
// satisfy other requirements for recorder.stop
|
|
661
|
+
rec["stopPromise"] = new Promise<string>(() => {});
|
|
662
|
+
rec["filename"] = "fakename";
|
|
663
|
+
|
|
664
|
+
// no s3
|
|
665
|
+
rec["_s3"] = undefined;
|
|
160
666
|
|
|
161
|
-
// Upload the file to s3
|
|
162
667
|
rec["localDownload"] = false;
|
|
163
|
-
rec["_s3"] = new Data.LookitS3("some key");
|
|
164
668
|
|
|
165
|
-
|
|
669
|
+
expect(rec.stop).toThrow(S3UndefinedError);
|
|
670
|
+
expect(window.chs.pendingUploads.length).toBe(0);
|
|
671
|
+
});
|
|
166
672
|
|
|
167
|
-
|
|
168
|
-
|
|
673
|
+
test("S3 undefined error does not throw from recorder stop with local download", () => {
|
|
674
|
+
const jsPsych = initJsPsych();
|
|
675
|
+
const rec = new Recorder(jsPsych);
|
|
676
|
+
|
|
677
|
+
// satisty other requirements for recorder.stop and s3 upload
|
|
678
|
+
rec["stopPromise"] = new Promise<string>(() => {});
|
|
679
|
+
rec["filename"] = "fakename";
|
|
680
|
+
|
|
681
|
+
// no s3
|
|
682
|
+
rec["_s3"] = undefined;
|
|
683
|
+
|
|
684
|
+
rec["localDownload"] = true;
|
|
685
|
+
|
|
686
|
+
expect(rec.stop).not.toThrow(S3UndefinedError);
|
|
169
687
|
});
|
|
170
688
|
|
|
171
|
-
test("Recorder handleStop
|
|
689
|
+
test("Recorder handleStop", () => {
|
|
690
|
+
// Mock createObjectURL to return a specific value
|
|
691
|
+
const originalCreateObjectURL = global.URL.createObjectURL;
|
|
692
|
+
global.URL.createObjectURL = jest.fn().mockReturnValue("mock-url");
|
|
693
|
+
|
|
694
|
+
const jsPsych = initJsPsych();
|
|
695
|
+
const rec = new Recorder(jsPsych);
|
|
696
|
+
|
|
697
|
+
// stop promise resolve function that is passed into handleStop
|
|
698
|
+
const resolve = jest.fn();
|
|
699
|
+
|
|
700
|
+
// Manual mock
|
|
701
|
+
rec["blobs"] = ["some recorded data" as unknown as Blob];
|
|
702
|
+
|
|
703
|
+
const handleStop = rec["handleStop"](resolve);
|
|
704
|
+
|
|
705
|
+
handleStop();
|
|
706
|
+
|
|
707
|
+
expect(resolve).toHaveBeenCalledWith("mock-url");
|
|
708
|
+
expect(rec["url"]).toBe("mock-url");
|
|
709
|
+
|
|
710
|
+
// Restore the original createObjectURL function
|
|
711
|
+
global.URL.createObjectURL = originalCreateObjectURL;
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("Recorder handleStop error with no blob data", () => {
|
|
172
715
|
const rec = new Recorder(initJsPsych());
|
|
173
716
|
const resolve = jest.fn();
|
|
174
717
|
const handleStop = rec["handleStop"](resolve);
|
|
175
|
-
expect(
|
|
718
|
+
expect(handleStop).toThrow(CreateURLError);
|
|
176
719
|
});
|
|
177
720
|
|
|
178
721
|
test("Recorder handleDataAvailable", () => {
|
|
@@ -262,20 +805,25 @@ test("Webcam feed is removed when stream access stops", async () => {
|
|
|
262
805
|
|
|
263
806
|
const jsPsych = initJsPsych();
|
|
264
807
|
const rec = new Recorder(jsPsych);
|
|
265
|
-
const stopPromise = Promise.resolve();
|
|
808
|
+
const stopPromise = Promise.resolve("url");
|
|
266
809
|
|
|
810
|
+
// manual mocks
|
|
811
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
812
|
+
rec["filename"] = "fakename";
|
|
267
813
|
rec["stopPromise"] = stopPromise;
|
|
814
|
+
|
|
268
815
|
rec.insertWebcamFeed(webcam_div);
|
|
269
816
|
expect(document.body.innerHTML).toContain("<video");
|
|
270
817
|
|
|
271
|
-
|
|
818
|
+
const { stopped } = rec.stop();
|
|
819
|
+
await stopped;
|
|
272
820
|
expect(document.body.innerHTML).not.toContain("<video");
|
|
273
821
|
|
|
274
822
|
// Reset the document body.
|
|
275
823
|
document.body.innerHTML = "";
|
|
276
824
|
});
|
|
277
825
|
|
|
278
|
-
test("Webcam feed container maintains size with recorder.stop
|
|
826
|
+
test("Webcam feed container maintains size with maintain_container_size: true passed to recorder.stop", async () => {
|
|
279
827
|
// Add webcam container to document body.
|
|
280
828
|
const webcam_container_id = "webcam-container";
|
|
281
829
|
document.body.innerHTML = `<div id="${webcam_container_id}"></div>`;
|
|
@@ -285,9 +833,13 @@ test("Webcam feed container maintains size with recorder.stop(true)", async () =
|
|
|
285
833
|
|
|
286
834
|
const jsPsych = initJsPsych();
|
|
287
835
|
const rec = new Recorder(jsPsych);
|
|
288
|
-
const stopPromise = Promise.resolve();
|
|
836
|
+
const stopPromise = Promise.resolve("url");
|
|
289
837
|
|
|
838
|
+
// manual mocks
|
|
839
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
840
|
+
rec["filename"] = "fakename";
|
|
290
841
|
rec["stopPromise"] = stopPromise;
|
|
842
|
+
|
|
291
843
|
rec.insertWebcamFeed(webcam_div);
|
|
292
844
|
|
|
293
845
|
// Mock the return values for the video element's offsetHeight/offsetWidth, which are used to set the container size
|
|
@@ -298,7 +850,8 @@ test("Webcam feed container maintains size with recorder.stop(true)", async () =
|
|
|
298
850
|
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
299
851
|
.mockImplementation(() => 300);
|
|
300
852
|
|
|
301
|
-
|
|
853
|
+
const { stopped } = rec.stop({ maintain_container_size: true });
|
|
854
|
+
await stopped;
|
|
302
855
|
|
|
303
856
|
// Container div's dimensions should match the video element dimensions
|
|
304
857
|
expect(
|
|
@@ -325,9 +878,13 @@ test("Webcam feed container size is not maintained with recorder.stop(false)", a
|
|
|
325
878
|
|
|
326
879
|
const jsPsych = initJsPsych();
|
|
327
880
|
const rec = new Recorder(jsPsych);
|
|
328
|
-
const stopPromise = Promise.resolve();
|
|
881
|
+
const stopPromise = Promise.resolve("url");
|
|
329
882
|
|
|
883
|
+
// manual mocks
|
|
884
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
885
|
+
rec["filename"] = "fakename";
|
|
330
886
|
rec["stopPromise"] = stopPromise;
|
|
887
|
+
|
|
331
888
|
rec.insertWebcamFeed(webcam_div);
|
|
332
889
|
|
|
333
890
|
// Mock the return values for the video element offsetHeight/offsetWidth, which are used to set the container size
|
|
@@ -338,7 +895,8 @@ test("Webcam feed container size is not maintained with recorder.stop(false)", a
|
|
|
338
895
|
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
339
896
|
.mockImplementation(() => 300);
|
|
340
897
|
|
|
341
|
-
|
|
898
|
+
const { stopped } = rec.stop();
|
|
899
|
+
await stopped;
|
|
342
900
|
|
|
343
901
|
// Container div's dimensions should not be set
|
|
344
902
|
expect(
|
|
@@ -355,7 +913,7 @@ test("Webcam feed container size is not maintained with recorder.stop(false)", a
|
|
|
355
913
|
jest.restoreAllMocks();
|
|
356
914
|
});
|
|
357
915
|
|
|
358
|
-
test("Webcam feed container size is not maintained with recorder.stop()",
|
|
916
|
+
test("Webcam feed container size is not maintained with recorder.stop()", () => {
|
|
359
917
|
// Add webcam container to document body.
|
|
360
918
|
const webcam_container_id = "webcam-container";
|
|
361
919
|
document.body.innerHTML = `<div id="${webcam_container_id}"></div>`;
|
|
@@ -365,9 +923,13 @@ test("Webcam feed container size is not maintained with recorder.stop()", async
|
|
|
365
923
|
|
|
366
924
|
const jsPsych = initJsPsych();
|
|
367
925
|
const rec = new Recorder(jsPsych);
|
|
368
|
-
const stopPromise = Promise.resolve();
|
|
926
|
+
const stopPromise = Promise.resolve("url");
|
|
369
927
|
|
|
928
|
+
// manual mocks
|
|
929
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
930
|
+
rec["filename"] = "fakename";
|
|
370
931
|
rec["stopPromise"] = stopPromise;
|
|
932
|
+
|
|
371
933
|
rec.insertWebcamFeed(webcam_div);
|
|
372
934
|
|
|
373
935
|
// Mock the return values for the video element offsetHeight/offsetWidth, which are used to set the container size
|
|
@@ -378,7 +940,7 @@ test("Webcam feed container size is not maintained with recorder.stop()", async
|
|
|
378
940
|
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
379
941
|
.mockImplementation(() => 300);
|
|
380
942
|
|
|
381
|
-
|
|
943
|
+
rec.stop();
|
|
382
944
|
|
|
383
945
|
// Container div's dimensions should not be set
|
|
384
946
|
expect(
|
|
@@ -433,12 +995,10 @@ test("Recorder initializeRecorder", () => {
|
|
|
433
995
|
|
|
434
996
|
test("Recorder download", () => {
|
|
435
997
|
const rec = new Recorder(initJsPsych());
|
|
436
|
-
rec["url"] = "some url";
|
|
437
|
-
rec["filename"] = "some filename";
|
|
438
998
|
const download = rec["download"];
|
|
439
999
|
const click = jest.spyOn(HTMLAnchorElement.prototype, "click");
|
|
440
1000
|
|
|
441
|
-
download();
|
|
1001
|
+
download("some filename", "some url");
|
|
442
1002
|
|
|
443
1003
|
expect(click).toHaveBeenCalledTimes(1);
|
|
444
1004
|
});
|
|
@@ -453,7 +1013,7 @@ test("Recorder s3 get error when undefined", () => {
|
|
|
453
1013
|
test("Recorder reset error when stream active", () => {
|
|
454
1014
|
const jsPsych = initJsPsych();
|
|
455
1015
|
const rec = new Recorder(jsPsych);
|
|
456
|
-
expect(
|
|
1016
|
+
expect(rec.reset).toThrow(StreamActiveOnResetError);
|
|
457
1017
|
});
|
|
458
1018
|
|
|
459
1019
|
test("Recorder reset", () => {
|
|
@@ -548,7 +1108,7 @@ test("Record initialize error inactive stream", () => {
|
|
|
548
1108
|
.fn()
|
|
549
1109
|
.mockReturnValue({ stream: { active: false } });
|
|
550
1110
|
|
|
551
|
-
expect(
|
|
1111
|
+
expect(initializeCheck).toThrow(StreamInactiveInitializeError);
|
|
552
1112
|
|
|
553
1113
|
jsPsych.pluginAPI.getCameraRecorder = getCameraRecorder;
|
|
554
1114
|
});
|
|
@@ -560,7 +1120,7 @@ test("Record initialize error inactive stream", () => {
|
|
|
560
1120
|
|
|
561
1121
|
rec["blobs"] = ["some stream data" as unknown as Blob];
|
|
562
1122
|
|
|
563
|
-
expect(
|
|
1123
|
+
expect(initializeCheck).toThrow(StreamDataInitializeError);
|
|
564
1124
|
});
|
|
565
1125
|
|
|
566
1126
|
test("Recorder insert record Feed with height/width", () => {
|
|
@@ -576,7 +1136,7 @@ test("Recorder insert record Feed with height/width", () => {
|
|
|
576
1136
|
};
|
|
577
1137
|
|
|
578
1138
|
jest.spyOn(Handlebars, "compile");
|
|
579
|
-
jest.spyOn(rec, "insertVideoFeed");
|
|
1139
|
+
jest.spyOn(rec as any, "insertVideoFeed");
|
|
580
1140
|
|
|
581
1141
|
rec.insertRecordFeed(display);
|
|
582
1142
|
|
|
@@ -713,3 +1273,47 @@ test("New recorder uses a default mime type if none is set already", () => {
|
|
|
713
1273
|
|
|
714
1274
|
jsPsych.pluginAPI.initializeCameraRecorder = originalInitializeCameraRecorder;
|
|
715
1275
|
});
|
|
1276
|
+
|
|
1277
|
+
test("Recorder generates a timeout handler function with the event that is being awaited", () => {
|
|
1278
|
+
const jsPsych = initJsPsych();
|
|
1279
|
+
const rec = new Recorder(jsPsych);
|
|
1280
|
+
|
|
1281
|
+
const timeout_fn = rec["createTimeoutHandler"]("long process", "fakename");
|
|
1282
|
+
|
|
1283
|
+
expect(timeout_fn).toBeInstanceOf(Function);
|
|
1284
|
+
|
|
1285
|
+
timeout_fn();
|
|
1286
|
+
|
|
1287
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
1288
|
+
"Recorder long process timed out: fakename",
|
|
1289
|
+
);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
test("Recorder createTimeoutHandler catches error when trying to reset recorder after timeout", () => {
|
|
1293
|
+
const jsPsych = initJsPsych();
|
|
1294
|
+
const rec = new Recorder(jsPsych);
|
|
1295
|
+
|
|
1296
|
+
// mock error thrown from Recorder.reset
|
|
1297
|
+
jest.spyOn(rec, "reset").mockImplementation(() => {
|
|
1298
|
+
throw new Error("Could not reset.");
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// de-activate stream
|
|
1302
|
+
(
|
|
1303
|
+
jsPsych.pluginAPI.getCameraRecorder().stream as unknown as MockStream
|
|
1304
|
+
).__forceStop();
|
|
1305
|
+
|
|
1306
|
+
const timeout_fn = rec["createTimeoutHandler"]("upload", "fakename");
|
|
1307
|
+
|
|
1308
|
+
expect(timeout_fn).toBeInstanceOf(Function);
|
|
1309
|
+
|
|
1310
|
+
timeout_fn();
|
|
1311
|
+
|
|
1312
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
1313
|
+
"Recorder upload timed out: fakename",
|
|
1314
|
+
);
|
|
1315
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1316
|
+
"Error while resetting recorder after timeout: ",
|
|
1317
|
+
new Error("Could not reset."),
|
|
1318
|
+
);
|
|
1319
|
+
});
|