@lookit/record 0.0.3

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.
@@ -0,0 +1,1113 @@
1
+ import { clickTarget } from "@jspsych/test-utils";
2
+ import Handlebars from "handlebars";
3
+ import { initJsPsych, JsPsych } from "jspsych";
4
+ import videoConfig from "../templates/video-config.hbs";
5
+ import { NoStreamError } from "./errors";
6
+ import chromeInitialPrompt from "./img/chrome_initialprompt.png";
7
+ import chromeAlwaysAllow from "./img/chrome_step1_alwaysallow.png";
8
+ import chromePermissions from "./img/chrome_step1_permissions.png";
9
+ import firefoxInitialPrompt from "./img/firefox_initialprompt.png";
10
+ import firefoxChooseDevice from "./img/firefox_prompt_choose_device.png";
11
+ import firefoxDevicesBlocked from "./img/firefox_prompt_devices_blocked.png";
12
+ import Recorder from "./recorder";
13
+ import VideoConfigPlugin, { VideoConsentTrialType } from "./video_config";
14
+
15
+ jest.mock("./recorder");
16
+ jest.mock("@lookit/data");
17
+ jest.mock("jspsych", () => ({
18
+ ...jest.requireActual("jspsych"),
19
+ initJsPsych: jest.fn().mockReturnValue({
20
+ finishTrial: jest.fn().mockImplementation(),
21
+ pluginAPI: {
22
+ initializeCameraRecorder: jest.fn(),
23
+ getCameraStream: jest.fn().mockReturnValue({
24
+ active: true,
25
+ clone: jest.fn(),
26
+ getTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
27
+ }),
28
+ getCameraRecorder: jest.fn().mockReturnValue({
29
+ addEventListener: jest.fn(),
30
+ start: jest.fn(),
31
+ stop: jest.fn(),
32
+ stream: {
33
+ active: true,
34
+ clone: jest.fn(),
35
+ getTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
36
+ },
37
+ }),
38
+ },
39
+ }),
40
+ }));
41
+
42
+ let display_el: HTMLBodyElement;
43
+ let jsPsych: JsPsych;
44
+ let video_config: VideoConfigPlugin;
45
+ let html_params: object;
46
+ let devices: MediaDeviceInfo[];
47
+ let devicesObj: {
48
+ cam1: MediaDeviceInfo;
49
+ cam2: MediaDeviceInfo;
50
+ mic1: MediaDeviceInfo;
51
+ mic2: MediaDeviceInfo;
52
+ };
53
+ let returnedDeviceLists: {
54
+ cameras: MediaDeviceInfo[];
55
+ mics: MediaDeviceInfo[];
56
+ };
57
+
58
+ /**
59
+ * Clean rendered html to be compared with DOM.
60
+ *
61
+ * @param html - HTML string
62
+ * @returns Cleaned HTML string
63
+ */
64
+ const cleanHTML = (html: string) => {
65
+ return (
66
+ html
67
+ // Multiple whitespaces
68
+ .replace(/\s\s+/gm, " ")
69
+ // Whitespace and or slash before html element end
70
+ .replace(/\s*\/*>/gm, ">")
71
+ // equals empty string
72
+ .replace(/(="")/gm, "")
73
+ );
74
+ };
75
+
76
+ beforeEach(() => {
77
+ jsPsych = initJsPsych();
78
+ display_el = document.getElementsByTagName("body")[0] as HTMLBodyElement;
79
+ video_config = new VideoConfigPlugin(jsPsych);
80
+ video_config["display_el"] = display_el;
81
+
82
+ // Set up parameters for rendering HTML template
83
+ html_params = {
84
+ webcam_container_id: video_config["webcam_container_id"],
85
+ reload_button_id_cam: video_config["reload_button_id_cam"],
86
+ camera_selection_id: video_config["camera_selection_id"],
87
+ mic_selection_id: video_config["mic_selection_id"],
88
+ step1_id: video_config["step1_id"],
89
+ step2_id: video_config["step2_id"],
90
+ step3_id: video_config["step3_id"],
91
+ step_complete_class: video_config["step_complete_class"],
92
+ step_complete_text: video_config["step_complete_text"],
93
+ reload_button_id_text: video_config["reload_button_id_text"],
94
+ next_button_id: video_config["next_button_id"],
95
+ chromeInitialPrompt: chromeInitialPrompt,
96
+ chromeAlwaysAllow: chromeAlwaysAllow,
97
+ chromePermissions: chromePermissions,
98
+ firefoxInitialPrompt: firefoxInitialPrompt,
99
+ firefoxChooseDevice: firefoxChooseDevice,
100
+ firefoxDevicesBlocked: firefoxDevicesBlocked,
101
+ troubleshooting_intro: "",
102
+ };
103
+
104
+ // Mocks for handling media streams and devices
105
+ const mic1 = {
106
+ deviceId: "mic1",
107
+ kind: "audioinput",
108
+ label: "",
109
+ groupId: "default",
110
+ } as MediaDeviceInfo;
111
+ const cam1 = {
112
+ deviceId: "cam1",
113
+ kind: "videoinput",
114
+ label: "",
115
+ groupId: "default",
116
+ } as MediaDeviceInfo;
117
+ const mic2 = {
118
+ deviceId: "mic2",
119
+ kind: "audioinput",
120
+ label: "",
121
+ groupId: "other",
122
+ } as MediaDeviceInfo;
123
+ const cam2 = {
124
+ deviceId: "cam2",
125
+ kind: "videoinput",
126
+ label: "",
127
+ groupId: "other",
128
+ } as MediaDeviceInfo;
129
+
130
+ devicesObj = { cam1, cam2, mic1, mic2 };
131
+
132
+ // Array format for devices returned from global.navigator.mediaDevices.enumerateDevices
133
+ devices = [cam1, cam2, mic1, mic2];
134
+ Object.defineProperty(global.navigator, "mediaDevices", {
135
+ writable: true,
136
+ value: {
137
+ ondevicechange: jest.fn(),
138
+ enumerateDevices: jest.fn(
139
+ () =>
140
+ new Promise<MediaDeviceInfo[]>((resolve) => {
141
+ resolve(devices);
142
+ }),
143
+ ),
144
+ },
145
+ });
146
+
147
+ // Object format for devices returned from getDeviceLists
148
+ returnedDeviceLists = {
149
+ cameras: [devicesObj.cam1, devicesObj.cam2],
150
+ mics: [devicesObj.mic1, devicesObj.mic2],
151
+ };
152
+ });
153
+
154
+ afterEach(() => {
155
+ jest.clearAllMocks();
156
+ // Reset the document body.
157
+ document.body.innerHTML = "";
158
+ });
159
+
160
+ test("Video config trial method sets up trial", () => {
161
+ const trial_info = {
162
+ troubleshooting_intro: "",
163
+ } as unknown as VideoConsentTrialType;
164
+
165
+ // Set up mocks for upstream functions called in trial method.
166
+ const addHtmlMock = jest
167
+ .spyOn(video_config, "addHtmlContent")
168
+ .mockImplementation(jest.fn());
169
+ const addEventListenerslMock = jest
170
+ .spyOn(video_config, "addEventListeners")
171
+ .mockImplementation(jest.fn());
172
+ const setupRecorderMock = jest
173
+ .spyOn(video_config, "setupRecorder")
174
+ .mockImplementation(jest.fn());
175
+
176
+ expect(addHtmlMock).toHaveBeenCalledTimes(0);
177
+ expect(addEventListenerslMock).toHaveBeenCalledTimes(0);
178
+ expect(setupRecorderMock).toHaveBeenCalledTimes(0);
179
+ expect(video_config["start_time"]).toBeNull();
180
+
181
+ video_config.trial(display_el, trial_info);
182
+
183
+ expect(global.navigator.mediaDevices.ondevicechange).toBe(
184
+ video_config["onDeviceChange"],
185
+ );
186
+ expect(addHtmlMock).toHaveBeenCalledTimes(1);
187
+ expect(addEventListenerslMock).toHaveBeenCalledTimes(1);
188
+ expect(setupRecorderMock).toHaveBeenCalledTimes(1);
189
+ expect(video_config["start_time"]).not.toBeNull();
190
+ expect(video_config["start_time"]).toBeGreaterThan(0);
191
+ });
192
+
193
+ test("Video config addHtmlContent loads template", () => {
194
+ expect(video_config["display_el"]?.innerHTML).toBe("");
195
+
196
+ // Render the template with the HTML parameters.
197
+ const rendered_trial_html = cleanHTML(
198
+ Handlebars.compile(videoConfig)(html_params),
199
+ );
200
+
201
+ // Run addHtmlContent to get the actual trial HTML.
202
+ video_config["addHtmlContent"]("");
203
+ const displayed_html = cleanHTML(document.body.innerHTML);
204
+
205
+ expect(displayed_html).toStrictEqual(rendered_trial_html);
206
+ });
207
+
208
+ test("Video config addHtmlContent loads template with custom troubleshooting text", () => {
209
+ expect(video_config["display_el"]?.innerHTML).toBe("");
210
+
211
+ // Render the template with a custom trial parameter.
212
+ const troubleshooting_intro = "Custom text.";
213
+ const html_params_custom_intro = {
214
+ ...html_params,
215
+ troubleshooting_intro,
216
+ };
217
+
218
+ // Remove new lines, indents (tabs or spaces), and empty HTML property values.
219
+ const rendered_trial_html = cleanHTML(
220
+ Handlebars.compile(videoConfig)(html_params_custom_intro),
221
+ );
222
+
223
+ // Get the actual trial HTML
224
+ video_config["addHtmlContent"](troubleshooting_intro);
225
+ const displayed_html = cleanHTML(document.body.innerHTML);
226
+
227
+ expect(displayed_html).toStrictEqual(rendered_trial_html);
228
+ });
229
+
230
+ test("Video config add event listeners", async () => {
231
+ expect(video_config["display_el"]?.innerHTML).toBe("");
232
+ video_config["addHtmlContent"]("");
233
+
234
+ // Get relevant elements (device selection elements tested separately)
235
+ const next_button_el = video_config["display_el"]?.querySelector(
236
+ `#${video_config["next_button_id"]}`,
237
+ ) as HTMLButtonElement;
238
+ const reload_button_els = video_config["display_el"]?.querySelectorAll(
239
+ `#${video_config["reload_button_id_cam"]}, #${video_config["reload_button_id_text"]}`,
240
+ ) as NodeListOf<HTMLButtonElement>;
241
+ const acc_button_els = document.getElementsByClassName(
242
+ "lookit-jspsych-accordion",
243
+ ) as HTMLCollectionOf<HTMLButtonElement>;
244
+
245
+ // Mock event-related callback functions.
246
+ const nextBtnClickMock = jest
247
+ .spyOn(video_config, "nextButtonClick")
248
+ .mockImplementation(jest.fn());
249
+ const reloadBtnClickMock = jest
250
+ .spyOn(video_config, "reloadButtonClick")
251
+ .mockImplementation(jest.fn());
252
+
253
+ video_config["addEventListeners"]();
254
+
255
+ // Next button is disabled when the page loads - it must be enabled before it will accept a click event.
256
+ video_config["enableNext"](true);
257
+ expect(nextBtnClickMock).toHaveBeenCalledTimes(0);
258
+ await clickTarget(next_button_el);
259
+ expect(nextBtnClickMock).toHaveBeenCalledTimes(1);
260
+
261
+ // Reload buttons.
262
+ expect(reloadBtnClickMock.mock.calls).toHaveLength(0);
263
+ for (let i = 0; i < reload_button_els.length; i++) {
264
+ await clickTarget(reload_button_els[i]);
265
+ }
266
+ expect(reloadBtnClickMock.mock.calls).toHaveLength(reload_button_els.length);
267
+
268
+ // Buttons and content (panel) for accordion section (troubleshooting)
269
+ for (let i = 0; i < acc_button_els.length; i++) {
270
+ const this_btn = acc_button_els[i];
271
+ const this_panel = this_btn.nextElementSibling as HTMLDivElement;
272
+
273
+ // Initial state is no active class and panel display: none
274
+ expect(this_btn.classList.contains("active")).toBe(false);
275
+ expect(this_panel.style.display).toBe("none");
276
+
277
+ await clickTarget(this_btn);
278
+
279
+ // Button active and panel display: block
280
+ expect(this_btn.classList.contains("active")).toBe(true);
281
+ expect(this_panel.style.display).toBe("block");
282
+
283
+ await clickTarget(this_btn);
284
+
285
+ // Clicking it again returns it to initial state.
286
+ expect(this_btn.classList.contains("active")).toBe(false);
287
+ expect(this_panel.style.display).toBe("none");
288
+ }
289
+ });
290
+
291
+ test("Video config enable next", () => {
292
+ expect(video_config["display_el"]?.innerHTML).toBe("");
293
+
294
+ video_config["addHtmlContent"]("");
295
+ const next_button_el = video_config["display_el"]?.querySelector(
296
+ `#${video_config["next_button_id"]}`,
297
+ ) as HTMLButtonElement;
298
+
299
+ // When trial first loads, next button should exist but be disabled and no 'step_complete_class'
300
+ expect(next_button_el).toBeTruthy();
301
+ expect(
302
+ next_button_el.classList.contains(video_config["step_complete_class"]),
303
+ ).toBe(false);
304
+ expect(next_button_el.disabled).toBe(true);
305
+
306
+ // Calling with 'true' enables the button and adds the class.
307
+ video_config["enableNext"](true);
308
+ expect(
309
+ next_button_el.classList.contains(video_config["step_complete_class"]),
310
+ ).toBe(true);
311
+ expect(next_button_el.disabled).toBe(false);
312
+
313
+ // Calling with 'false' sets everything back.
314
+ video_config["enableNext"](false);
315
+ expect(
316
+ next_button_el.classList.contains(video_config["step_complete_class"]),
317
+ ).toBe(false);
318
+ expect(next_button_el.disabled).toBe(true);
319
+ });
320
+
321
+ test("Video config update instructions", () => {
322
+ expect(video_config["display_el"]?.innerHTML).toBe("");
323
+ video_config["addHtmlContent"]("");
324
+
325
+ // Get the relevant elements from the instructions section.
326
+ const step1_id = video_config["step1_id"];
327
+ const step2_id = video_config["step2_id"];
328
+ const step3_id = video_config["step3_id"];
329
+ const step1_span = video_config["display_el"]?.querySelector(
330
+ `#${step1_id}-span`,
331
+ ) as HTMLSpanElement;
332
+ const step1_para = video_config["display_el"]?.querySelector(
333
+ `#${step1_id}-paragraph`,
334
+ ) as HTMLParagraphElement;
335
+ const step2_span = video_config["display_el"]?.querySelector(
336
+ `#${step2_id}-span`,
337
+ ) as HTMLSpanElement;
338
+ const step2_para = video_config["display_el"]?.querySelector(
339
+ `#${step2_id}-paragraph`,
340
+ ) as HTMLParagraphElement;
341
+ const step3_span = video_config["display_el"]?.querySelector(
342
+ `#${step3_id}-span`,
343
+ ) as HTMLSpanElement;
344
+ const step3_para = video_config["display_el"]?.querySelector(
345
+ `#${step3_id}-paragraph`,
346
+ ) as HTMLParagraphElement;
347
+
348
+ // For each step 1-3:
349
+ // - calling with 'true' should make the span element visible and set the paragraph text color to gray.
350
+ // - calling with 'false' should make the span hidden and set the paragraph text color to black.
351
+ // The initial state is the same as with 'false', except that the text color is unset (inherits).
352
+ expect(step1_span.style.visibility).toBe("hidden");
353
+ video_config["updateInstructions"](1, true);
354
+ expect(step1_span.style.visibility).toBe("visible");
355
+ expect(step1_para.style.color).toBe("gray");
356
+ video_config["updateInstructions"](1, false);
357
+ expect(step1_span.style.visibility).toBe("hidden");
358
+ expect(step1_para.style.color).toBe("black");
359
+
360
+ expect(step2_span.style.visibility).toBe("hidden");
361
+ video_config["updateInstructions"](2, true);
362
+ expect(step2_span.style.visibility).toBe("visible");
363
+ expect(step2_para.style.color).toBe("gray");
364
+ video_config["updateInstructions"](2, false);
365
+ expect(step2_span.style.visibility).toBe("hidden");
366
+ expect(step2_para.style.color).toBe("black");
367
+
368
+ expect(step3_span.style.visibility).toBe("hidden");
369
+ video_config["updateInstructions"](3, true);
370
+ expect(step3_span.style.visibility).toBe("visible");
371
+ expect(step3_para.style.color).toBe("gray");
372
+ video_config["updateInstructions"](3, false);
373
+ expect(step3_span.style.visibility).toBe("hidden");
374
+ expect(step3_para.style.color).toBe("black");
375
+ });
376
+
377
+ test("Video config update errors", () => {
378
+ expect(video_config["display_el"]?.innerHTML).toBe("");
379
+ video_config["addHtmlContent"]("");
380
+ const error_msg_div = video_config["display_el"]?.querySelector(
381
+ `#${video_config["error_msg_div_id"]}`,
382
+ ) as HTMLDivElement;
383
+
384
+ // Error/info message div is empty when the page first loads.
385
+ expect(error_msg_div.innerHTML).toStrictEqual("");
386
+
387
+ const test_msg = "Test message.";
388
+ video_config["updateErrors"](test_msg);
389
+
390
+ expect(error_msg_div.innerHTML).toStrictEqual(test_msg);
391
+ });
392
+
393
+ test("Video config reload button click", async () => {
394
+ expect(video_config["hasReloaded"]).toBe(false);
395
+
396
+ video_config["addHtmlContent"]("");
397
+
398
+ const reload_button_els = video_config["display_el"]?.querySelectorAll(
399
+ `#${video_config["reload_button_id_cam"]}, #${video_config["reload_button_id_text"]}`,
400
+ ) as NodeListOf<HTMLButtonElement>;
401
+
402
+ // Mock upstream function calls.
403
+ const updateInstructionskMock = jest
404
+ .spyOn(video_config, "updateInstructions")
405
+ .mockImplementation(jest.fn());
406
+ const destroyMock = jest
407
+ .spyOn(video_config, "destroyRecorder")
408
+ .mockImplementation(jest.fn());
409
+ const setupRecorderMock = jest
410
+ .spyOn(video_config, "setupRecorder")
411
+ .mockImplementation(jest.fn());
412
+
413
+ video_config["addEventListeners"]();
414
+
415
+ expect(updateInstructionskMock).toHaveBeenCalledTimes(0);
416
+ expect(destroyMock).toHaveBeenCalledTimes(0);
417
+ expect(setupRecorderMock).toHaveBeenCalledTimes(0);
418
+
419
+ await clickTarget(reload_button_els[0]);
420
+
421
+ expect(updateInstructionskMock).toHaveBeenCalledTimes(1);
422
+ expect(destroyMock).toHaveBeenCalledTimes(1);
423
+ expect(setupRecorderMock).toHaveBeenCalledTimes(1);
424
+ expect(video_config["hasReloaded"]).toBe(true);
425
+
426
+ await clickTarget(reload_button_els[1]);
427
+
428
+ expect(updateInstructionskMock).toHaveBeenCalledTimes(2);
429
+ expect(destroyMock).toHaveBeenCalledTimes(2);
430
+ expect(setupRecorderMock).toHaveBeenCalledTimes(2);
431
+ expect(video_config["hasReloaded"]).toBe(true);
432
+ });
433
+
434
+ test("Video config updateDeviceSelection", () => {
435
+ video_config["addHtmlContent"]("");
436
+ const cam_selection_el = video_config["display_el"]?.querySelector(
437
+ `#${video_config["camera_selection_id"]}`,
438
+ ) as HTMLSelectElement;
439
+ const mic_selection_el = video_config["display_el"]?.querySelector(
440
+ `#${video_config["mic_selection_id"]}`,
441
+ ) as HTMLSelectElement;
442
+
443
+ expect(cam_selection_el).not.toBeUndefined();
444
+ expect(cam_selection_el.innerHTML).toBe("");
445
+ expect(mic_selection_el).not.toBeUndefined();
446
+ expect(mic_selection_el.innerHTML).toBe("");
447
+
448
+ // Populates the select elements with new device info.
449
+ video_config["updateDeviceSelection"]({
450
+ cameras: [devicesObj.cam1],
451
+ mics: [devicesObj.mic1],
452
+ });
453
+ expect(mic_selection_el.innerHTML).toContain(
454
+ `<option value="${devicesObj.mic1.deviceId}"></option>`,
455
+ );
456
+ expect(cam_selection_el.innerHTML).toContain(
457
+ `<option value="${devicesObj.cam1.deviceId}"></option>`,
458
+ );
459
+
460
+ // Also clears old options in select elements that are no longer in the devices lists.
461
+ video_config["updateDeviceSelection"]({
462
+ cameras: [devicesObj.cam2],
463
+ mics: [devicesObj.mic2],
464
+ });
465
+ expect(mic_selection_el.innerHTML).not.toContain(
466
+ `<option value="${devicesObj.mic1.deviceId}"></option>`,
467
+ );
468
+ expect(cam_selection_el.innerHTML).not.toContain(
469
+ `<option value="${devicesObj.cam1.deviceId}"></option>`,
470
+ );
471
+ expect(mic_selection_el.innerHTML).toContain(
472
+ `<option value="${devicesObj.mic2.deviceId}"></option>`,
473
+ );
474
+ expect(cam_selection_el.innerHTML).toContain(
475
+ `<option value="${devicesObj.cam2.deviceId}"></option>`,
476
+ );
477
+
478
+ // If a device was previously selected (via setDevices), and is in the updated list, it is automatically selected again.
479
+ video_config["camId"] = devicesObj.cam2.deviceId;
480
+ video_config["micId"] = devicesObj.mic2.deviceId;
481
+ cam_selection_el.value = devicesObj.cam2.deviceId;
482
+ mic_selection_el.value = devicesObj.mic2.deviceId;
483
+ video_config["updateDeviceSelection"]({
484
+ cameras: [devicesObj.cam1, devicesObj.cam2],
485
+ mics: [devicesObj.mic1, devicesObj.mic2],
486
+ });
487
+ expect(mic_selection_el.innerHTML).toContain(
488
+ `<option value="${devicesObj.mic1.deviceId}"></option>`,
489
+ );
490
+ expect(cam_selection_el.innerHTML).toContain(
491
+ `<option value="${devicesObj.cam1.deviceId}"></option>`,
492
+ );
493
+ expect(mic_selection_el.innerHTML).toContain(
494
+ `<option value="${devicesObj.mic2.deviceId}"></option>`,
495
+ );
496
+ expect(cam_selection_el.innerHTML).toContain(
497
+ `<option value="${devicesObj.cam2.deviceId}"></option>`,
498
+ );
499
+ expect(cam_selection_el.value).toBe(devicesObj.cam2.deviceId);
500
+ expect(mic_selection_el.value).toBe(devicesObj.mic2.deviceId);
501
+
502
+ // If a previously-selected device is no longer in the list, there is no error.
503
+ video_config["updateDeviceSelection"]({
504
+ cameras: [devicesObj.cam1],
505
+ mics: [devicesObj.mic1],
506
+ });
507
+ expect(mic_selection_el.innerHTML).toContain(
508
+ `<option value="${devicesObj.mic1.deviceId}"></option>`,
509
+ );
510
+ expect(cam_selection_el.innerHTML).toContain(
511
+ `<option value="${devicesObj.cam1.deviceId}"></option>`,
512
+ );
513
+ expect(mic_selection_el.innerHTML).not.toContain(
514
+ `<option value="${devicesObj.mic2.deviceId}"></option>`,
515
+ );
516
+ expect(cam_selection_el.innerHTML).not.toContain(
517
+ `<option value="${devicesObj.cam2.deviceId}"></option>`,
518
+ );
519
+ expect(cam_selection_el.value).toBe(devicesObj.cam1.deviceId);
520
+ expect(mic_selection_el.value).toBe(devicesObj.mic1.deviceId);
521
+ });
522
+
523
+ test("Video config end trial", () => {
524
+ // Set up some data values
525
+ const response = { rt: 1000 };
526
+ const camId = "cam Id";
527
+ const micId = "mic Id";
528
+ video_config["response"] = response;
529
+ video_config["camId"] = camId;
530
+ video_config["micId"] = micId;
531
+
532
+ // Ending the trial should trigger finishTrial with the data.
533
+ video_config["endTrial"]();
534
+ expect(jsPsych.finishTrial).toHaveBeenCalledWith({
535
+ rt: response.rt,
536
+ camId,
537
+ micId,
538
+ });
539
+ });
540
+
541
+ test("Video config setupRecorder", async () => {
542
+ // Mock the upstream function calls.
543
+ const updateInstructionsMock = jest
544
+ .spyOn(video_config, "updateInstructions")
545
+ .mockImplementation(jest.fn());
546
+ const updateErrorsMock = jest
547
+ .spyOn(video_config, "updateErrors")
548
+ .mockImplementation(jest.fn());
549
+ const requestPermissionMock = jest
550
+ .spyOn(video_config, "requestPermission")
551
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
552
+ .mockImplementation((constraints: MediaStreamConstraints) =>
553
+ Promise.resolve(jsPsych.pluginAPI.getCameraRecorder().stream),
554
+ );
555
+ const getDeviceListsMock = jest
556
+ .spyOn(video_config, "getDeviceLists")
557
+ .mockImplementation(
558
+ (
559
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
560
+ include_audio?: boolean | undefined,
561
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
562
+ include_camera?: boolean | undefined,
563
+ ) => {
564
+ return Promise.resolve(returnedDeviceLists);
565
+ },
566
+ );
567
+ const updateDeviceSelectionMock = jest
568
+ .spyOn(video_config, "updateDeviceSelection")
569
+ .mockImplementation(jest.fn());
570
+ const setDevicesMock = jest
571
+ .spyOn(video_config, "setDevices")
572
+ .mockImplementation(jest.fn());
573
+ const runStreamChecksMock = jest
574
+ .spyOn(video_config, "runStreamChecks")
575
+ .mockImplementation(jest.fn());
576
+
577
+ await video_config["setupRecorder"]();
578
+
579
+ // Updates the Instructions - resets the "completed" status of steps 1 and 3.
580
+ expect(updateInstructionsMock).toHaveBeenCalledTimes(2);
581
+ expect(updateInstructionsMock.mock.calls[0]).toStrictEqual([1, false]);
582
+ expect(updateInstructionsMock.mock.calls[1]).toStrictEqual([3, false]);
583
+ // Adds error message when waiting for stream access, then clears the message after access is granted.
584
+ expect(updateErrorsMock).toHaveBeenCalledTimes(2);
585
+ expect(updateErrorsMock.mock.calls[0]).toStrictEqual([
586
+ video_config["waiting_for_access_msg"],
587
+ ]);
588
+ expect(updateErrorsMock.mock.calls[1]).toStrictEqual([""]);
589
+ // Needs to request permissions before it can get device lists.
590
+ expect(requestPermissionMock).toHaveBeenCalledWith({
591
+ video: true,
592
+ audio: true,
593
+ });
594
+ // Gets device lists.
595
+ expect(getDeviceListsMock).toHaveBeenCalledTimes(1);
596
+ // Updates selection elements with available devices.
597
+ expect(updateDeviceSelectionMock).toHaveBeenCalledTimes(1);
598
+ expect(updateDeviceSelectionMock).toHaveBeenCalledWith(returnedDeviceLists);
599
+ // Set the current devices.
600
+ expect(setDevicesMock).toHaveBeenCalledTimes(1);
601
+ // Run stream checks on current devices.
602
+ expect(runStreamChecksMock).toHaveBeenCalledTimes(1);
603
+ });
604
+
605
+ test("Video config setDevices", async () => {
606
+ video_config["addHtmlContent"]("");
607
+ const cam_selection_el = video_config["display_el"]?.querySelector(
608
+ `#${video_config["camera_selection_id"]}`,
609
+ ) as HTMLSelectElement;
610
+ const mic_selection_el = video_config["display_el"]?.querySelector(
611
+ `#${video_config["mic_selection_id"]}`,
612
+ ) as HTMLSelectElement;
613
+ const next_button_el = video_config["display_el"]?.querySelector(
614
+ `#${video_config["next_button_id"]}`,
615
+ ) as HTMLButtonElement;
616
+ video_config["recorder"] = new Recorder(jsPsych);
617
+
618
+ // Populate the selection elements with device options.
619
+ video_config["updateDeviceSelection"]({
620
+ cameras: [devicesObj.cam1, devicesObj.cam2],
621
+ mics: [devicesObj.mic1, devicesObj.mic2],
622
+ });
623
+
624
+ // Select some devices.
625
+ cam_selection_el.value = devicesObj.cam2.deviceId;
626
+ mic_selection_el.value = devicesObj.mic2.deviceId;
627
+
628
+ // Setup upstream mocks.
629
+ const enableNextMock = jest
630
+ .spyOn(video_config, "enableNext")
631
+ .mockImplementation(jest.fn());
632
+ const updateInstructionsMock = jest
633
+ .spyOn(video_config, "updateInstructions")
634
+ .mockImplementation(jest.fn());
635
+ const requestPermissionMock = jest
636
+ .spyOn(video_config, "requestPermission")
637
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
638
+ .mockImplementation((constraints: MediaStreamConstraints) =>
639
+ Promise.resolve(jsPsych.pluginAPI.getCameraRecorder().stream),
640
+ );
641
+ const createInitializeRecorderMock = jest.spyOn(
642
+ video_config,
643
+ "initializeAndCreateRecorder",
644
+ );
645
+
646
+ expect(next_button_el.disabled).toBe(true);
647
+ expect(video_config["camId"]).toStrictEqual("");
648
+ expect(video_config["micId"]).toStrictEqual("");
649
+
650
+ await video_config["setDevices"]();
651
+
652
+ // Disables next button
653
+ expect(enableNextMock).toHaveBeenCalledTimes(1);
654
+ expect(enableNextMock).toHaveBeenCalledWith(false);
655
+ expect(next_button_el.disabled).toBe(true);
656
+ // Updates instructions step 3 (mic check)
657
+ expect(updateInstructionsMock).toHaveBeenCalledTimes(1);
658
+ expect(updateInstructionsMock).toHaveBeenCalledWith(3, false);
659
+ // Sets this.camId and this.micId values based on selection elements.
660
+ expect(video_config["camId"]).toStrictEqual(devicesObj.cam2.deviceId);
661
+ expect(video_config["micId"]).toStrictEqual(devicesObj.mic2.deviceId);
662
+ // Requests permissions for those specific devices
663
+ expect(requestPermissionMock).toHaveBeenCalledWith({
664
+ video: { deviceId: devicesObj.cam2.deviceId },
665
+ audio: { deviceId: devicesObj.mic2.deviceId },
666
+ });
667
+ // Calls initializeAndCreateRecorder with the returned stream
668
+ expect(createInitializeRecorderMock).toHaveBeenCalledWith(
669
+ jsPsych.pluginAPI.getCameraRecorder().stream,
670
+ );
671
+
672
+ // Now change the device selections - values should update accordingly.
673
+ cam_selection_el.value = devicesObj.cam1.deviceId;
674
+ mic_selection_el.value = devicesObj.mic1.deviceId;
675
+
676
+ await video_config["setDevices"]();
677
+ expect(video_config["camId"]).toStrictEqual(devicesObj.cam1.deviceId);
678
+ expect(video_config["micId"]).toStrictEqual(devicesObj.mic1.deviceId);
679
+ expect(requestPermissionMock).toHaveBeenCalledWith({
680
+ video: { deviceId: devicesObj.cam1.deviceId },
681
+ audio: { deviceId: devicesObj.mic1.deviceId },
682
+ });
683
+ });
684
+
685
+ test("Video config runStreamChecks throws NoStreamAccess", () => {
686
+ video_config["addHtmlContent"]("");
687
+ video_config["recorder"] = new Recorder(initJsPsych());
688
+ jest
689
+ .spyOn(jsPsych.pluginAPI, "getCameraRecorder")
690
+ // @ts-expect-error - jsPsych says the return value should be MediaRecorder, but in fact it's MediaRecorder or null.
691
+ .mockImplementationOnce(() => null);
692
+ // When the recorder has not been initialized with jsPsych.
693
+ expect(async () => await video_config["runStreamChecks"]()).rejects.toThrow(
694
+ NoStreamError,
695
+ );
696
+
697
+ expect(jsPsych.pluginAPI.getCameraRecorder).toBeTruthy();
698
+
699
+ // When the Recorder has not been created.
700
+ video_config["recorder"] = null;
701
+ expect(async () => await video_config["runStreamChecks"]()).rejects.toThrow(
702
+ NoStreamError,
703
+ );
704
+ });
705
+
706
+ test("Video config runStreamChecks throws Mic Check error", async () => {
707
+ video_config["addHtmlContent"]("");
708
+ video_config["recorder"] = new Recorder(jsPsych);
709
+
710
+ // Setup upstream mocks.
711
+ Recorder.prototype.insertWebcamFeed = jest.fn();
712
+ video_config["updateInstructions"] = jest.fn();
713
+ const updateErrorsMock = jest
714
+ .spyOn(video_config, "updateErrors")
715
+ .mockImplementation(jest.fn());
716
+ // Mock an error during the mic check setup.
717
+ const mockError = jest.fn(() => {
718
+ const promise = new Promise<void>(() => {
719
+ throw "Mic check problem";
720
+ });
721
+ promise.catch(() => null); // Prevent an uncaught error here so that it propogates to the catch block.
722
+ return promise;
723
+ });
724
+ video_config["checkMic"] = mockError;
725
+
726
+ await expect(video_config["runStreamChecks"]()).rejects.toThrow(
727
+ "Mic check problem",
728
+ );
729
+
730
+ // If the recorder mic check throws an error, then the plugin should update the error message and throw the error.
731
+ expect(updateErrorsMock).toHaveBeenCalledTimes(2);
732
+ expect(updateErrorsMock.mock.calls[0]).toStrictEqual([
733
+ video_config["checking_mic_msg"],
734
+ ]);
735
+ expect(updateErrorsMock.mock.calls[1]).toStrictEqual([
736
+ video_config["setup_problem_msg"],
737
+ ]);
738
+ });
739
+
740
+ test("Video config runStreamChecks", async () => {
741
+ video_config["addHtmlContent"]("");
742
+ video_config["recorder"] = new Recorder(jsPsych);
743
+
744
+ // Setup upstream mocks.
745
+ const insertWebcamMock = jest
746
+ .spyOn(Recorder.prototype, "insertWebcamFeed")
747
+ .mockImplementation(jest.fn());
748
+ const updateInstructionsMock = jest
749
+ .spyOn(video_config, "updateInstructions")
750
+ .mockImplementation(jest.fn());
751
+ const updateErrorsMock = jest
752
+ .spyOn(video_config, "updateErrors")
753
+ .mockImplementation(jest.fn());
754
+ const checkMicMock = jest
755
+ .spyOn(video_config, "checkMic")
756
+ .mockImplementation(jest.fn(() => Promise.resolve()));
757
+
758
+ await video_config["runStreamChecks"]();
759
+
760
+ expect(insertWebcamMock).toHaveBeenCalledTimes(1);
761
+ // Called twice: first for passing the stream access check, then for passing the mic input check.
762
+ expect(updateInstructionsMock).toHaveBeenCalledTimes(2);
763
+ expect(updateInstructionsMock.mock.calls[0]).toStrictEqual([1, true]);
764
+ expect(updateInstructionsMock.mock.calls[1]).toStrictEqual([3, true]);
765
+ // Called twice: first with "checking mic" message, then to clear that message.
766
+ expect(updateErrorsMock).toHaveBeenCalledTimes(2);
767
+ expect(updateErrorsMock.mock.calls[0]).toStrictEqual([
768
+ video_config["checking_mic_msg"],
769
+ ]);
770
+ expect(updateErrorsMock.mock.calls[1]).toStrictEqual([""]);
771
+ expect(checkMicMock).toHaveBeenCalledTimes(1);
772
+ });
773
+
774
+ test("Video config runStreamChecks enables next button when all checks have passed", async () => {
775
+ video_config["addHtmlContent"]("");
776
+ video_config["recorder"] = new Recorder(jsPsych);
777
+
778
+ // Setup upstream mocks.
779
+ Recorder.prototype.insertWebcamFeed = jest.fn();
780
+ video_config["updateInstructions"] = jest.fn();
781
+ video_config["updateErrors"] = jest.fn();
782
+ video_config["checkMic"] = jest.fn(() => Promise.resolve());
783
+ const enableNextMock = jest
784
+ .spyOn(video_config, "enableNext")
785
+ .mockImplementation(jest.fn());
786
+
787
+ video_config["hasReloaded"] = true;
788
+
789
+ await video_config["runStreamChecks"]();
790
+
791
+ expect(enableNextMock).toHaveBeenCalledTimes(1);
792
+ expect(enableNextMock).toHaveBeenCalledWith(true);
793
+ });
794
+
795
+ test("Video config onDeviceChange event listener", async () => {
796
+ video_config["recorder"] = new Recorder(jsPsych);
797
+ // Mock the MediaDevices API
798
+ const mockMediaDevices = {
799
+ ondevicechange: null as ((this: MediaDevices, ev: Event) => void) | null,
800
+ };
801
+ global.navigator["mediaDevices"] = mockMediaDevices;
802
+
803
+ // The onDeviceChange event handler is set up in the plugin's trial method.
804
+ // Set up mocks for all other functions called in trial method.
805
+ video_config["addHtmlContent"] = jest.fn();
806
+ video_config["addEventListeners"] = jest.fn();
807
+ video_config["setupRecorder"] = jest.fn();
808
+
809
+ // Setup upstream mocks for functions called inside onDeviceChange.
810
+ const getDeviceListsMock = jest
811
+ .spyOn(video_config, "getDeviceLists")
812
+ .mockImplementation(
813
+ (
814
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
815
+ include_audio?: boolean | undefined,
816
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
817
+ include_camera?: boolean | undefined,
818
+ ) => {
819
+ return Promise.resolve(returnedDeviceLists);
820
+ },
821
+ );
822
+ const updateDeviceSelectionMock = jest
823
+ .spyOn(video_config, "updateDeviceSelection")
824
+ .mockImplementation(jest.fn());
825
+
826
+ // Call trial method to set up onDeviceChange.
827
+ expect(navigator.mediaDevices.ondevicechange).toBeNull();
828
+ const trial_info = {
829
+ troubleshooting_intro: "",
830
+ } as unknown as VideoConsentTrialType;
831
+ video_config.trial(display_el, trial_info);
832
+ expect(navigator.mediaDevices.ondevicechange).not.toBeNull();
833
+
834
+ // Simulate a device change event.
835
+ const event = new Event("devicechange");
836
+ await navigator.mediaDevices.ondevicechange?.(event);
837
+
838
+ expect(getDeviceListsMock).toHaveBeenCalledTimes(1);
839
+ expect(updateDeviceSelectionMock).toHaveBeenCalledWith(returnedDeviceLists);
840
+ });
841
+
842
+ test("Video config device selection element on change event listener", async () => {
843
+ video_config["addHtmlContent"]("");
844
+
845
+ // Mock upstream functions called by device selection change event listener.
846
+ const setDevicesMock = jest
847
+ .spyOn(video_config, "setDevices")
848
+ .mockImplementation(jest.fn(() => Promise.resolve()));
849
+ const runStreamChecksMock = jest
850
+ .spyOn(video_config, "runStreamChecks")
851
+ .mockImplementation(jest.fn(() => Promise.resolve()));
852
+
853
+ // Get device selection elements.
854
+ const device_selection_els = video_config["display_el"]?.querySelectorAll(
855
+ ".lookit-jspsych-device-selection",
856
+ ) as NodeListOf<HTMLSelectElement>;
857
+
858
+ video_config["addEventListeners"]();
859
+
860
+ expect(setDevicesMock.mock.calls).toHaveLength(0);
861
+ expect(runStreamChecksMock.mock.calls).toHaveLength(0);
862
+
863
+ // Simulate device selection change events.
864
+ for (let i = 0; i < device_selection_els.length; i++) {
865
+ const changeEvent = new Event("change", { bubbles: true });
866
+ await device_selection_els[i].dispatchEvent(changeEvent);
867
+ }
868
+
869
+ expect(setDevicesMock.mock.calls).toHaveLength(device_selection_els.length);
870
+ expect(runStreamChecksMock.mock.calls).toHaveLength(
871
+ device_selection_els.length,
872
+ );
873
+ });
874
+
875
+ test("Video config next button click", async () => {
876
+ video_config["start_time"] = 1;
877
+ const perfNowMock = jest
878
+ .spyOn(performance, "now")
879
+ .mockImplementation(jest.fn().mockReturnValue(5));
880
+ const destroyMock = jest
881
+ .spyOn(video_config, "destroyRecorder")
882
+ .mockImplementation(jest.fn().mockReturnValue(Promise.resolve()));
883
+ const endMock = jest
884
+ .spyOn(video_config, "endTrial")
885
+ .mockImplementation(jest.fn());
886
+
887
+ await video_config["nextButtonClick"]();
888
+
889
+ expect(perfNowMock).toHaveBeenCalledTimes(1);
890
+ expect(video_config["response"]).toHaveProperty("rt");
891
+ expect(video_config["response"]["rt"]).toBe(4);
892
+ expect(destroyMock).toHaveBeenCalledTimes(1);
893
+ expect(endMock.mock.calls).toHaveLength(1);
894
+
895
+ video_config["start_time"] = null;
896
+
897
+ await video_config["nextButtonClick"]();
898
+
899
+ expect(video_config["response"]).toHaveProperty("rt");
900
+ expect(video_config["response"]["rt"]).toBeNull();
901
+ });
902
+
903
+ test("Video config destroyRecorder", () => {
904
+ const recorderStopTracksSpy = jest.spyOn(Recorder.prototype, "stopTracks");
905
+ video_config["recorder"] = new Recorder(jsPsych);
906
+ const enableNextMock = jest
907
+ .spyOn(video_config, "enableNext")
908
+ .mockImplementation(jest.fn());
909
+ const updateInstructionsMock = jest
910
+ .spyOn(video_config, "updateInstructions")
911
+ .mockImplementation(jest.fn());
912
+
913
+ expect(video_config["recorder"]).not.toBeNull();
914
+
915
+ video_config["destroyRecorder"]();
916
+
917
+ // Should stop the tracks, set the recorder to null, disable the next button,
918
+ // and update the instructions (checks 1/3 not passed).
919
+ expect(recorderStopTracksSpy).toHaveBeenCalledTimes(1);
920
+ expect(video_config["recorder"]).toBeNull();
921
+ expect(enableNextMock).toHaveBeenCalledTimes(1);
922
+ expect(enableNextMock).toHaveBeenCalledWith(false);
923
+ expect(updateInstructionsMock).toHaveBeenCalledTimes(2);
924
+ expect(updateInstructionsMock.mock.calls[0]).toStrictEqual([3, false]);
925
+ expect(updateInstructionsMock.mock.calls[1]).toStrictEqual([1, false]);
926
+ });
927
+
928
+ test("Video config destroyRecorder with no recorder", () => {
929
+ const recorderStopTracksSpy = jest.spyOn(Recorder.prototype, "stopTracks");
930
+ const enableNextMock = jest
931
+ .spyOn(video_config, "enableNext")
932
+ .mockImplementation(jest.fn());
933
+ const updateInstructionsMock = jest
934
+ .spyOn(video_config, "updateInstructions")
935
+ .mockImplementation(jest.fn());
936
+
937
+ expect(video_config["recorder"]).toBeNull();
938
+
939
+ video_config["destroyRecorder"]();
940
+
941
+ expect(recorderStopTracksSpy).not.toHaveBeenCalled();
942
+ expect(video_config["recorder"]).toBeNull();
943
+ expect(enableNextMock).toHaveBeenCalledTimes(1);
944
+ expect(enableNextMock).toHaveBeenCalledWith(false);
945
+ expect(updateInstructionsMock).toHaveBeenCalledTimes(2);
946
+ expect(updateInstructionsMock.mock.calls[0]).toStrictEqual([3, false]);
947
+ expect(updateInstructionsMock.mock.calls[1]).toStrictEqual([1, false]);
948
+ });
949
+
950
+ test("Video config requestPermission", async () => {
951
+ const stream = { fake: "stream" } as unknown as MediaStream;
952
+ const mockGetUserMedia = jest.fn(
953
+ () =>
954
+ new Promise<MediaStream>((resolve) => {
955
+ resolve(stream);
956
+ }),
957
+ );
958
+ Object.defineProperty(global.navigator, "mediaDevices", {
959
+ writable: true,
960
+ value: {
961
+ getUserMedia: mockGetUserMedia,
962
+ },
963
+ });
964
+ const constraints = { video: true, audio: true };
965
+
966
+ const returnedStream = await video_config.requestPermission(constraints);
967
+ expect(returnedStream).toStrictEqual(stream);
968
+ expect(global.navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith(
969
+ constraints,
970
+ );
971
+ });
972
+
973
+ test("Video config getDeviceLists", async () => {
974
+ const mic1 = {
975
+ deviceId: "mic1",
976
+ kind: "audioinput",
977
+ label: "",
978
+ groupId: "default",
979
+ } as MediaDeviceInfo;
980
+ const cam1 = {
981
+ deviceId: "cam1",
982
+ kind: "videoinput",
983
+ label: "",
984
+ groupId: "default",
985
+ } as MediaDeviceInfo;
986
+ const mic2 = {
987
+ deviceId: "mic2",
988
+ kind: "audioinput",
989
+ label: "",
990
+ groupId: "other",
991
+ } as MediaDeviceInfo;
992
+ const cam2 = {
993
+ deviceId: "cam2",
994
+ kind: "videoinput",
995
+ label: "",
996
+ groupId: "other",
997
+ } as MediaDeviceInfo;
998
+
999
+ // Returns the mic/cam devices from navigator.mediaDevices.enumerateDevices as an object with 'cameras' and 'mics' (arrays of media device info objects).
1000
+ const devices = [mic1, mic2, cam1, cam2];
1001
+ Object.defineProperty(global.navigator, "mediaDevices", {
1002
+ writable: true,
1003
+ value: {
1004
+ enumerateDevices: jest.fn(
1005
+ () =>
1006
+ new Promise<MediaDeviceInfo[]>((resolve) => {
1007
+ resolve(devices);
1008
+ }),
1009
+ ),
1010
+ },
1011
+ });
1012
+
1013
+ const returnedDevices = await video_config.getDeviceLists();
1014
+ expect(global.navigator.mediaDevices.enumerateDevices).toHaveBeenCalledTimes(
1015
+ 1,
1016
+ );
1017
+ expect(returnedDevices).toHaveProperty("cameras");
1018
+ expect(returnedDevices).toHaveProperty("mics");
1019
+ expect(returnedDevices.cameras.sort()).toStrictEqual([cam1, cam2].sort());
1020
+ expect(returnedDevices.mics.sort()).toStrictEqual([mic1, mic2].sort());
1021
+
1022
+ // Removes duplicate devices and handles empty device categories.
1023
+ const devices_duplicate = [mic1, mic1, mic1];
1024
+ Object.defineProperty(global.navigator, "mediaDevices", {
1025
+ writable: true,
1026
+ value: {
1027
+ enumerateDevices: jest.fn(
1028
+ () =>
1029
+ new Promise<MediaDeviceInfo[]>((resolve) => {
1030
+ resolve(devices_duplicate);
1031
+ }),
1032
+ ),
1033
+ },
1034
+ });
1035
+
1036
+ const returnedDevicesDuplicates = await video_config.getDeviceLists();
1037
+ expect(returnedDevicesDuplicates.cameras).toStrictEqual([]);
1038
+ expect(returnedDevicesDuplicates.mics).toStrictEqual([mic1]);
1039
+ });
1040
+
1041
+ test("Video config onMicActivityLevel", () => {
1042
+ type micEventType = {
1043
+ currentActivityLevel: number;
1044
+ minVolume: number;
1045
+ resolve: () => void;
1046
+ };
1047
+ const event_fail = {
1048
+ currentActivityLevel: 0.0001,
1049
+ minVolume: video_config["minVolume"],
1050
+ resolve: jest.fn(),
1051
+ } as micEventType;
1052
+
1053
+ expect(video_config["micChecked"]).toBe(false);
1054
+ video_config["onMicActivityLevel"](
1055
+ event_fail.currentActivityLevel,
1056
+ event_fail.minVolume,
1057
+ event_fail.resolve,
1058
+ );
1059
+ expect(video_config["micChecked"]).toBe(false);
1060
+ expect(event_fail.resolve).not.toHaveBeenCalled();
1061
+
1062
+ const event_pass = {
1063
+ currentActivityLevel: 0.2,
1064
+ minVolume: video_config["minVolume"],
1065
+ resolve: jest.fn(),
1066
+ } as micEventType;
1067
+
1068
+ expect(video_config["micChecked"]).toBe(false);
1069
+ video_config["onMicActivityLevel"](
1070
+ event_pass.currentActivityLevel,
1071
+ event_pass.minVolume,
1072
+ event_pass.resolve,
1073
+ );
1074
+ expect(video_config["micChecked"]).toBe(true);
1075
+ expect(event_pass.resolve).toHaveBeenCalled();
1076
+ });
1077
+
1078
+ test("Video config onMicActivityLevel", () => {
1079
+ type micEventType = {
1080
+ currentActivityLevel: number;
1081
+ minVolume: number;
1082
+ resolve: () => void;
1083
+ };
1084
+ const event_fail = {
1085
+ currentActivityLevel: 0.0001,
1086
+ minVolume: video_config["minVolume"],
1087
+ resolve: jest.fn(),
1088
+ } as micEventType;
1089
+
1090
+ expect(video_config["micChecked"]).toBe(false);
1091
+ video_config["onMicActivityLevel"](
1092
+ event_fail.currentActivityLevel,
1093
+ event_fail.minVolume,
1094
+ event_fail.resolve,
1095
+ );
1096
+ expect(video_config["micChecked"]).toBe(false);
1097
+ expect(event_fail.resolve).not.toHaveBeenCalled();
1098
+
1099
+ const event_pass = {
1100
+ currentActivityLevel: 0.2,
1101
+ minVolume: video_config["minVolume"],
1102
+ resolve: jest.fn(),
1103
+ } as micEventType;
1104
+
1105
+ expect(video_config["micChecked"]).toBe(false);
1106
+ video_config["onMicActivityLevel"](
1107
+ event_pass.currentActivityLevel,
1108
+ event_pass.minVolume,
1109
+ event_pass.resolve,
1110
+ );
1111
+ expect(video_config["micChecked"]).toBe(true);
1112
+ expect(event_pass.resolve).toHaveBeenCalled();
1113
+ });