@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/README.md +125 -6
- package/dist/index.browser.js +70 -12
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +16 -16
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +69 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +104 -9
- package/dist/index.js +69 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/consentVideo.spec.ts +0 -1
- package/src/consentVideo.ts +5 -1
- package/src/index.spec.ts +387 -12
- package/src/recorder.spec.ts +50 -6
- package/src/recorder.ts +9 -1
- package/src/start.ts +7 -1
- package/src/stop.ts +41 -7
- package/src/trial.ts +97 -11
package/package.json
CHANGED
package/src/consentVideo.spec.ts
CHANGED
|
@@ -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
|
});
|
package/src/consentVideo.ts
CHANGED
|
@@ -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().
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
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(
|
|
28
|
-
value:
|
|
29
|
-
|
|
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
|
|
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("
|
|
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
|
|
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
|
+
});
|
package/src/recorder.spec.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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>{
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
display_element.innerHTML =
|
|
48
|
-
|
|
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
|
}
|