@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/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().mockReturnValue({
14
- finishTrial: jest.fn().mockImplementation(),
15
- getCurrentTrial: jest
16
- .fn()
17
- .mockReturnValue({ type: { info: { name: "test-type" } } }),
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(global, "window", {
28
- value: {
29
- chs,
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 does nothing", async () => {
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("Start Recording", async () => {
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 Recording", async () => {
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
+ });
@@ -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
- const rec = new Recorder(initJsPsych());
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
- const handleStop = rec["handleStop"](resolve);
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
- // manual mock
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
- // let's download the file locally
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);