@lookit/record 3.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -6
- package/dist/index.browser.js +136 -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 +135 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +129 -12
- package/dist/index.js +135 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/consentVideo.spec.ts +38 -12
- package/src/consentVideo.ts +80 -19
- package/src/errors.ts +7 -27
- package/src/index.spec.ts +387 -12
- package/src/recorder.spec.ts +170 -6
- package/src/recorder.ts +37 -5
- package/src/start.ts +7 -1
- package/src/stop.ts +41 -7
- package/src/trial.ts +97 -11
package/src/index.spec.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LookitWindow } from "@lookit/data/dist/types";
|
|
2
|
+
import chsTemplates from "@lookit/templates";
|
|
2
3
|
import { initJsPsych, PluginInfo, TrialType } from "jspsych";
|
|
3
4
|
import { ExistingRecordingError, NoSessionRecordingError } from "./errors";
|
|
4
5
|
import Rec from "./index";
|
|
@@ -6,15 +7,22 @@ import Recorder from "./recorder";
|
|
|
6
7
|
|
|
7
8
|
declare const window: LookitWindow;
|
|
8
9
|
|
|
10
|
+
let global_display_el: HTMLDivElement;
|
|
11
|
+
|
|
9
12
|
jest.mock("./recorder");
|
|
10
13
|
jest.mock("@lookit/data");
|
|
11
14
|
jest.mock("jspsych", () => ({
|
|
12
15
|
...jest.requireActual("jspsych"),
|
|
13
|
-
initJsPsych: jest.fn().
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
16
|
+
initJsPsych: jest.fn().mockImplementation(() => {
|
|
17
|
+
// create a new display element for each jsPsych instance
|
|
18
|
+
global_display_el = document.createElement("div");
|
|
19
|
+
return {
|
|
20
|
+
finishTrial: jest.fn().mockImplementation(),
|
|
21
|
+
getCurrentTrial: jest
|
|
22
|
+
.fn()
|
|
23
|
+
.mockReturnValue({ type: { info: { name: "test-type" } } }),
|
|
24
|
+
getDisplayElement: jest.fn(() => global_display_el),
|
|
25
|
+
};
|
|
18
26
|
}),
|
|
19
27
|
}));
|
|
20
28
|
|
|
@@ -24,10 +32,10 @@ jest.mock("jspsych", () => ({
|
|
|
24
32
|
* @param chs - Contents of chs storage.
|
|
25
33
|
*/
|
|
26
34
|
const setCHSValue = (chs = {}) => {
|
|
27
|
-
Object.defineProperty(
|
|
28
|
-
value:
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
Object.defineProperty(window, "chs", {
|
|
36
|
+
value: chs,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
31
39
|
});
|
|
32
40
|
};
|
|
33
41
|
|
|
@@ -36,6 +44,7 @@ beforeEach(() => {
|
|
|
36
44
|
});
|
|
37
45
|
|
|
38
46
|
afterEach(() => {
|
|
47
|
+
jest.restoreAllMocks();
|
|
39
48
|
jest.clearAllMocks();
|
|
40
49
|
});
|
|
41
50
|
|
|
@@ -57,15 +66,223 @@ test("Trial recording", () => {
|
|
|
57
66
|
expect(getCurrentPluginNameSpy).toHaveBeenCalledTimes(1);
|
|
58
67
|
});
|
|
59
68
|
|
|
60
|
-
test("Trial recording's initialize
|
|
69
|
+
test("Trial recording's initialize with no parameters", async () => {
|
|
61
70
|
const jsPsych = initJsPsych();
|
|
62
71
|
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
63
72
|
|
|
64
73
|
expect(await trialRec.initialize()).toBeUndefined();
|
|
74
|
+
expect(trialRec["uploadMsg"]).toBeNull;
|
|
75
|
+
expect(trialRec["locale"]).toBe("en-us");
|
|
76
|
+
expect(Recorder).toHaveBeenCalledTimes(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("Trial recording's initialize with locale parameter", async () => {
|
|
80
|
+
const jsPsych = initJsPsych();
|
|
81
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
82
|
+
|
|
83
|
+
expect(await trialRec.initialize({ locale: "fr" })).toBeUndefined();
|
|
84
|
+
expect(trialRec["locale"]).toBe("fr");
|
|
85
|
+
expect(trialRec["uploadMsg"]).toBeNull;
|
|
86
|
+
expect(Recorder).toHaveBeenCalledTimes(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("Trial recording's initialize with wait_for_upload_message parameter", async () => {
|
|
90
|
+
const jsPsych = initJsPsych();
|
|
91
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
92
|
+
|
|
93
|
+
expect(
|
|
94
|
+
await trialRec.initialize({ wait_for_upload_message: "Please wait..." }),
|
|
95
|
+
).toBeUndefined();
|
|
96
|
+
expect(trialRec["uploadMsg"]).toBe("Please wait...");
|
|
97
|
+
expect(trialRec["locale"]).toBe("en-us");
|
|
98
|
+
expect(Recorder).toHaveBeenCalledTimes(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("Trial recording start with locale parameter", async () => {
|
|
102
|
+
const jsPsych = initJsPsych();
|
|
103
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
104
|
+
|
|
105
|
+
await trialRec.initialize();
|
|
106
|
+
|
|
107
|
+
expect(trialRec["uploadMsg"]).toBeNull;
|
|
108
|
+
expect(trialRec["locale"]).toBe("en-us");
|
|
109
|
+
expect(Recorder).toHaveBeenCalledTimes(0);
|
|
110
|
+
|
|
111
|
+
trialRec.on_start({ locale: "fr" });
|
|
112
|
+
|
|
113
|
+
expect(trialRec["uploadMsg"]).toBeNull;
|
|
114
|
+
expect(trialRec["locale"]).toBe("fr");
|
|
115
|
+
expect(Recorder).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("Trial recording start with wait_for_upload_message parameter", async () => {
|
|
119
|
+
const jsPsych = initJsPsych();
|
|
120
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
121
|
+
|
|
122
|
+
await trialRec.initialize();
|
|
123
|
+
|
|
124
|
+
expect(trialRec["uploadMsg"]).toBeNull;
|
|
125
|
+
expect(trialRec["locale"]).toBe("en-us");
|
|
65
126
|
expect(Recorder).toHaveBeenCalledTimes(0);
|
|
127
|
+
|
|
128
|
+
trialRec.on_start({ wait_for_upload_message: "Please wait..." });
|
|
129
|
+
|
|
130
|
+
expect(trialRec["uploadMsg"]).toBe("Please wait...");
|
|
131
|
+
expect(trialRec["locale"]).toBe("en-us");
|
|
132
|
+
expect(Recorder).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("Trial recording stop/finish with default uploading msg in English", async () => {
|
|
136
|
+
// control the recorder stop promise so that we can inspect the display before it resolves
|
|
137
|
+
let resolveStop!: () => void;
|
|
138
|
+
const stopPromise = new Promise<void>((res) => (resolveStop = res));
|
|
139
|
+
|
|
140
|
+
jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
|
|
141
|
+
|
|
142
|
+
const jsPsych = initJsPsych();
|
|
143
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
144
|
+
|
|
145
|
+
const params = {
|
|
146
|
+
locale: "en-us",
|
|
147
|
+
wait_for_upload_message: null,
|
|
148
|
+
};
|
|
149
|
+
await trialRec.initialize(params);
|
|
150
|
+
trialRec.on_start();
|
|
151
|
+
trialRec.on_load();
|
|
152
|
+
|
|
153
|
+
// call on_finish but don't await so that we can inspect before it resolves
|
|
154
|
+
trialRec.on_finish();
|
|
155
|
+
|
|
156
|
+
expect(global_display_el.innerHTML).toBe(
|
|
157
|
+
chsTemplates.uploadingVideo({
|
|
158
|
+
type: jsPsych.getCurrentTrial().type,
|
|
159
|
+
locale: params.locale,
|
|
160
|
+
} as TrialType<PluginInfo>),
|
|
161
|
+
);
|
|
162
|
+
expect(global_display_el.innerHTML).toBe(
|
|
163
|
+
"<div>uploading video, please wait...</div>",
|
|
164
|
+
);
|
|
165
|
+
//expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
|
|
166
|
+
|
|
167
|
+
// resolve the stop promise
|
|
168
|
+
resolveStop();
|
|
169
|
+
await stopPromise;
|
|
170
|
+
await Promise.resolve();
|
|
171
|
+
|
|
172
|
+
// check the display cleanup
|
|
173
|
+
expect(global_display_el.innerHTML).toBe("");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("Trial recording stop/finish with different locale should display default uploading msg in specified language", async () => {
|
|
177
|
+
// control the recorder stop promise so that we can inspect the display before it resolves
|
|
178
|
+
let resolveStop!: () => void;
|
|
179
|
+
const stopPromise = new Promise<void>((res) => (resolveStop = res));
|
|
180
|
+
|
|
181
|
+
jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
|
|
182
|
+
|
|
183
|
+
const jsPsych = initJsPsych();
|
|
184
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
185
|
+
|
|
186
|
+
const params = {
|
|
187
|
+
locale: "fr",
|
|
188
|
+
wait_for_upload_message: null,
|
|
189
|
+
};
|
|
190
|
+
await trialRec.initialize(params);
|
|
191
|
+
trialRec.on_start();
|
|
192
|
+
trialRec.on_load();
|
|
193
|
+
|
|
194
|
+
// call on_finish but don't await so that we can inspect before it resolves
|
|
195
|
+
trialRec.on_finish();
|
|
196
|
+
|
|
197
|
+
expect(global_display_el.innerHTML).toBe(
|
|
198
|
+
chsTemplates.uploadingVideo({
|
|
199
|
+
type: jsPsych.getCurrentTrial().type,
|
|
200
|
+
locale: params.locale,
|
|
201
|
+
} as TrialType<PluginInfo>),
|
|
202
|
+
);
|
|
203
|
+
expect(global_display_el.innerHTML).toBe(
|
|
204
|
+
"<div>téléchargement video en cours, veuillez attendre...</div>",
|
|
205
|
+
);
|
|
206
|
+
//expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
|
|
207
|
+
|
|
208
|
+
// resolve the stop promise
|
|
209
|
+
resolveStop();
|
|
210
|
+
await stopPromise;
|
|
211
|
+
await Promise.resolve();
|
|
212
|
+
|
|
213
|
+
// check the display cleanup
|
|
214
|
+
expect(global_display_el.innerHTML).toBe("");
|
|
66
215
|
});
|
|
67
216
|
|
|
68
|
-
test("
|
|
217
|
+
test("Trial recording stop/finish with custom uploading message", async () => {
|
|
218
|
+
// control the recorder stop promise so that we can inspect the display before it resolves
|
|
219
|
+
let resolveStop!: () => void;
|
|
220
|
+
const stopPromise = new Promise<void>((res) => (resolveStop = res));
|
|
221
|
+
|
|
222
|
+
jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
|
|
223
|
+
|
|
224
|
+
const jsPsych = initJsPsych();
|
|
225
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
226
|
+
|
|
227
|
+
const params = {
|
|
228
|
+
wait_for_upload_message: "Wait!",
|
|
229
|
+
};
|
|
230
|
+
await trialRec.initialize(params);
|
|
231
|
+
trialRec.on_start();
|
|
232
|
+
trialRec.on_load();
|
|
233
|
+
|
|
234
|
+
// call on_finish but don't await so that we can inspect before it resolves
|
|
235
|
+
trialRec.on_finish();
|
|
236
|
+
|
|
237
|
+
expect(global_display_el.innerHTML).toBe("Wait!");
|
|
238
|
+
//expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
|
|
239
|
+
|
|
240
|
+
// resolve the stop promise
|
|
241
|
+
resolveStop();
|
|
242
|
+
await stopPromise;
|
|
243
|
+
await Promise.resolve();
|
|
244
|
+
|
|
245
|
+
// check the display cleanup
|
|
246
|
+
expect(global_display_el.innerHTML).toBe("");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("Trial recording rejection path (failure during upload)", async () => {
|
|
250
|
+
// Create a controlled promise and capture the reject function
|
|
251
|
+
let rejectStop!: (err: unknown) => void;
|
|
252
|
+
const stopPromise = new Promise<void>((_, reject) => {
|
|
253
|
+
rejectStop = reject;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
|
|
257
|
+
|
|
258
|
+
const jsPsych = initJsPsych();
|
|
259
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
260
|
+
|
|
261
|
+
await trialRec.initialize();
|
|
262
|
+
trialRec.on_start();
|
|
263
|
+
trialRec.on_load();
|
|
264
|
+
|
|
265
|
+
// call on_finish but don't await so that we can inspect before it resolves
|
|
266
|
+
trialRec.on_finish();
|
|
267
|
+
|
|
268
|
+
// Should show initial wait for upload message
|
|
269
|
+
expect(global_display_el.innerHTML).toBe(
|
|
270
|
+
"<div>uploading video, please wait...</div>",
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Reject stop
|
|
274
|
+
rejectStop(new Error("upload failed"));
|
|
275
|
+
|
|
276
|
+
// Wait for plugin's `.catch()` handler to run
|
|
277
|
+
await Promise.resolve();
|
|
278
|
+
|
|
279
|
+
// TO DO: modify the trial extension code to display translated error msg and/or researcher contact info
|
|
280
|
+
expect(global_display_el.innerHTML).toBe(
|
|
281
|
+
"<div>uploading video, please wait...</div>",
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("Start session recording", async () => {
|
|
69
286
|
const mockRecStart = jest.spyOn(Recorder.prototype, "start");
|
|
70
287
|
const jsPsych = initJsPsych();
|
|
71
288
|
const startRec = new Rec.StartRecordPlugin(jsPsych);
|
|
@@ -81,7 +298,7 @@ test("Start Recording", async () => {
|
|
|
81
298
|
}).toThrow(ExistingRecordingError);
|
|
82
299
|
});
|
|
83
300
|
|
|
84
|
-
test("Stop
|
|
301
|
+
test("Stop session recording", async () => {
|
|
85
302
|
const mockRecStop = jest.spyOn(Recorder.prototype, "stop");
|
|
86
303
|
const jsPsych = initJsPsych();
|
|
87
304
|
|
|
@@ -112,3 +329,161 @@ test("Stop Recording", async () => {
|
|
|
112
329
|
NoSessionRecordingError,
|
|
113
330
|
);
|
|
114
331
|
});
|
|
332
|
+
|
|
333
|
+
test("Stop session recording should display default uploading msg in English", async () => {
|
|
334
|
+
// control the recorder stop promise so that we can inspect the display before it resolves
|
|
335
|
+
let resolveStop!: () => void;
|
|
336
|
+
const stopPromise = new Promise<void>((res) => (resolveStop = res));
|
|
337
|
+
|
|
338
|
+
jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
|
|
339
|
+
|
|
340
|
+
const jsPsych = initJsPsych();
|
|
341
|
+
|
|
342
|
+
setCHSValue({
|
|
343
|
+
sessionRecorder: new Recorder(jsPsych),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
|
|
347
|
+
const display_element = document.createElement("div");
|
|
348
|
+
|
|
349
|
+
const trial = {
|
|
350
|
+
type: Rec.StopRecordPlugin.info.name,
|
|
351
|
+
locale: "en-us",
|
|
352
|
+
wait_for_upload_message: null,
|
|
353
|
+
} as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
|
|
354
|
+
|
|
355
|
+
// call trial but don't await so that we can inspect before it resolves
|
|
356
|
+
stop_rec_plugin.trial(display_element, trial);
|
|
357
|
+
|
|
358
|
+
expect(display_element.innerHTML).toBe(chsTemplates.uploadingVideo(trial));
|
|
359
|
+
expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
|
|
360
|
+
|
|
361
|
+
// resolve the stop promise
|
|
362
|
+
resolveStop();
|
|
363
|
+
await stopPromise;
|
|
364
|
+
await Promise.resolve();
|
|
365
|
+
|
|
366
|
+
// check the cleanup tasks after the trial method has resolved
|
|
367
|
+
expect(display_element.innerHTML).toBe("");
|
|
368
|
+
expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
|
|
369
|
+
expect(window.chs.sessionRecorder).toBeNull();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("Stop session recording with different locale should display default uploading msg in specified language", async () => {
|
|
373
|
+
// control the recorder stop promise so that we can inspect the display before it resolves
|
|
374
|
+
let resolveStop!: () => void;
|
|
375
|
+
const stopPromise = new Promise<void>((res) => (resolveStop = res));
|
|
376
|
+
|
|
377
|
+
jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
|
|
378
|
+
|
|
379
|
+
const jsPsych = initJsPsych();
|
|
380
|
+
|
|
381
|
+
setCHSValue({
|
|
382
|
+
sessionRecorder: new Recorder(jsPsych),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
|
|
386
|
+
const display_element = document.createElement("div");
|
|
387
|
+
|
|
388
|
+
// set locale to fr
|
|
389
|
+
const trial = {
|
|
390
|
+
type: Rec.StopRecordPlugin.info.name,
|
|
391
|
+
locale: "fr",
|
|
392
|
+
wait_for_upload_message: null,
|
|
393
|
+
} as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
|
|
394
|
+
|
|
395
|
+
// call trial but don't await so that we can inspect before it resolves
|
|
396
|
+
stop_rec_plugin.trial(display_element, trial);
|
|
397
|
+
|
|
398
|
+
const fr_uploading_msg = chsTemplates.uploadingVideo(trial);
|
|
399
|
+
|
|
400
|
+
// check that fr translation is used
|
|
401
|
+
expect(fr_uploading_msg).toBe(
|
|
402
|
+
"<div>téléchargement video en cours, veuillez attendre...</div>",
|
|
403
|
+
);
|
|
404
|
+
expect(display_element.innerHTML).toBe(fr_uploading_msg);
|
|
405
|
+
expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
|
|
406
|
+
|
|
407
|
+
// resolve the stop promise
|
|
408
|
+
resolveStop();
|
|
409
|
+
await stopPromise;
|
|
410
|
+
await Promise.resolve();
|
|
411
|
+
|
|
412
|
+
// check the cleanup tasks after the trial method has resolved
|
|
413
|
+
expect(display_element.innerHTML).toBe("");
|
|
414
|
+
expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
|
|
415
|
+
expect(window.chs.sessionRecorder).toBeNull();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("Stop session recording with custom uploading message", async () => {
|
|
419
|
+
// control the recorder stop promise so that we can inspect the display before it resolves
|
|
420
|
+
let resolveStop!: () => void;
|
|
421
|
+
const stopPromise = new Promise<void>((res) => (resolveStop = res));
|
|
422
|
+
|
|
423
|
+
jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
|
|
424
|
+
|
|
425
|
+
const jsPsych = initJsPsych();
|
|
426
|
+
setCHSValue({ sessionRecorder: new Recorder(jsPsych) });
|
|
427
|
+
|
|
428
|
+
const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
|
|
429
|
+
const display_element = document.createElement("div");
|
|
430
|
+
|
|
431
|
+
const trial = {
|
|
432
|
+
type: Rec.StopRecordPlugin.info.name,
|
|
433
|
+
locale: "en-us",
|
|
434
|
+
wait_for_upload_message: "<p>Custom message…</p>",
|
|
435
|
+
} as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
|
|
436
|
+
|
|
437
|
+
stop_rec_plugin.trial(display_element, trial);
|
|
438
|
+
|
|
439
|
+
// check display before stop is resolved
|
|
440
|
+
expect(display_element.innerHTML).toBe("<p>Custom message…</p>");
|
|
441
|
+
|
|
442
|
+
resolveStop();
|
|
443
|
+
await stopPromise;
|
|
444
|
+
await Promise.resolve();
|
|
445
|
+
|
|
446
|
+
// check the cleanup tasks after the trial method has resolved
|
|
447
|
+
expect(display_element.innerHTML).toBe("");
|
|
448
|
+
expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
|
|
449
|
+
expect(window.chs.sessionRecorder).toBeNull();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("Stop recording rejection path (failure during upload)", async () => {
|
|
453
|
+
// Create a controlled promise and capture the reject function
|
|
454
|
+
let rejectStop!: (err: unknown) => void;
|
|
455
|
+
const stopPromise = new Promise<void>((_, reject) => {
|
|
456
|
+
rejectStop = reject;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
|
|
460
|
+
|
|
461
|
+
const jsPsych = initJsPsych();
|
|
462
|
+
setCHSValue({ sessionRecorder: new Recorder(jsPsych) });
|
|
463
|
+
|
|
464
|
+
const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
|
|
465
|
+
const display_element = document.createElement("div");
|
|
466
|
+
|
|
467
|
+
const trial = {
|
|
468
|
+
type: Rec.StopRecordPlugin.info.name,
|
|
469
|
+
locale: "en-us",
|
|
470
|
+
wait_for_upload_message: "Wait…",
|
|
471
|
+
} as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
|
|
472
|
+
|
|
473
|
+
stop_rec_plugin.trial(display_element, trial);
|
|
474
|
+
|
|
475
|
+
// Should show initial wait for upload message
|
|
476
|
+
expect(display_element.innerHTML).toBe("Wait…");
|
|
477
|
+
|
|
478
|
+
// Reject stop
|
|
479
|
+
rejectStop(new Error("upload failed"));
|
|
480
|
+
|
|
481
|
+
// Wait for plugin's `.catch()` handler to run
|
|
482
|
+
await Promise.resolve();
|
|
483
|
+
|
|
484
|
+
// Trial doesn't end and the cleanup tasks don't run.
|
|
485
|
+
// TO DO: modify the plugin code to display translated error msg and/or researcher contact info
|
|
486
|
+
expect(display_element.innerHTML).toBe("Wait…");
|
|
487
|
+
expect(jsPsych.finishTrial).not.toHaveBeenCalled();
|
|
488
|
+
expect(window.chs.sessionRecorder).not.toBeNull();
|
|
489
|
+
});
|
package/src/recorder.spec.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Data from "@lookit/data";
|
|
2
2
|
import { LookitWindow } from "@lookit/data/dist/types";
|
|
3
3
|
import Handlebars from "handlebars";
|
|
4
|
-
import { initJsPsych } from "jspsych";
|
|
4
|
+
import { initJsPsych, JsPsych } from "jspsych";
|
|
5
5
|
import playbackFeed from "../hbs/playback-feed.hbs";
|
|
6
6
|
import recordFeed from "../hbs/record-feed.hbs";
|
|
7
7
|
import webcamFeed from "../hbs/webcam-feed.hbs";
|
|
@@ -123,6 +123,7 @@ test("Recorder no stop promise", () => {
|
|
|
123
123
|
|
|
124
124
|
expect(async () => await rec.stop()).rejects.toThrow(NoStopPromiseError);
|
|
125
125
|
});
|
|
126
|
+
|
|
126
127
|
test("Recorder initialize error", () => {
|
|
127
128
|
const jsPsych = initJsPsych();
|
|
128
129
|
const rec = new Recorder(jsPsych);
|
|
@@ -142,29 +143,73 @@ test("Recorder initialize error", () => {
|
|
|
142
143
|
});
|
|
143
144
|
|
|
144
145
|
test("Recorder handleStop", async () => {
|
|
145
|
-
|
|
146
|
+
// Define a custom mockStream so that we can dynamically modify active value
|
|
147
|
+
const mockStream = {
|
|
148
|
+
active: true,
|
|
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
|
+
};
|
|
161
|
+
|
|
162
|
+
// Minimal fake jsPsych object to pass to the Recorder constructor
|
|
163
|
+
const jsPsych = {
|
|
164
|
+
pluginAPI: {
|
|
165
|
+
getCameraRecorder: jest.fn().mockReturnValue(mockRecorder),
|
|
166
|
+
initializeCameraRecorder: jest.fn().mockReturnValue(mockRecorder),
|
|
167
|
+
},
|
|
168
|
+
data: {
|
|
169
|
+
getLastTrialData: jest.fn().mockReturnValue({
|
|
170
|
+
values: jest.fn().mockReturnValue([]),
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
} as unknown as JsPsych;
|
|
174
|
+
|
|
175
|
+
const rec = new Recorder(jsPsych);
|
|
176
|
+
|
|
146
177
|
const download = jest.fn();
|
|
147
178
|
const resolve = jest.fn();
|
|
148
|
-
|
|
179
|
+
// This allows us to keep strict typing and avoid use of 'any'
|
|
180
|
+
const resetSpy = jest.spyOn(rec, "reset" satisfies keyof typeof rec);
|
|
149
181
|
|
|
150
|
-
//
|
|
182
|
+
// Manual mock
|
|
151
183
|
rec["download"] = download;
|
|
152
184
|
rec["blobs"] = ["some recorded data" as unknown as Blob];
|
|
153
185
|
URL.createObjectURL = jest.fn();
|
|
154
186
|
|
|
155
|
-
//
|
|
187
|
+
// Download the file locally
|
|
156
188
|
rec["localDownload"] = true;
|
|
157
189
|
|
|
190
|
+
// Stream cannot be active when handleStop/reset is called
|
|
191
|
+
mockStream.active = false;
|
|
192
|
+
|
|
193
|
+
const handleStop = rec["handleStop"](resolve);
|
|
194
|
+
|
|
158
195
|
await handleStop();
|
|
159
196
|
|
|
197
|
+
expect(download).toHaveBeenCalledTimes(1);
|
|
198
|
+
expect(resetSpy).toHaveBeenCalledTimes(1);
|
|
199
|
+
|
|
160
200
|
// Upload the file to s3
|
|
161
201
|
rec["localDownload"] = false;
|
|
162
202
|
rec["_s3"] = new Data.LookitS3("some key");
|
|
163
203
|
|
|
204
|
+
// The first 'handleStop' resets the recorder, which resets blobs to [], so we need to fake the blob data again.
|
|
205
|
+
rec["blobs"] = ["some recorded data" as unknown as Blob];
|
|
206
|
+
|
|
164
207
|
await handleStop();
|
|
165
208
|
|
|
166
|
-
expect(download).toHaveBeenCalledTimes(1);
|
|
167
209
|
expect(Data.LookitS3.prototype.completeUpload).toHaveBeenCalledTimes(1);
|
|
210
|
+
expect(resetSpy).toHaveBeenCalledTimes(2);
|
|
211
|
+
|
|
212
|
+
resetSpy.mockRestore();
|
|
168
213
|
});
|
|
169
214
|
|
|
170
215
|
test("Recorder handleStop error with no url", () => {
|
|
@@ -274,6 +319,125 @@ test("Webcam feed is removed when stream access stops", async () => {
|
|
|
274
319
|
document.body.innerHTML = "";
|
|
275
320
|
});
|
|
276
321
|
|
|
322
|
+
test("Webcam feed container maintains size with recorder.stop(true)", async () => {
|
|
323
|
+
// Add webcam container to document body.
|
|
324
|
+
const webcam_container_id = "webcam-container";
|
|
325
|
+
document.body.innerHTML = `<div id="${webcam_container_id}"></div>`;
|
|
326
|
+
const webcam_div = document.getElementById(
|
|
327
|
+
webcam_container_id,
|
|
328
|
+
) as HTMLDivElement;
|
|
329
|
+
|
|
330
|
+
const jsPsych = initJsPsych();
|
|
331
|
+
const rec = new Recorder(jsPsych);
|
|
332
|
+
const stopPromise = Promise.resolve();
|
|
333
|
+
|
|
334
|
+
rec["stopPromise"] = stopPromise;
|
|
335
|
+
rec.insertWebcamFeed(webcam_div);
|
|
336
|
+
|
|
337
|
+
// Mock the return values for the video element's offsetHeight/offsetWidth, which are used to set the container size
|
|
338
|
+
jest
|
|
339
|
+
.spyOn(document.getElementsByTagName("video")[0], "offsetWidth", "get")
|
|
340
|
+
.mockImplementation(() => 400);
|
|
341
|
+
jest
|
|
342
|
+
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
343
|
+
.mockImplementation(() => 300);
|
|
344
|
+
|
|
345
|
+
await rec.stop(true);
|
|
346
|
+
|
|
347
|
+
// Container div's dimensions should match the video element dimensions
|
|
348
|
+
expect(
|
|
349
|
+
(document.getElementById(webcam_container_id) as HTMLDivElement).style
|
|
350
|
+
.width,
|
|
351
|
+
).toBe("400px");
|
|
352
|
+
expect(
|
|
353
|
+
(document.getElementById(webcam_container_id) as HTMLDivElement).style
|
|
354
|
+
.height,
|
|
355
|
+
).toBe("300px");
|
|
356
|
+
|
|
357
|
+
document.body.innerHTML = "";
|
|
358
|
+
// restore the offsetWidth/offsetHeight getters
|
|
359
|
+
jest.restoreAllMocks();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("Webcam feed container size is not maintained with recorder.stop(false)", async () => {
|
|
363
|
+
// Add webcam container to document body.
|
|
364
|
+
const webcam_container_id = "webcam-container";
|
|
365
|
+
document.body.innerHTML = `<div id="${webcam_container_id}"></div>`;
|
|
366
|
+
const webcam_div = document.getElementById(
|
|
367
|
+
webcam_container_id,
|
|
368
|
+
) as HTMLDivElement;
|
|
369
|
+
|
|
370
|
+
const jsPsych = initJsPsych();
|
|
371
|
+
const rec = new Recorder(jsPsych);
|
|
372
|
+
const stopPromise = Promise.resolve();
|
|
373
|
+
|
|
374
|
+
rec["stopPromise"] = stopPromise;
|
|
375
|
+
rec.insertWebcamFeed(webcam_div);
|
|
376
|
+
|
|
377
|
+
// Mock the return values for the video element offsetHeight/offsetWidth, which are used to set the container size
|
|
378
|
+
jest
|
|
379
|
+
.spyOn(document.getElementsByTagName("video")[0], "offsetWidth", "get")
|
|
380
|
+
.mockImplementation(() => 400);
|
|
381
|
+
jest
|
|
382
|
+
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
383
|
+
.mockImplementation(() => 300);
|
|
384
|
+
|
|
385
|
+
await rec.stop(false);
|
|
386
|
+
|
|
387
|
+
// Container div's dimensions should not be set
|
|
388
|
+
expect(
|
|
389
|
+
(document.getElementById(webcam_container_id) as HTMLDivElement).style
|
|
390
|
+
.width,
|
|
391
|
+
).toBe("");
|
|
392
|
+
expect(
|
|
393
|
+
(document.getElementById(webcam_container_id) as HTMLDivElement).style
|
|
394
|
+
.height,
|
|
395
|
+
).toBe("");
|
|
396
|
+
|
|
397
|
+
document.body.innerHTML = "";
|
|
398
|
+
// restore the offsetWidth/offsetHeight getters
|
|
399
|
+
jest.restoreAllMocks();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("Webcam feed container size is not maintained with recorder.stop()", async () => {
|
|
403
|
+
// Add webcam container to document body.
|
|
404
|
+
const webcam_container_id = "webcam-container";
|
|
405
|
+
document.body.innerHTML = `<div id="${webcam_container_id}"></div>`;
|
|
406
|
+
const webcam_div = document.getElementById(
|
|
407
|
+
webcam_container_id,
|
|
408
|
+
) as HTMLDivElement;
|
|
409
|
+
|
|
410
|
+
const jsPsych = initJsPsych();
|
|
411
|
+
const rec = new Recorder(jsPsych);
|
|
412
|
+
const stopPromise = Promise.resolve();
|
|
413
|
+
|
|
414
|
+
rec["stopPromise"] = stopPromise;
|
|
415
|
+
rec.insertWebcamFeed(webcam_div);
|
|
416
|
+
|
|
417
|
+
// Mock the return values for the video element offsetHeight/offsetWidth, which are used to set the container size
|
|
418
|
+
jest
|
|
419
|
+
.spyOn(document.getElementsByTagName("video")[0], "offsetWidth", "get")
|
|
420
|
+
.mockImplementation(() => 400);
|
|
421
|
+
jest
|
|
422
|
+
.spyOn(document.getElementsByTagName("video")[0], "offsetHeight", "get")
|
|
423
|
+
.mockImplementation(() => 300);
|
|
424
|
+
|
|
425
|
+
await rec.stop();
|
|
426
|
+
|
|
427
|
+
// Container div's dimensions should not be set
|
|
428
|
+
expect(
|
|
429
|
+
(document.getElementById(webcam_container_id) as HTMLDivElement).style
|
|
430
|
+
.width,
|
|
431
|
+
).toBe("");
|
|
432
|
+
expect(
|
|
433
|
+
(document.getElementById(webcam_container_id) as HTMLDivElement).style
|
|
434
|
+
.height,
|
|
435
|
+
).toBe("");
|
|
436
|
+
|
|
437
|
+
document.body.innerHTML = "";
|
|
438
|
+
jest.restoreAllMocks();
|
|
439
|
+
});
|
|
440
|
+
|
|
277
441
|
test("Recorder initializeRecorder", () => {
|
|
278
442
|
const jsPsych = initJsPsych();
|
|
279
443
|
const rec = new Recorder(jsPsych);
|