@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/src/index.spec.ts CHANGED
@@ -1,20 +1,45 @@
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";
5
6
  import Recorder from "./recorder";
7
+ import type { StopResult } from "./types";
6
8
 
7
9
  declare const window: LookitWindow;
8
10
 
11
+ let global_display_el: HTMLDivElement;
12
+
13
+ let consoleLogSpy: jest.SpyInstance<
14
+ void,
15
+ [message?: unknown, ...optionalParams: unknown[]],
16
+ unknown
17
+ >;
18
+ let consoleWarnSpy: jest.SpyInstance<
19
+ void,
20
+ [message?: unknown, ...optionalParams: unknown[]],
21
+ unknown
22
+ >;
23
+ let consoleErrorSpy: jest.SpyInstance<
24
+ void,
25
+ [message?: unknown, ...optionalParams: unknown[]],
26
+ unknown
27
+ >;
28
+
9
29
  jest.mock("./recorder");
10
30
  jest.mock("@lookit/data");
11
31
  jest.mock("jspsych", () => ({
12
32
  ...jest.requireActual("jspsych"),
13
- initJsPsych: jest.fn().mockReturnValue({
14
- finishTrial: jest.fn().mockImplementation(),
15
- getCurrentTrial: jest
16
- .fn()
17
- .mockReturnValue({ type: { info: { name: "test-type" } } }),
33
+ initJsPsych: jest.fn().mockImplementation(() => {
34
+ // create a new display element for each jsPsych instance
35
+ global_display_el = document.createElement("div");
36
+ return {
37
+ finishTrial: jest.fn().mockImplementation(),
38
+ getCurrentTrial: jest
39
+ .fn()
40
+ .mockReturnValue({ type: { info: { name: "test-type" } } }),
41
+ getDisplayElement: jest.fn(() => global_display_el),
42
+ };
18
43
  }),
19
44
  }));
20
45
 
@@ -24,31 +49,43 @@ jest.mock("jspsych", () => ({
24
49
  * @param chs - Contents of chs storage.
25
50
  */
26
51
  const setCHSValue = (chs = {}) => {
27
- Object.defineProperty(global, "window", {
28
- value: {
29
- chs,
30
- },
52
+ Object.defineProperty(window, "chs", {
53
+ value: chs,
54
+ configurable: true,
55
+ writable: true,
31
56
  });
32
57
  };
33
58
 
34
59
  beforeEach(() => {
35
60
  setCHSValue();
61
+ // Hide the console output during tests. Tests can still assert on these spies to check console calls.
62
+ consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
63
+ consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
64
+ consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
36
65
  });
37
66
 
38
67
  afterEach(() => {
68
+ jest.restoreAllMocks();
39
69
  jest.clearAllMocks();
70
+
71
+ consoleLogSpy.mockRestore();
72
+ consoleWarnSpy.mockRestore();
73
+ consoleErrorSpy.mockRestore();
40
74
  });
41
75
 
42
- test("Trial recording", () => {
76
+ test("Trial recording", async () => {
43
77
  const mockRecStart = jest.spyOn(Recorder.prototype, "start");
44
- const mockRecStop = jest.spyOn(Recorder.prototype, "stop");
78
+ const mockRecStop = jest.spyOn(Recorder.prototype, "stop").mockReturnValue({
79
+ stopped: Promise.resolve("url"),
80
+ uploaded: Promise.resolve(),
81
+ });
45
82
  const jsPsych = initJsPsych();
46
83
  const trialRec = new Rec.TrialRecordExtension(jsPsych);
47
84
  const getCurrentPluginNameSpy = jest.spyOn(trialRec, "getCurrentPluginName");
48
85
 
49
86
  trialRec.on_start();
50
87
  trialRec.on_load();
51
- trialRec.on_finish();
88
+ await trialRec.on_finish();
52
89
 
53
90
  expect(Recorder).toHaveBeenCalledTimes(1);
54
91
  expect(mockRecStart).toHaveBeenCalledTimes(1);
@@ -57,23 +94,464 @@ test("Trial recording", () => {
57
94
  expect(getCurrentPluginNameSpy).toHaveBeenCalledTimes(1);
58
95
  });
59
96
 
60
- test("Trial recording's initialize does nothing", async () => {
97
+ test("Trial recording's initialize with no parameters", async () => {
61
98
  const jsPsych = initJsPsych();
62
99
  const trialRec = new Rec.TrialRecordExtension(jsPsych);
63
100
 
64
101
  expect(await trialRec.initialize()).toBeUndefined();
102
+ expect(trialRec["uploadMsg"]).toBeNull;
103
+ expect(trialRec["locale"]).toBe("en-us");
104
+ expect(Recorder).toHaveBeenCalledTimes(0);
105
+ });
106
+
107
+ test("Trial recording's initialize with locale parameter", async () => {
108
+ const jsPsych = initJsPsych();
109
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
110
+
111
+ expect(await trialRec.initialize({ locale: "fr" })).toBeUndefined();
112
+ expect(trialRec["locale"]).toBe("fr");
113
+ expect(trialRec["uploadMsg"]).toBeNull;
114
+ expect(Recorder).toHaveBeenCalledTimes(0);
115
+ });
116
+
117
+ test("Trial recording's initialize with wait_for_upload_message parameter", async () => {
118
+ const jsPsych = initJsPsych();
119
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
120
+
121
+ expect(
122
+ await trialRec.initialize({ wait_for_upload_message: "Please wait..." }),
123
+ ).toBeUndefined();
124
+ expect(trialRec["uploadMsg"]).toBe("Please wait...");
125
+ expect(trialRec["locale"]).toBe("en-us");
126
+ expect(Recorder).toHaveBeenCalledTimes(0);
127
+ });
128
+
129
+ test("Trial recording start with locale parameter", async () => {
130
+ const jsPsych = initJsPsych();
131
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
132
+
133
+ await trialRec.initialize();
134
+
135
+ expect(trialRec["uploadMsg"]).toBeNull;
136
+ expect(trialRec["locale"]).toBe("en-us");
65
137
  expect(Recorder).toHaveBeenCalledTimes(0);
138
+
139
+ trialRec.on_start({ locale: "fr" });
140
+
141
+ expect(trialRec["uploadMsg"]).toBeNull;
142
+ expect(trialRec["locale"]).toBe("fr");
143
+ expect(Recorder).toHaveBeenCalledTimes(1);
144
+ });
145
+
146
+ test("Trial recording start with wait_for_upload_message parameter", async () => {
147
+ const jsPsych = initJsPsych();
148
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
149
+
150
+ await trialRec.initialize();
151
+
152
+ expect(trialRec["uploadMsg"]).toBeNull;
153
+ expect(trialRec["locale"]).toBe("en-us");
154
+ expect(Recorder).toHaveBeenCalledTimes(0);
155
+
156
+ trialRec.on_start({ wait_for_upload_message: "Please wait..." });
157
+
158
+ expect(trialRec["uploadMsg"]).toBe("Please wait...");
159
+ expect(trialRec["locale"]).toBe("en-us");
160
+ expect(Recorder).toHaveBeenCalledTimes(1);
161
+ });
162
+
163
+ test("Trial recording stop/finish with default uploading msg in English", async () => {
164
+ // control the recorder stop promise so that we can inspect the display before it resolves
165
+ let resolveStop!: (value: string) => void;
166
+ let resolveUpload!: () => void;
167
+ const stopPromise = new Promise<string>((res) => {
168
+ resolveStop = res;
169
+ });
170
+ const uploadPromise = new Promise<void>((res) => {
171
+ resolveUpload = res;
172
+ });
173
+
174
+ jest
175
+ .spyOn(Recorder.prototype, "stop")
176
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
177
+
178
+ const jsPsych = initJsPsych();
179
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
180
+
181
+ const params = {
182
+ locale: "en-us",
183
+ wait_for_upload_message: null,
184
+ };
185
+ await trialRec.initialize(params);
186
+ trialRec.on_start();
187
+ trialRec.on_load();
188
+
189
+ // call on_finish but don't await so that we can inspect before it resolves
190
+ trialRec.on_finish();
191
+
192
+ expect(global_display_el.innerHTML).toBe(
193
+ chsTemplates.uploadingVideo({
194
+ type: jsPsych.getCurrentTrial().type,
195
+ locale: params.locale,
196
+ } as TrialType<PluginInfo>),
197
+ );
198
+ expect(global_display_el.innerHTML).toBe(
199
+ `<div id="lookit-uploading-video-msg-container">
200
+ <div>uploading video, please wait...</div>
201
+ <div id="lookit-loader-container"><div class="loader"></div></div></div>`,
202
+ );
203
+
204
+ // resolve the stop promise
205
+ resolveStop("url");
206
+ await stopPromise;
207
+ // resolve the upload promise
208
+ resolveUpload();
209
+ await uploadPromise;
210
+
211
+ // check the display cleanup
212
+ expect(global_display_el.innerHTML).toBe("");
213
+ });
214
+
215
+ test("Trial recording stop/finish with different locale should display default uploading msg in specified language", async () => {
216
+ // control the recorder stop promise so that we can inspect the display before it resolves
217
+ let resolveStop!: (value: string) => void;
218
+ let resolveUpload!: () => void;
219
+ const stopPromise = new Promise<string>((res) => {
220
+ resolveStop = res;
221
+ });
222
+ const uploadPromise = new Promise<void>((res) => {
223
+ resolveUpload = res;
224
+ });
225
+
226
+ jest
227
+ .spyOn(Recorder.prototype, "stop")
228
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
229
+
230
+ const jsPsych = initJsPsych();
231
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
232
+
233
+ const params = {
234
+ locale: "fr",
235
+ wait_for_upload_message: null,
236
+ };
237
+ await trialRec.initialize(params);
238
+ trialRec.on_start();
239
+ trialRec.on_load();
240
+
241
+ // call on_finish but don't await so that we can inspect before it resolves
242
+ trialRec.on_finish();
243
+
244
+ expect(global_display_el.innerHTML).toBe(
245
+ chsTemplates.uploadingVideo({
246
+ type: jsPsych.getCurrentTrial().type,
247
+ locale: params.locale,
248
+ } as TrialType<PluginInfo>),
249
+ );
250
+ expect(global_display_el.innerHTML).toBe(
251
+ `<div id="lookit-uploading-video-msg-container">
252
+ <div>téléchargement video en cours, veuillez attendre...</div>
253
+ <div id="lookit-loader-container"><div class="loader"></div></div></div>`,
254
+ );
255
+
256
+ // resolve the stop promise
257
+ resolveStop("url");
258
+ await stopPromise;
259
+ // resolve the upload promise
260
+ resolveUpload();
261
+ await uploadPromise;
262
+
263
+ // check the display cleanup
264
+ expect(global_display_el.innerHTML).toBe("");
265
+ });
266
+
267
+ test("Trial recording stop/finish with custom uploading message", async () => {
268
+ // control the recorder stop promise so that we can inspect the display before it resolves
269
+ let resolveStop!: (value: string) => void;
270
+ let resolveUpload!: () => void;
271
+ const stopPromise = new Promise<string>((res) => {
272
+ resolveStop = res;
273
+ });
274
+ const uploadPromise = new Promise<void>((res) => {
275
+ resolveUpload = res;
276
+ });
277
+
278
+ jest
279
+ .spyOn(Recorder.prototype, "stop")
280
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
281
+
282
+ const jsPsych = initJsPsych();
283
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
284
+
285
+ const params = {
286
+ wait_for_upload_message: "Wait!",
287
+ };
288
+ await trialRec.initialize(params);
289
+ trialRec.on_start();
290
+ trialRec.on_load();
291
+
292
+ // call on_finish but don't await so that we can inspect before it resolves
293
+ trialRec.on_finish();
294
+
295
+ expect(global_display_el.innerHTML).toBe("Wait!");
296
+
297
+ // resolve the stop promise
298
+ resolveStop("url");
299
+ await stopPromise;
300
+ resolveUpload();
301
+ await uploadPromise;
302
+
303
+ // check the display cleanup
304
+ expect(global_display_el.innerHTML).toBe("");
305
+ });
306
+
307
+ test("Trial recording stop/finish timeout with default parameters", async () => {
308
+ // simulate a resolved stop promise and timeout upload promise
309
+ const stopPromise = new Promise<string>((res) => res("url"));
310
+ const uploadPromise = new Promise<string>((res) => res("timeout"));
311
+
312
+ const recStopSpy = jest
313
+ .spyOn(Recorder.prototype, "stop")
314
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
315
+
316
+ const jsPsych = initJsPsych();
317
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
318
+
319
+ await trialRec.initialize();
320
+ trialRec.on_start();
321
+ trialRec.on_load();
322
+
323
+ await trialRec.on_finish();
324
+
325
+ // recorder.stop should be called with the default max upload duration
326
+ expect(recStopSpy).toHaveBeenCalledWith({
327
+ upload_timeout_ms: 10000,
328
+ });
329
+
330
+ // check the display cleanup
331
+ expect(global_display_el.innerHTML).toBe("");
332
+ });
333
+
334
+ test("Trial recording stop/finish with max upload duration initialize parameter", async () => {
335
+ // simulate a resolved stop promise and timeout upload promise
336
+ const stopPromise = new Promise<string>((res) => res("url"));
337
+ const uploadPromise = new Promise<string>((res) => res("timeout"));
338
+
339
+ const recStopSpy = jest
340
+ .spyOn(Recorder.prototype, "stop")
341
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
342
+
343
+ const jsPsych = initJsPsych();
344
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
345
+
346
+ const params = {
347
+ max_upload_seconds: 20,
348
+ };
349
+
350
+ await trialRec.initialize(params);
351
+ trialRec.on_start();
352
+ trialRec.on_load();
353
+
354
+ await trialRec.on_finish();
355
+
356
+ // recorder.stop should be called with 20 seconds as the max upload duration
357
+ expect(recStopSpy).toHaveBeenCalledWith({
358
+ upload_timeout_ms: 20000,
359
+ });
360
+
361
+ // check the display cleanup
362
+ expect(global_display_el.innerHTML).toBe("");
363
+ });
364
+
365
+ test("Trial recording stop/finish with max upload duration start parameter", async () => {
366
+ // simulate a resolved stop promise and timeout upload promise
367
+ const stopPromise = new Promise<string>((res) => res("url"));
368
+ const uploadPromise = new Promise<string>((res) => res("timeout"));
369
+
370
+ const recStopSpy = jest
371
+ .spyOn(Recorder.prototype, "stop")
372
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
373
+
374
+ const jsPsych = initJsPsych();
375
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
376
+
377
+ const initParams = {
378
+ max_upload_seconds: null,
379
+ };
380
+ const startParams = {
381
+ max_upload_seconds: 20,
382
+ };
383
+
384
+ await trialRec.initialize(initParams);
385
+ trialRec.on_start(startParams);
386
+ trialRec.on_load();
387
+
388
+ await trialRec.on_finish();
389
+
390
+ // recorder.stop should be called with 20 seconds as the max upload duration
391
+ expect(recStopSpy).toHaveBeenCalledWith({
392
+ upload_timeout_ms: 20000,
393
+ });
394
+
395
+ // check the display cleanup
396
+ expect(global_display_el.innerHTML).toBe("");
397
+ });
398
+
399
+ test("Trial recording stop/finish with null max upload duration", async () => {
400
+ // simulate a resolved stop promise and resolved upload promise
401
+ const stopPromise = new Promise<string>((res) => res("url"));
402
+ const uploadPromise = new Promise<void>((res) => res());
403
+
404
+ const recStopSpy = jest
405
+ .spyOn(Recorder.prototype, "stop")
406
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
407
+
408
+ const jsPsych = initJsPsych();
409
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
410
+
411
+ const params = {
412
+ max_upload_seconds: null,
413
+ };
414
+
415
+ await trialRec.initialize();
416
+ trialRec.on_start(params);
417
+ trialRec.on_load();
418
+
419
+ await trialRec.on_finish();
420
+
421
+ // recorder.stop should be called with null as the max upload duration
422
+ expect(recStopSpy).toHaveBeenCalledWith({
423
+ upload_timeout_ms: null,
424
+ });
425
+
426
+ // check the display cleanup
427
+ expect(global_display_el.innerHTML).toBe("");
428
+ });
429
+
430
+ test("Trial recording stop with failure during stop", async () => {
431
+ // Create a controlled promise and capture the reject function
432
+ let rejectStop!: (err: unknown) => void;
433
+ const stopPromise = new Promise<string>((_, reject) => {
434
+ rejectStop = reject;
435
+ });
436
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
437
+ let resolveUpload!: () => void;
438
+ const uploadPromise = new Promise<void>((res) => {
439
+ resolveUpload = res;
440
+ });
441
+
442
+ jest
443
+ .spyOn(Recorder.prototype, "stop")
444
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
445
+
446
+ const jsPsych = initJsPsych();
447
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
448
+
449
+ await trialRec.initialize();
450
+ trialRec.on_start();
451
+ trialRec.on_load();
452
+
453
+ // call on_finish but don't await so that we can inspect before it resolves
454
+ trialRec.on_finish();
455
+
456
+ // Should show initial wait for upload message
457
+ expect(global_display_el.innerHTML).toBe(
458
+ `<div id="lookit-uploading-video-msg-container">
459
+ <div>uploading video, please wait...</div>
460
+ <div id="lookit-loader-container"><div class="loader"></div></div></div>`,
461
+ );
462
+
463
+ // Reject stop
464
+ rejectStop(new Error("stop failed"));
465
+
466
+ // Wait for plugin's `.catch()` handler to run
467
+ await Promise.resolve();
468
+
469
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
470
+ "TrialRecordExtension: recorder stop/upload failed.",
471
+ Error("stop failed"),
472
+ );
473
+
474
+ // Wait for plugin's `.catch()` handler to run
475
+ await Promise.resolve();
476
+
477
+ // TO DO: modify the trial extension code to display translated error msg and/or researcher contact info
478
+ expect(global_display_el.innerHTML).toBe("");
66
479
  });
67
480
 
68
- test("Start Recording", async () => {
481
+ test("Trial recording stop with failure during upload", async () => {
482
+ let resolveStop!: (value: string) => void;
483
+ const stopPromise = new Promise<string>((res) => {
484
+ resolveStop = res;
485
+ });
486
+ // Create a controlled promise and capture the reject function
487
+ let rejectUpload!: (err: unknown) => void;
488
+ const uploadPromise = new Promise<string>((_, reject) => {
489
+ rejectUpload = reject;
490
+ });
491
+
492
+ jest
493
+ .spyOn(Recorder.prototype, "stop")
494
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
495
+
496
+ const jsPsych = initJsPsych();
497
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
498
+
499
+ await trialRec.initialize();
500
+ trialRec.on_start();
501
+ trialRec.on_load();
502
+
503
+ // call on_finish but don't await so that we can inspect before it resolves
504
+ trialRec.on_finish();
505
+
506
+ // Should show initial wait for upload message
507
+ expect(global_display_el.innerHTML).toBe(
508
+ `<div id="lookit-uploading-video-msg-container">
509
+ <div>uploading video, please wait...</div>
510
+ <div id="lookit-loader-container"><div class="loader"></div></div></div>`,
511
+ );
512
+
513
+ // Resolve stop
514
+ resolveStop("url");
515
+ // Reject upload
516
+ rejectUpload(new Error("upload failed"));
517
+
518
+ // Wait for plugin's `.catch()` handler to run and flush microtasks
519
+ await Promise.resolve();
520
+ await Promise.resolve();
521
+
522
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
523
+ "TrialRecordExtension: recorder stop/upload failed.",
524
+ Error("upload failed"),
525
+ );
526
+
527
+ // TO DO: modify the trial extension code to display translated error msg and/or researcher contact info
528
+ expect(global_display_el.innerHTML).toBe("");
529
+ });
530
+
531
+ test("Trial recording stop with no recorder", async () => {
532
+ const jsPsych = initJsPsych();
533
+ const trialRec = new Rec.TrialRecordExtension(jsPsych);
534
+
535
+ // no recorder - extension should clean up display and immediately resolve on_finish
536
+ await trialRec.on_finish();
537
+ expect(global_display_el.innerHTML).toBe("");
538
+ });
539
+
540
+ test("Start session recording", async () => {
69
541
  const mockRecStart = jest.spyOn(Recorder.prototype, "start");
70
542
  const jsPsych = initJsPsych();
71
543
  const startRec = new Rec.StartRecordPlugin(jsPsych);
544
+ const display_element = jest
545
+ .fn()
546
+ .mockImplementation() as unknown as HTMLElement;
547
+ const trial = {
548
+ locale: "en-us",
549
+ } as unknown as TrialType<PluginInfo>;
72
550
 
73
551
  // manual mock
74
552
  mockRecStart.mockImplementation(jest.fn().mockReturnValue(Promise.resolve()));
75
553
 
76
- await startRec.trial();
554
+ await startRec.trial(display_element, trial);
77
555
 
78
556
  expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
79
557
  expect(() => {
@@ -81,7 +559,102 @@ test("Start Recording", async () => {
81
559
  }).toThrow(ExistingRecordingError);
82
560
  });
83
561
 
84
- test("Stop Recording", async () => {
562
+ test("Start session recording with default wait for connection message", async () => {
563
+ let resolveMockStart!: () => void;
564
+ const startRecPromise = new Promise<void>((res) => {
565
+ resolveMockStart = res;
566
+ });
567
+ const mockRecStart = jest.spyOn(Recorder.prototype, "start");
568
+ mockRecStart.mockImplementation(jest.fn().mockReturnValue(startRecPromise));
569
+
570
+ const jsPsych = initJsPsych();
571
+ const startRec = new Rec.StartRecordPlugin(jsPsych);
572
+ const display_element = jest
573
+ .fn()
574
+ .mockImplementation() as unknown as HTMLElement;
575
+ const trial = { locale: "en-us" } as unknown as TrialType<PluginInfo>;
576
+
577
+ // call trial but don't await so that we can inspect display element
578
+ startRec.trial(display_element, trial);
579
+ expect(display_element.innerHTML).toBe(
580
+ `<div id="lookit-establishing-connection-msg">
581
+ <div>establishing video connection, please wait...</div>
582
+ <div id="lookit-loader-container"><div class="loader"></div></div></div>`,
583
+ );
584
+
585
+ // now resolve start promise and await
586
+ resolveMockStart();
587
+ await startRecPromise;
588
+
589
+ // clean up tasks should run
590
+ expect(display_element.innerHTML).toBe("");
591
+ expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
592
+ });
593
+
594
+ test("Start session recording with translated connection message", async () => {
595
+ let resolveMockStart!: () => void;
596
+ const startRecPromise = new Promise<void>((res) => {
597
+ resolveMockStart = res;
598
+ });
599
+ const mockRecStart = jest.spyOn(Recorder.prototype, "start");
600
+ mockRecStart.mockImplementation(jest.fn().mockReturnValue(startRecPromise));
601
+
602
+ const jsPsych = initJsPsych();
603
+ const startRec = new Rec.StartRecordPlugin(jsPsych);
604
+ const display_element = jest
605
+ .fn()
606
+ .mockImplementation() as unknown as HTMLElement;
607
+ const trial = { locale: "fr" } as unknown as TrialType<PluginInfo>;
608
+
609
+ // call trial but don't await so that we can inspect display element
610
+ startRec.trial(display_element, trial);
611
+ expect(display_element.innerHTML).toBe(
612
+ `<div id="lookit-establishing-connection-msg">
613
+ <div>en attente de connection video, veuillez attendre...</div>
614
+ <div id="lookit-loader-container"><div class="loader"></div></div></div>`,
615
+ );
616
+
617
+ // now resolve start promise and await
618
+ resolveMockStart();
619
+ await startRecPromise;
620
+
621
+ // clean up tasks should run
622
+ expect(display_element.innerHTML).toBe("");
623
+ expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
624
+ });
625
+
626
+ test("Start session recording with custom wait for connection message", async () => {
627
+ let resolveMockStart!: () => void;
628
+ const startRecPromise = new Promise<void>((res) => {
629
+ resolveMockStart = res;
630
+ });
631
+ const mockRecStart = jest.spyOn(Recorder.prototype, "start");
632
+ mockRecStart.mockImplementation(jest.fn().mockReturnValue(startRecPromise));
633
+
634
+ const jsPsych = initJsPsych();
635
+ const startRec = new Rec.StartRecordPlugin(jsPsych);
636
+ const display_element = jest
637
+ .fn()
638
+ .mockImplementation() as unknown as HTMLElement;
639
+ const trial = {
640
+ wait_for_connection_message: "Hello!",
641
+ locale: "de", // should be ignored
642
+ } as unknown as TrialType<PluginInfo>;
643
+
644
+ // call trial but don't await so that we can inspect display element
645
+ startRec.trial(display_element, trial);
646
+ expect(display_element.innerHTML).toBe("Hello!");
647
+
648
+ // now resolve start promise and await
649
+ resolveMockStart();
650
+ await startRecPromise;
651
+
652
+ // clean up tasks should run
653
+ expect(display_element.innerHTML).toBe("");
654
+ expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
655
+ });
656
+
657
+ test("Stop session recording", async () => {
85
658
  const mockRecStop = jest.spyOn(Recorder.prototype, "stop");
86
659
  const jsPsych = initJsPsych();
87
660
 
@@ -94,7 +667,12 @@ test("Stop Recording", async () => {
94
667
  .fn()
95
668
  .mockImplementation() as unknown as HTMLElement;
96
669
 
97
- mockRecStop.mockImplementation(jest.fn().mockReturnValue(Promise.resolve()));
670
+ mockRecStop.mockImplementation(
671
+ (): StopResult => ({
672
+ stopped: Promise.resolve("mock-url"),
673
+ uploaded: Promise.resolve(),
674
+ }),
675
+ );
98
676
 
99
677
  const trial = {
100
678
  locale: "en-us",
@@ -112,3 +690,243 @@ test("Stop Recording", async () => {
112
690
  NoSessionRecordingError,
113
691
  );
114
692
  });
693
+
694
+ test("Stop session recording should display default uploading msg in English", async () => {
695
+ // control the recorder stop promise so that we can inspect the display before it resolves
696
+ let resolveStop!: (value: string) => void;
697
+ let resolveUpload!: () => void;
698
+ const stopPromise = new Promise<string>((res) => {
699
+ resolveStop = res;
700
+ });
701
+ const uploadPromise = new Promise<void>((res) => {
702
+ resolveUpload = res;
703
+ });
704
+
705
+ jest
706
+ .spyOn(Recorder.prototype, "stop")
707
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
708
+
709
+ const jsPsych = initJsPsych();
710
+
711
+ setCHSValue({
712
+ sessionRecorder: new Recorder(jsPsych),
713
+ });
714
+
715
+ const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
716
+ const display_element = document.createElement("div");
717
+
718
+ const trial = {
719
+ type: Rec.StopRecordPlugin.info.name,
720
+ locale: "en-us",
721
+ wait_for_upload_message: null,
722
+ } as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
723
+
724
+ // call trial but don't await so that we can inspect before it resolves
725
+ stop_rec_plugin.trial(display_element, trial);
726
+
727
+ const en_uploading_msg = chsTemplates.uploadingVideo(trial);
728
+
729
+ // check that en (default) is used
730
+ expect(en_uploading_msg).toBe(
731
+ `<div id="lookit-uploading-video-msg-container">
732
+ <div>uploading video, please wait...</div>
733
+ <div id="lookit-loader-container"><div class="loader"></div></div></div>`,
734
+ );
735
+ expect(display_element.innerHTML).toBe(en_uploading_msg);
736
+ expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
737
+
738
+ // resolve the stop promise and upload promise
739
+ resolveStop("url");
740
+ await stopPromise;
741
+ await Promise.resolve();
742
+ resolveUpload();
743
+ await uploadPromise;
744
+ await Promise.resolve();
745
+
746
+ // check the cleanup tasks after the trial method has resolved
747
+ expect(display_element.innerHTML).toBe("");
748
+ expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
749
+ expect(window.chs.sessionRecorder).toBeNull();
750
+ });
751
+
752
+ test("Stop session recording with different locale should display default uploading msg in specified language", async () => {
753
+ // control the recorder stop promise so that we can inspect the display before it resolves
754
+ let resolveStop!: (value: string) => void;
755
+ let resolveUpload!: () => void;
756
+ const stopPromise = new Promise<string>((res) => {
757
+ resolveStop = res;
758
+ });
759
+ const uploadPromise = new Promise<void>((res) => {
760
+ resolveUpload = res;
761
+ });
762
+
763
+ jest
764
+ .spyOn(Recorder.prototype, "stop")
765
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
766
+
767
+ const jsPsych = initJsPsych();
768
+
769
+ setCHSValue({
770
+ sessionRecorder: new Recorder(jsPsych),
771
+ });
772
+
773
+ const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
774
+ const display_element = document.createElement("div");
775
+
776
+ // set locale to fr
777
+ const trial = {
778
+ type: Rec.StopRecordPlugin.info.name,
779
+ locale: "fr",
780
+ wait_for_upload_message: null,
781
+ } as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
782
+
783
+ // call trial but don't await so that we can inspect before it resolves
784
+ stop_rec_plugin.trial(display_element, trial);
785
+
786
+ const fr_uploading_msg = chsTemplates.uploadingVideo(trial);
787
+
788
+ // check that fr translation is used
789
+ expect(fr_uploading_msg).toBe(
790
+ `<div id="lookit-uploading-video-msg-container">
791
+ <div>téléchargement video en cours, veuillez attendre...</div>
792
+ <div id="lookit-loader-container"><div class="loader"></div></div></div>`,
793
+ );
794
+ expect(display_element.innerHTML).toBe(fr_uploading_msg);
795
+ expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
796
+
797
+ // resolve the stop promise and upload promise
798
+ resolveStop("url");
799
+ await stopPromise;
800
+ await Promise.resolve();
801
+ resolveUpload();
802
+ await uploadPromise;
803
+ await Promise.resolve();
804
+
805
+ // check the cleanup tasks after the trial method has resolved
806
+ expect(display_element.innerHTML).toBe("");
807
+ expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
808
+ expect(window.chs.sessionRecorder).toBeNull();
809
+ });
810
+
811
+ test("Stop session recording with custom uploading message", async () => {
812
+ // control the recorder stop promise so that we can inspect the display before it resolves
813
+ let resolveStop!: (value: string) => void;
814
+ let resolveUpload!: () => void;
815
+ const stopPromise = new Promise<string>((res) => {
816
+ resolveStop = res;
817
+ });
818
+ const uploadPromise = new Promise<void>((res) => {
819
+ resolveUpload = res;
820
+ });
821
+
822
+ jest
823
+ .spyOn(Recorder.prototype, "stop")
824
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
825
+
826
+ const jsPsych = initJsPsych();
827
+ setCHSValue({ sessionRecorder: new Recorder(jsPsych) });
828
+
829
+ const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
830
+ const display_element = document.createElement("div");
831
+
832
+ const trial = {
833
+ type: Rec.StopRecordPlugin.info.name,
834
+ locale: "en-us",
835
+ wait_for_upload_message: "<p>Custom message…</p>",
836
+ } as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
837
+
838
+ stop_rec_plugin.trial(display_element, trial);
839
+
840
+ // check display before stop is resolved
841
+ expect(display_element.innerHTML).toBe("<p>Custom message…</p>");
842
+
843
+ resolveStop("url");
844
+ await stopPromise;
845
+ await Promise.resolve();
846
+ resolveUpload();
847
+ await uploadPromise;
848
+ await Promise.resolve();
849
+
850
+ // check the cleanup tasks after the trial method has resolved
851
+ expect(display_element.innerHTML).toBe("");
852
+ expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
853
+ expect(window.chs.sessionRecorder).toBeNull();
854
+ });
855
+
856
+ test("Session recording stop with null as max upload seconds (no upload timeout)", () => {
857
+ // simulate a resolved stop promise and upload promise
858
+ const stopPromise = new Promise<string>((res) => res("url"));
859
+ const uploadPromise = new Promise<void>((res) => res());
860
+
861
+ const recStopSpy = jest
862
+ .spyOn(Recorder.prototype, "stop")
863
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
864
+
865
+ const jsPsych = initJsPsych();
866
+ setCHSValue({ sessionRecorder: new Recorder(jsPsych) });
867
+
868
+ const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
869
+ const display_element = document.createElement("div");
870
+
871
+ const trial = {
872
+ type: Rec.StopRecordPlugin.info.name,
873
+ locale: "en-us",
874
+ max_upload_seconds: null,
875
+ } as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
876
+
877
+ stop_rec_plugin.trial(display_element, trial);
878
+
879
+ // recorder.stop should be called with null as the max upload duration
880
+ expect(recStopSpy).toHaveBeenCalledWith({
881
+ upload_timeout_ms: null,
882
+ });
883
+
884
+ // check the display cleanup
885
+ expect(global_display_el.innerHTML).toBe("");
886
+ });
887
+
888
+ test("Stop recording stop with failure during upload", async () => {
889
+ // Create a controlled promise and capture the reject function
890
+ let rejectStop!: (err: unknown) => void;
891
+ const stopPromise = new Promise<string>((_, reject) => {
892
+ rejectStop = reject;
893
+ });
894
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
895
+ let resolveUpload!: () => void;
896
+ const uploadPromise = new Promise<void>((res) => {
897
+ resolveUpload = res;
898
+ });
899
+
900
+ jest
901
+ .spyOn(Recorder.prototype, "stop")
902
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
903
+
904
+ const jsPsych = initJsPsych();
905
+ setCHSValue({ sessionRecorder: new Recorder(jsPsych) });
906
+
907
+ const stop_rec_plugin = new Rec.StopRecordPlugin(jsPsych);
908
+ const display_element = document.createElement("div");
909
+
910
+ const trial = {
911
+ type: Rec.StopRecordPlugin.info.name,
912
+ locale: "en-us",
913
+ wait_for_upload_message: "Wait…",
914
+ } as unknown as TrialType<PluginInfo>; // need to cast here because the "type" param is a string and should be a class
915
+
916
+ stop_rec_plugin.trial(display_element, trial);
917
+
918
+ // Should show initial wait for upload message
919
+ expect(display_element.innerHTML).toBe("Wait…");
920
+
921
+ // Reject stop
922
+ rejectStop(new Error("upload failed"));
923
+
924
+ // Wait for plugin's `.catch()` handler to run
925
+ await Promise.resolve();
926
+
927
+ // TO DO: modify the plugin code to display translated error msg and/or researcher contact info
928
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
929
+ "StopRecordPlugin: recorder stop/upload failed.",
930
+ Error("upload failed"),
931
+ );
932
+ });