@lookit/record 4.1.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 +82 -10
- package/dist/index.browser.js +201 -41
- 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 +200 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +83 -7
- package/dist/index.js +200 -40
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/consentVideo.spec.ts +9 -0
- package/src/consentVideo.ts +5 -1
- package/src/errors.ts +28 -0
- package/src/index.spec.ts +497 -54
- package/src/recorder.spec.ts +669 -109
- package/src/recorder.ts +184 -36
- package/src/start.ts +45 -5
- package/src/stop.ts +29 -12
- package/src/trial.ts +42 -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,7 +1,9 @@
|
|
|
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";
|
|
4
|
-
import { initJsPsych
|
|
6
|
+
import { initJsPsych } from "jspsych";
|
|
5
7
|
import playbackFeed from "../hbs/playback-feed.hbs";
|
|
6
8
|
import recordFeed from "../hbs/record-feed.hbs";
|
|
7
9
|
import webcamFeed from "../hbs/webcam-feed.hbs";
|
|
@@ -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);
|
|
505
|
+
});
|
|
506
|
+
|
|
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
|
+
]);
|
|
125
588
|
});
|
|
126
589
|
|
|
127
|
-
test("Recorder
|
|
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,81 +631,91 @@ test("Recorder initialize error", () => {
|
|
|
142
631
|
jsPsych.pluginAPI.getCameraRecorder = getCameraRecorder;
|
|
143
632
|
});
|
|
144
633
|
|
|
145
|
-
test("Recorder
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
clone: jest.fn(),
|
|
150
|
-
getTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
|
|
151
|
-
};
|
|
152
|
-
mockStream.clone = jest.fn().mockReturnValue(mockStream);
|
|
153
|
-
// We need a mock MediaRecorder object that uses the custom mockStream
|
|
154
|
-
const mockRecorder = {
|
|
155
|
-
addEventListener: jest.fn(),
|
|
156
|
-
mimeType: "video/webm",
|
|
157
|
-
start: jest.fn(),
|
|
158
|
-
stop: jest.fn(),
|
|
159
|
-
stream: mockStream,
|
|
160
|
-
};
|
|
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;
|
|
161
638
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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";
|
|
643
|
+
|
|
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);
|
|
174
651
|
|
|
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();
|
|
175
658
|
const rec = new Recorder(jsPsych);
|
|
176
659
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const resetSpy = jest.spyOn(rec, "reset" satisfies keyof typeof rec);
|
|
660
|
+
// satisfy other requirements for recorder.stop
|
|
661
|
+
rec["stopPromise"] = new Promise<string>(() => {});
|
|
662
|
+
rec["filename"] = "fakename";
|
|
181
663
|
|
|
182
|
-
//
|
|
183
|
-
rec["
|
|
184
|
-
rec["blobs"] = ["some recorded data" as unknown as Blob];
|
|
185
|
-
URL.createObjectURL = jest.fn();
|
|
664
|
+
// no s3
|
|
665
|
+
rec["_s3"] = undefined;
|
|
186
666
|
|
|
187
|
-
|
|
188
|
-
rec["localDownload"] = true;
|
|
667
|
+
rec["localDownload"] = false;
|
|
189
668
|
|
|
190
|
-
|
|
191
|
-
|
|
669
|
+
expect(rec.stop).toThrow(S3UndefinedError);
|
|
670
|
+
expect(window.chs.pendingUploads.length).toBe(0);
|
|
671
|
+
});
|
|
192
672
|
|
|
193
|
-
|
|
673
|
+
test("S3 undefined error does not throw from recorder stop with local download", () => {
|
|
674
|
+
const jsPsych = initJsPsych();
|
|
675
|
+
const rec = new Recorder(jsPsych);
|
|
194
676
|
|
|
195
|
-
|
|
677
|
+
// satisty other requirements for recorder.stop and s3 upload
|
|
678
|
+
rec["stopPromise"] = new Promise<string>(() => {});
|
|
679
|
+
rec["filename"] = "fakename";
|
|
196
680
|
|
|
197
|
-
|
|
198
|
-
|
|
681
|
+
// no s3
|
|
682
|
+
rec["_s3"] = undefined;
|
|
199
683
|
|
|
200
|
-
|
|
201
|
-
rec["localDownload"] = false;
|
|
202
|
-
rec["_s3"] = new Data.LookitS3("some key");
|
|
684
|
+
rec["localDownload"] = true;
|
|
203
685
|
|
|
204
|
-
|
|
686
|
+
expect(rec.stop).not.toThrow(S3UndefinedError);
|
|
687
|
+
});
|
|
688
|
+
|
|
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
|
|
205
701
|
rec["blobs"] = ["some recorded data" as unknown as Blob];
|
|
206
702
|
|
|
207
|
-
|
|
703
|
+
const handleStop = rec["handleStop"](resolve);
|
|
704
|
+
|
|
705
|
+
handleStop();
|
|
208
706
|
|
|
209
|
-
expect(
|
|
210
|
-
expect(
|
|
707
|
+
expect(resolve).toHaveBeenCalledWith("mock-url");
|
|
708
|
+
expect(rec["url"]).toBe("mock-url");
|
|
211
709
|
|
|
212
|
-
|
|
710
|
+
// Restore the original createObjectURL function
|
|
711
|
+
global.URL.createObjectURL = originalCreateObjectURL;
|
|
213
712
|
});
|
|
214
713
|
|
|
215
|
-
test("Recorder handleStop error with no
|
|
714
|
+
test("Recorder handleStop error with no blob data", () => {
|
|
216
715
|
const rec = new Recorder(initJsPsych());
|
|
217
716
|
const resolve = jest.fn();
|
|
218
717
|
const handleStop = rec["handleStop"](resolve);
|
|
219
|
-
expect(
|
|
718
|
+
expect(handleStop).toThrow(CreateURLError);
|
|
220
719
|
});
|
|
221
720
|
|
|
222
721
|
test("Recorder handleDataAvailable", () => {
|
|
@@ -306,20 +805,25 @@ test("Webcam feed is removed when stream access stops", async () => {
|
|
|
306
805
|
|
|
307
806
|
const jsPsych = initJsPsych();
|
|
308
807
|
const rec = new Recorder(jsPsych);
|
|
309
|
-
const stopPromise = Promise.resolve();
|
|
808
|
+
const stopPromise = Promise.resolve("url");
|
|
310
809
|
|
|
810
|
+
// manual mocks
|
|
811
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
812
|
+
rec["filename"] = "fakename";
|
|
311
813
|
rec["stopPromise"] = stopPromise;
|
|
814
|
+
|
|
312
815
|
rec.insertWebcamFeed(webcam_div);
|
|
313
816
|
expect(document.body.innerHTML).toContain("<video");
|
|
314
817
|
|
|
315
|
-
|
|
818
|
+
const { stopped } = rec.stop();
|
|
819
|
+
await stopped;
|
|
316
820
|
expect(document.body.innerHTML).not.toContain("<video");
|
|
317
821
|
|
|
318
822
|
// Reset the document body.
|
|
319
823
|
document.body.innerHTML = "";
|
|
320
824
|
});
|
|
321
825
|
|
|
322
|
-
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 () => {
|
|
323
827
|
// Add webcam container to document body.
|
|
324
828
|
const webcam_container_id = "webcam-container";
|
|
325
829
|
document.body.innerHTML = `<div id="${webcam_container_id}"></div>`;
|
|
@@ -329,9 +833,13 @@ test("Webcam feed container maintains size with recorder.stop(true)", async () =
|
|
|
329
833
|
|
|
330
834
|
const jsPsych = initJsPsych();
|
|
331
835
|
const rec = new Recorder(jsPsych);
|
|
332
|
-
const stopPromise = Promise.resolve();
|
|
836
|
+
const stopPromise = Promise.resolve("url");
|
|
333
837
|
|
|
838
|
+
// manual mocks
|
|
839
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
840
|
+
rec["filename"] = "fakename";
|
|
334
841
|
rec["stopPromise"] = stopPromise;
|
|
842
|
+
|
|
335
843
|
rec.insertWebcamFeed(webcam_div);
|
|
336
844
|
|
|
337
845
|
// Mock the return values for the video element's offsetHeight/offsetWidth, which are used to set the container size
|
|
@@ -342,7 +850,8 @@ test("Webcam feed container maintains size with recorder.stop(true)", async () =
|
|
|
342
850
|
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
343
851
|
.mockImplementation(() => 300);
|
|
344
852
|
|
|
345
|
-
|
|
853
|
+
const { stopped } = rec.stop({ maintain_container_size: true });
|
|
854
|
+
await stopped;
|
|
346
855
|
|
|
347
856
|
// Container div's dimensions should match the video element dimensions
|
|
348
857
|
expect(
|
|
@@ -369,9 +878,13 @@ test("Webcam feed container size is not maintained with recorder.stop(false)", a
|
|
|
369
878
|
|
|
370
879
|
const jsPsych = initJsPsych();
|
|
371
880
|
const rec = new Recorder(jsPsych);
|
|
372
|
-
const stopPromise = Promise.resolve();
|
|
881
|
+
const stopPromise = Promise.resolve("url");
|
|
373
882
|
|
|
883
|
+
// manual mocks
|
|
884
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
885
|
+
rec["filename"] = "fakename";
|
|
374
886
|
rec["stopPromise"] = stopPromise;
|
|
887
|
+
|
|
375
888
|
rec.insertWebcamFeed(webcam_div);
|
|
376
889
|
|
|
377
890
|
// Mock the return values for the video element offsetHeight/offsetWidth, which are used to set the container size
|
|
@@ -382,7 +895,8 @@ test("Webcam feed container size is not maintained with recorder.stop(false)", a
|
|
|
382
895
|
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
383
896
|
.mockImplementation(() => 300);
|
|
384
897
|
|
|
385
|
-
|
|
898
|
+
const { stopped } = rec.stop();
|
|
899
|
+
await stopped;
|
|
386
900
|
|
|
387
901
|
// Container div's dimensions should not be set
|
|
388
902
|
expect(
|
|
@@ -399,7 +913,7 @@ test("Webcam feed container size is not maintained with recorder.stop(false)", a
|
|
|
399
913
|
jest.restoreAllMocks();
|
|
400
914
|
});
|
|
401
915
|
|
|
402
|
-
test("Webcam feed container size is not maintained with recorder.stop()",
|
|
916
|
+
test("Webcam feed container size is not maintained with recorder.stop()", () => {
|
|
403
917
|
// Add webcam container to document body.
|
|
404
918
|
const webcam_container_id = "webcam-container";
|
|
405
919
|
document.body.innerHTML = `<div id="${webcam_container_id}"></div>`;
|
|
@@ -409,9 +923,13 @@ test("Webcam feed container size is not maintained with recorder.stop()", async
|
|
|
409
923
|
|
|
410
924
|
const jsPsych = initJsPsych();
|
|
411
925
|
const rec = new Recorder(jsPsych);
|
|
412
|
-
const stopPromise = Promise.resolve();
|
|
926
|
+
const stopPromise = Promise.resolve("url");
|
|
413
927
|
|
|
928
|
+
// manual mocks
|
|
929
|
+
rec["_s3"] = new Data.LookitS3("some key");
|
|
930
|
+
rec["filename"] = "fakename";
|
|
414
931
|
rec["stopPromise"] = stopPromise;
|
|
932
|
+
|
|
415
933
|
rec.insertWebcamFeed(webcam_div);
|
|
416
934
|
|
|
417
935
|
// Mock the return values for the video element offsetHeight/offsetWidth, which are used to set the container size
|
|
@@ -422,7 +940,7 @@ test("Webcam feed container size is not maintained with recorder.stop()", async
|
|
|
422
940
|
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
423
941
|
.mockImplementation(() => 300);
|
|
424
942
|
|
|
425
|
-
|
|
943
|
+
rec.stop();
|
|
426
944
|
|
|
427
945
|
// Container div's dimensions should not be set
|
|
428
946
|
expect(
|
|
@@ -477,12 +995,10 @@ test("Recorder initializeRecorder", () => {
|
|
|
477
995
|
|
|
478
996
|
test("Recorder download", () => {
|
|
479
997
|
const rec = new Recorder(initJsPsych());
|
|
480
|
-
rec["url"] = "some url";
|
|
481
|
-
rec["filename"] = "some filename";
|
|
482
998
|
const download = rec["download"];
|
|
483
999
|
const click = jest.spyOn(HTMLAnchorElement.prototype, "click");
|
|
484
1000
|
|
|
485
|
-
download();
|
|
1001
|
+
download("some filename", "some url");
|
|
486
1002
|
|
|
487
1003
|
expect(click).toHaveBeenCalledTimes(1);
|
|
488
1004
|
});
|
|
@@ -497,7 +1013,7 @@ test("Recorder s3 get error when undefined", () => {
|
|
|
497
1013
|
test("Recorder reset error when stream active", () => {
|
|
498
1014
|
const jsPsych = initJsPsych();
|
|
499
1015
|
const rec = new Recorder(jsPsych);
|
|
500
|
-
expect(
|
|
1016
|
+
expect(rec.reset).toThrow(StreamActiveOnResetError);
|
|
501
1017
|
});
|
|
502
1018
|
|
|
503
1019
|
test("Recorder reset", () => {
|
|
@@ -592,7 +1108,7 @@ test("Record initialize error inactive stream", () => {
|
|
|
592
1108
|
.fn()
|
|
593
1109
|
.mockReturnValue({ stream: { active: false } });
|
|
594
1110
|
|
|
595
|
-
expect(
|
|
1111
|
+
expect(initializeCheck).toThrow(StreamInactiveInitializeError);
|
|
596
1112
|
|
|
597
1113
|
jsPsych.pluginAPI.getCameraRecorder = getCameraRecorder;
|
|
598
1114
|
});
|
|
@@ -604,7 +1120,7 @@ test("Record initialize error inactive stream", () => {
|
|
|
604
1120
|
|
|
605
1121
|
rec["blobs"] = ["some stream data" as unknown as Blob];
|
|
606
1122
|
|
|
607
|
-
expect(
|
|
1123
|
+
expect(initializeCheck).toThrow(StreamDataInitializeError);
|
|
608
1124
|
});
|
|
609
1125
|
|
|
610
1126
|
test("Recorder insert record Feed with height/width", () => {
|
|
@@ -620,7 +1136,7 @@ test("Recorder insert record Feed with height/width", () => {
|
|
|
620
1136
|
};
|
|
621
1137
|
|
|
622
1138
|
jest.spyOn(Handlebars, "compile");
|
|
623
|
-
jest.spyOn(rec, "insertVideoFeed");
|
|
1139
|
+
jest.spyOn(rec as any, "insertVideoFeed");
|
|
624
1140
|
|
|
625
1141
|
rec.insertRecordFeed(display);
|
|
626
1142
|
|
|
@@ -757,3 +1273,47 @@ test("New recorder uses a default mime type if none is set already", () => {
|
|
|
757
1273
|
|
|
758
1274
|
jsPsych.pluginAPI.initializeCameraRecorder = originalInitializeCameraRecorder;
|
|
759
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
|
+
});
|