@lookit/record 4.1.0 → 6.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
@@ -4,11 +4,28 @@ import { initJsPsych, PluginInfo, TrialType } from "jspsych";
4
4
  import { ExistingRecordingError, NoSessionRecordingError } from "./errors";
5
5
  import Rec from "./index";
6
6
  import Recorder from "./recorder";
7
+ import type { StopResult } from "./types";
7
8
 
8
9
  declare const window: LookitWindow;
9
10
 
10
11
  let global_display_el: HTMLDivElement;
11
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
+
12
29
  jest.mock("./recorder");
13
30
  jest.mock("@lookit/data");
14
31
  jest.mock("jspsych", () => ({
@@ -41,23 +58,34 @@ const setCHSValue = (chs = {}) => {
41
58
 
42
59
  beforeEach(() => {
43
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(() => {});
44
65
  });
45
66
 
46
67
  afterEach(() => {
47
68
  jest.restoreAllMocks();
48
69
  jest.clearAllMocks();
70
+
71
+ consoleLogSpy.mockRestore();
72
+ consoleWarnSpy.mockRestore();
73
+ consoleErrorSpy.mockRestore();
49
74
  });
50
75
 
51
- test("Trial recording", () => {
76
+ test("Trial recording", async () => {
52
77
  const mockRecStart = jest.spyOn(Recorder.prototype, "start");
53
- 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
+ });
54
82
  const jsPsych = initJsPsych();
55
83
  const trialRec = new Rec.TrialRecordExtension(jsPsych);
56
84
  const getCurrentPluginNameSpy = jest.spyOn(trialRec, "getCurrentPluginName");
57
85
 
58
86
  trialRec.on_start();
59
87
  trialRec.on_load();
60
- trialRec.on_finish();
88
+ await trialRec.on_finish();
61
89
 
62
90
  expect(Recorder).toHaveBeenCalledTimes(1);
63
91
  expect(mockRecStart).toHaveBeenCalledTimes(1);
@@ -134,10 +162,18 @@ test("Trial recording start with wait_for_upload_message parameter", async () =>
134
162
 
135
163
  test("Trial recording stop/finish with default uploading msg in English", async () => {
136
164
  // 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));
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
+ });
139
173
 
140
- jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
174
+ jest
175
+ .spyOn(Recorder.prototype, "stop")
176
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
141
177
 
142
178
  const jsPsych = initJsPsych();
143
179
  const trialRec = new Rec.TrialRecordExtension(jsPsych);
@@ -160,14 +196,17 @@ test("Trial recording stop/finish with default uploading msg in English", async
160
196
  } as TrialType<PluginInfo>),
161
197
  );
162
198
  expect(global_display_el.innerHTML).toBe(
163
- "<div>uploading video, please wait...</div>",
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>`,
164
202
  );
165
- //expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
166
203
 
167
204
  // resolve the stop promise
168
- resolveStop();
205
+ resolveStop("url");
169
206
  await stopPromise;
170
- await Promise.resolve();
207
+ // resolve the upload promise
208
+ resolveUpload();
209
+ await uploadPromise;
171
210
 
172
211
  // check the display cleanup
173
212
  expect(global_display_el.innerHTML).toBe("");
@@ -175,10 +214,18 @@ test("Trial recording stop/finish with default uploading msg in English", async
175
214
 
176
215
  test("Trial recording stop/finish with different locale should display default uploading msg in specified language", async () => {
177
216
  // 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));
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
+ });
180
225
 
181
- jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
226
+ jest
227
+ .spyOn(Recorder.prototype, "stop")
228
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
182
229
 
183
230
  const jsPsych = initJsPsych();
184
231
  const trialRec = new Rec.TrialRecordExtension(jsPsych);
@@ -201,14 +248,17 @@ test("Trial recording stop/finish with different locale should display default u
201
248
  } as TrialType<PluginInfo>),
202
249
  );
203
250
  expect(global_display_el.innerHTML).toBe(
204
- "<div>téléchargement video en cours, veuillez attendre...</div>",
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>`,
205
254
  );
206
- //expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
207
255
 
208
256
  // resolve the stop promise
209
- resolveStop();
257
+ resolveStop("url");
210
258
  await stopPromise;
211
- await Promise.resolve();
259
+ // resolve the upload promise
260
+ resolveUpload();
261
+ await uploadPromise;
212
262
 
213
263
  // check the display cleanup
214
264
  expect(global_display_el.innerHTML).toBe("");
@@ -216,10 +266,18 @@ test("Trial recording stop/finish with different locale should display default u
216
266
 
217
267
  test("Trial recording stop/finish with custom uploading message", async () => {
218
268
  // 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));
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
+ });
221
277
 
222
- jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
278
+ jest
279
+ .spyOn(Recorder.prototype, "stop")
280
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
223
281
 
224
282
  const jsPsych = initJsPsych();
225
283
  const trialRec = new Rec.TrialRecordExtension(jsPsych);
@@ -235,25 +293,155 @@ test("Trial recording stop/finish with custom uploading message", async () => {
235
293
  trialRec.on_finish();
236
294
 
237
295
  expect(global_display_el.innerHTML).toBe("Wait!");
238
- //expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
239
296
 
240
297
  // resolve the stop promise
241
- resolveStop();
298
+ resolveStop("url");
242
299
  await stopPromise;
243
- await Promise.resolve();
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
+ });
244
425
 
245
426
  // check the display cleanup
246
427
  expect(global_display_el.innerHTML).toBe("");
247
428
  });
248
429
 
249
- test("Trial recording rejection path (failure during upload)", async () => {
430
+ test("Trial recording stop with failure during stop", async () => {
250
431
  // Create a controlled promise and capture the reject function
251
432
  let rejectStop!: (err: unknown) => void;
252
- const stopPromise = new Promise<void>((_, reject) => {
433
+ const stopPromise = new Promise<string>((_, reject) => {
253
434
  rejectStop = reject;
254
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
+ });
255
441
 
256
- jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
442
+ jest
443
+ .spyOn(Recorder.prototype, "stop")
444
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
257
445
 
258
446
  const jsPsych = initJsPsych();
259
447
  const trialRec = new Rec.TrialRecordExtension(jsPsych);
@@ -267,30 +455,103 @@ test("Trial recording rejection path (failure during upload)", async () => {
267
455
 
268
456
  // Should show initial wait for upload message
269
457
  expect(global_display_el.innerHTML).toBe(
270
- "<div>uploading video, please wait...</div>",
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>`,
271
461
  );
272
462
 
273
463
  // Reject stop
274
- rejectStop(new Error("upload failed"));
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
+ );
275
473
 
276
474
  // Wait for plugin's `.catch()` handler to run
277
475
  await Promise.resolve();
278
476
 
279
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("");
479
+ });
480
+
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
280
507
  expect(global_display_el.innerHTML).toBe(
281
- "<div>uploading video, please wait...</div>",
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"),
282
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("");
283
538
  });
284
539
 
285
540
  test("Start session recording", async () => {
286
541
  const mockRecStart = jest.spyOn(Recorder.prototype, "start");
287
542
  const jsPsych = initJsPsych();
288
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>;
289
550
 
290
551
  // manual mock
291
552
  mockRecStart.mockImplementation(jest.fn().mockReturnValue(Promise.resolve()));
292
553
 
293
- await startRec.trial();
554
+ await startRec.trial(display_element, trial);
294
555
 
295
556
  expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
296
557
  expect(() => {
@@ -298,6 +559,101 @@ test("Start session recording", async () => {
298
559
  }).toThrow(ExistingRecordingError);
299
560
  });
300
561
 
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
+
301
657
  test("Stop session recording", async () => {
302
658
  const mockRecStop = jest.spyOn(Recorder.prototype, "stop");
303
659
  const jsPsych = initJsPsych();
@@ -311,7 +667,12 @@ test("Stop session recording", async () => {
311
667
  .fn()
312
668
  .mockImplementation() as unknown as HTMLElement;
313
669
 
314
- mockRecStop.mockImplementation(jest.fn().mockReturnValue(Promise.resolve()));
670
+ mockRecStop.mockImplementation(
671
+ (): StopResult => ({
672
+ stopped: Promise.resolve("mock-url"),
673
+ uploaded: Promise.resolve(),
674
+ }),
675
+ );
315
676
 
316
677
  const trial = {
317
678
  locale: "en-us",
@@ -332,10 +693,18 @@ test("Stop session recording", async () => {
332
693
 
333
694
  test("Stop session recording should display default uploading msg in English", async () => {
334
695
  // 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));
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
+ });
337
704
 
338
- jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
705
+ jest
706
+ .spyOn(Recorder.prototype, "stop")
707
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
339
708
 
340
709
  const jsPsych = initJsPsych();
341
710
 
@@ -355,13 +724,24 @@ test("Stop session recording should display default uploading msg in English", a
355
724
  // call trial but don't await so that we can inspect before it resolves
356
725
  stop_rec_plugin.trial(display_element, trial);
357
726
 
358
- expect(display_element.innerHTML).toBe(chsTemplates.uploadingVideo(trial));
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);
359
736
  expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
360
737
 
361
- // resolve the stop promise
362
- resolveStop();
738
+ // resolve the stop promise and upload promise
739
+ resolveStop("url");
363
740
  await stopPromise;
364
741
  await Promise.resolve();
742
+ resolveUpload();
743
+ await uploadPromise;
744
+ await Promise.resolve();
365
745
 
366
746
  // check the cleanup tasks after the trial method has resolved
367
747
  expect(display_element.innerHTML).toBe("");
@@ -371,10 +751,18 @@ test("Stop session recording should display default uploading msg in English", a
371
751
 
372
752
  test("Stop session recording with different locale should display default uploading msg in specified language", async () => {
373
753
  // 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));
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
+ });
376
762
 
377
- jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
763
+ jest
764
+ .spyOn(Recorder.prototype, "stop")
765
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
378
766
 
379
767
  const jsPsych = initJsPsych();
380
768
 
@@ -399,15 +787,20 @@ test("Stop session recording with different locale should display default upload
399
787
 
400
788
  // check that fr translation is used
401
789
  expect(fr_uploading_msg).toBe(
402
- "<div>téléchargement video en cours, veuillez attendre...</div>",
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>`,
403
793
  );
404
794
  expect(display_element.innerHTML).toBe(fr_uploading_msg);
405
795
  expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
406
796
 
407
- // resolve the stop promise
408
- resolveStop();
797
+ // resolve the stop promise and upload promise
798
+ resolveStop("url");
409
799
  await stopPromise;
410
800
  await Promise.resolve();
801
+ resolveUpload();
802
+ await uploadPromise;
803
+ await Promise.resolve();
411
804
 
412
805
  // check the cleanup tasks after the trial method has resolved
413
806
  expect(display_element.innerHTML).toBe("");
@@ -417,10 +810,18 @@ test("Stop session recording with different locale should display default upload
417
810
 
418
811
  test("Stop session recording with custom uploading message", async () => {
419
812
  // 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));
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
+ });
422
821
 
423
- jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
822
+ jest
823
+ .spyOn(Recorder.prototype, "stop")
824
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
424
825
 
425
826
  const jsPsych = initJsPsych();
426
827
  setCHSValue({ sessionRecorder: new Recorder(jsPsych) });
@@ -439,9 +840,12 @@ test("Stop session recording with custom uploading message", async () => {
439
840
  // check display before stop is resolved
440
841
  expect(display_element.innerHTML).toBe("<p>Custom message…</p>");
441
842
 
442
- resolveStop();
843
+ resolveStop("url");
443
844
  await stopPromise;
444
845
  await Promise.resolve();
846
+ resolveUpload();
847
+ await uploadPromise;
848
+ await Promise.resolve();
445
849
 
446
850
  // check the cleanup tasks after the trial method has resolved
447
851
  expect(display_element.innerHTML).toBe("");
@@ -449,14 +853,53 @@ test("Stop session recording with custom uploading message", async () => {
449
853
  expect(window.chs.sessionRecorder).toBeNull();
450
854
  });
451
855
 
452
- test("Stop recording rejection path (failure during upload)", async () => {
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 () => {
453
889
  // Create a controlled promise and capture the reject function
454
890
  let rejectStop!: (err: unknown) => void;
455
- const stopPromise = new Promise<void>((_, reject) => {
891
+ const stopPromise = new Promise<string>((_, reject) => {
456
892
  rejectStop = reject;
457
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
+ });
458
899
 
459
- jest.spyOn(Recorder.prototype, "stop").mockReturnValue(stopPromise);
900
+ jest
901
+ .spyOn(Recorder.prototype, "stop")
902
+ .mockReturnValue({ stopped: stopPromise, uploaded: uploadPromise });
460
903
 
461
904
  const jsPsych = initJsPsych();
462
905
  setCHSValue({ sessionRecorder: new Recorder(jsPsych) });
@@ -481,9 +924,9 @@ test("Stop recording rejection path (failure during upload)", async () => {
481
924
  // Wait for plugin's `.catch()` handler to run
482
925
  await Promise.resolve();
483
926
 
484
- // Trial doesn't end and the cleanup tasks don't run.
485
927
  // 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();
928
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
929
+ "StopRecordPlugin: recorder stop/upload failed.",
930
+ Error("upload failed"),
931
+ );
489
932
  });