@lookit/record 4.0.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lookit/record",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Recording extensions and plugins for CHS studies.",
5
5
  "homepage": "https://github.com/lookit/lookit-jspsych#readme",
6
6
  "bugs": {
@@ -310,7 +310,6 @@ test("stopButton", async () => {
310
310
  .forEach((button) => expect(button.disabled).toBeFalsy());
311
311
  expect(stopButton!.disabled).toBeTruthy();
312
312
  expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
313
- expect(Recorder.prototype.reset).toHaveBeenCalledTimes(1);
314
313
  expect(plugin["recordFeed"]).toHaveBeenCalledTimes(1);
315
314
  expect(plugin["recordFeed"]).toHaveBeenCalledWith(display);
316
315
  });
@@ -59,6 +59,11 @@ const info = <const>{
59
59
  consent_statement_text: { type: ParameterType.STRING, default: "" },
60
60
  omit_injury_phrase: { type: ParameterType.BOOL, default: false },
61
61
  },
62
+ data: {
63
+ chs_type: {
64
+ type: ParameterType.STRING,
65
+ },
66
+ },
62
67
  };
63
68
  type Info = typeof info;
64
69
 
@@ -301,7 +306,6 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
301
306
  stop.disabled = true;
302
307
  this.addMessage(display, this.uploadingMsg!);
303
308
  await this.recorder.stop(true);
304
- this.recorder.reset();
305
309
  this.recordFeed(display);
306
310
  this.getImg(display, "record-icon").style.visibility = "hidden";
307
311
  this.addMessage(display, this.notRecordingMsg!);
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";
@@ -143,29 +143,73 @@ test("Recorder initialize error", () => {
143
143
  });
144
144
 
145
145
  test("Recorder handleStop", async () => {
146
- 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
+
147
177
  const download = jest.fn();
148
178
  const resolve = jest.fn();
149
- 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);
150
181
 
151
- // manual mock
182
+ // Manual mock
152
183
  rec["download"] = download;
153
184
  rec["blobs"] = ["some recorded data" as unknown as Blob];
154
185
  URL.createObjectURL = jest.fn();
155
186
 
156
- // let's download the file locally
187
+ // Download the file locally
157
188
  rec["localDownload"] = true;
158
189
 
190
+ // Stream cannot be active when handleStop/reset is called
191
+ mockStream.active = false;
192
+
193
+ const handleStop = rec["handleStop"](resolve);
194
+
159
195
  await handleStop();
160
196
 
197
+ expect(download).toHaveBeenCalledTimes(1);
198
+ expect(resetSpy).toHaveBeenCalledTimes(1);
199
+
161
200
  // Upload the file to s3
162
201
  rec["localDownload"] = false;
163
202
  rec["_s3"] = new Data.LookitS3("some key");
164
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
+
165
207
  await handleStop();
166
208
 
167
- expect(download).toHaveBeenCalledTimes(1);
168
209
  expect(Data.LookitS3.prototype.completeUpload).toHaveBeenCalledTimes(1);
210
+ expect(resetSpy).toHaveBeenCalledTimes(2);
211
+
212
+ resetSpy.mockRestore();
169
213
  });
170
214
 
171
215
  test("Recorder handleStop error with no url", () => {
package/src/recorder.ts CHANGED
@@ -109,7 +109,13 @@ export default class Recorder {
109
109
  this.jsPsych.pluginAPI.initializeCameraRecorder(stream, recorder_options);
110
110
  }
111
111
 
112
- /** Reset the recorder to be used again. */
112
+ /**
113
+ * Reset the recorder. This is used internally after stopping/uploading a
114
+ * recording, in order to create a new active stream that can be used by a new
115
+ * Recorder instance. This can also be used by the consuming plugin/extension
116
+ * when a recorder needs to be reset without the stop/upload events (e.g. in
117
+ * the video config plugin).
118
+ */
113
119
  public reset() {
114
120
  if (this.stream.active) {
115
121
  throw new StreamActiveOnResetError();
@@ -332,6 +338,8 @@ export default class Recorder {
332
338
  } else {
333
339
  await this.s3.completeUpload();
334
340
  }
341
+ // Reset the recorder. This is necessary to create another active media stream from the stream clone, because the current stream is fully stopped/inactive and cannot be used again.
342
+ this.reset();
335
343
 
336
344
  resolve();
337
345
  };
package/src/start.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { LookitWindow } from "@lookit/data/dist/types";
2
2
  import { JsPsych, JsPsychPlugin } from "jspsych";
3
+ import { version } from "../package.json";
3
4
  import { ExistingRecordingError } from "./errors";
4
5
  import Recorder from "./recorder";
5
6
 
6
7
  declare let window: LookitWindow;
7
8
 
8
- const info = <const>{ name: "start-record-plugin", parameters: {} };
9
+ const info = <const>{
10
+ name: "start-record-plugin",
11
+ version,
12
+ parameters: {},
13
+ data: {},
14
+ };
9
15
  type Info = typeof info;
10
16
 
11
17
  /** Start recording. Used by researchers who want to record across trials. */
package/src/stop.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { LookitWindow } from "@lookit/data/dist/types";
2
2
  import chsTemplates from "@lookit/templates";
3
3
  import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
4
+ import { version } from "../package.json";
4
5
  import { NoSessionRecordingError } from "./errors";
5
6
  import Recorder from "./recorder";
6
7
 
@@ -8,9 +9,32 @@ declare let window: LookitWindow;
8
9
 
9
10
  const info = <const>{
10
11
  name: "stop-record-plugin",
12
+ version,
11
13
  parameters: {
12
- locale: { type: ParameterType.STRING, default: "en-us" },
14
+ /**
15
+ * This string can contain HTML markup. Any content provided will be
16
+ * displayed while the recording is uploading. If null (the default), then
17
+ * the default 'uploading video, please wait' (or appropriate translation
18
+ * based on 'locale') will be displayed. Use a blank string for no
19
+ * message/content.
20
+ */
21
+ wait_for_upload_message: {
22
+ type: ParameterType.HTML_STRING,
23
+ default: null as null | string,
24
+ },
25
+ /**
26
+ * Locale code used for translating the default 'uploading video, please
27
+ * wait' message. This code must be present in the translation files. If the
28
+ * code is not found then English will be used. If the
29
+ * 'wait_for_upload_message' parameter is specified then this value is
30
+ * ignored.
31
+ */
32
+ locale: {
33
+ type: ParameterType.STRING,
34
+ default: "en-us",
35
+ },
13
36
  },
37
+ data: {},
14
38
  };
15
39
  type Info = typeof info;
16
40
 
@@ -41,11 +65,21 @@ export default class StopRecordPlugin implements JsPsychPlugin<Info> {
41
65
  * @param trial - Trial object with parameters/values.
42
66
  */
43
67
  public trial(display_element: HTMLElement, trial: TrialType<Info>): void {
44
- display_element.innerHTML = chsTemplates.uploadingVideo(trial);
45
- this.recorder.stop().then(() => {
46
- window.chs.sessionRecorder = null;
47
- display_element.innerHTML = "";
48
- this.jsPsych.finishTrial();
49
- });
68
+ if (trial.wait_for_upload_message == null) {
69
+ display_element.innerHTML = chsTemplates.uploadingVideo(trial);
70
+ } else {
71
+ display_element.innerHTML = trial.wait_for_upload_message;
72
+ }
73
+ this.recorder
74
+ .stop()
75
+ .then(() => {
76
+ window.chs.sessionRecorder = null;
77
+ display_element.innerHTML = "";
78
+ this.jsPsych.finishTrial();
79
+ })
80
+ .catch((err) => {
81
+ console.error("StopRecordPlugin: recorder stop/upload failed.", err);
82
+ // TO DO: display translated error msg and/or researcher contact info
83
+ });
50
84
  }
51
85
  }