import audioKeyboardResponse from "@jspsych/plugin-audio-keyboard-response";
import imageKeyboardResponse from "@jspsych/plugin-image-keyboard-response";
import videoKeyboardResponse from "@jspsych/plugin-video-keyboard-response";
import { simulateTimeline, startTimeline } from "@jspsych/test-utils";
import { JsPsych, initJsPsych } from "jspsych";
import preloadPlugin from ".";
jest.useFakeTimers();
describe("preload plugin", () => {
let jsPsych: JsPsych;
let pluginAPI: JsPsych["pluginAPI"];
beforeEach(() => {
jsPsych = initJsPsych();
pluginAPI = jsPsych.pluginAPI;
});
function spyOnPreload(preloadType: "Audio" | "Video" | "Images") {
return jest
.spyOn(
pluginAPI,
`preload${preloadType}` as "preloadAudio" | "preloadVideo" | "preloadImages"
)
.mockImplementation((files, callback_complete, callback_load, callback_error) => {
callback_complete();
});
}
describe("auto_preload", () => {
test("auto_preload method works with simple timeline and image stimulus", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
},
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual(["img/foo.png"]);
});
test("auto_preload method works with simple timeline and audio stimulus", async () => {
const spy = spyOnPreload("Audio");
await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
},
{
type: audioKeyboardResponse,
stimulus: "sound/foo.mp3",
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual(["sound/foo.mp3"]);
});
test("auto_preload method works with simple timeline and video stimulus", async () => {
const spy = spyOnPreload("Video");
await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
},
{
type: videoKeyboardResponse,
stimulus: "video/foo.mp4",
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual(["video/foo.mp4"]);
});
test("auto_preload method works with nested timeline", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
},
{
type: imageKeyboardResponse,
render_on_canvas: false,
timeline: [{ stimulus: "img/foo.png" }],
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual(["img/foo.png"]);
});
test("auto_preload method works with looping timeline", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
},
{
timeline: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
loop_function: () => true,
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual(["img/foo.png"]);
});
test("auto_preload method works with conditional timeline", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
},
{
timeline: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
conditional_function: () => true,
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toStrictEqual(["img/foo.png"]);
});
test("auto_preload method works with timeline variables when stim is statically defined in trial object", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
},
{
timeline: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
data: jsPsych.timelineVariable("data"),
},
],
timeline_variables: [
{ data: { trial: 1 } },
{ data: { trial: 2 } },
{ data: { trial: 3 } },
],
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toStrictEqual(["img/foo.png"]);
});
});
describe("trials parameter", () => {
test("trials parameter works with simple timeline", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
trials: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toStrictEqual(["img/foo.png"]);
});
test("trials parameter works with looping timeline", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
trials: [
{
timeline: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
loop_function: () => true,
},
],
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toStrictEqual(["img/foo.png"]);
});
test("trials parameter works with conditional timeline", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
trials: [
{
timeline: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
conditional_function: () => false,
},
],
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toStrictEqual(["img/foo.png"]);
});
test("trials parameter works with timeline variables when stim is statically defined in trial object", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
trials: [
{
timeline: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
data: jsPsych.timelineVariable("data"),
},
],
timeline_variables: [
{ data: { trial: 1 } },
{ data: { trial: 2 } },
{ data: { trial: 3 } },
],
},
],
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toStrictEqual(["img/foo.png"]);
});
test("timeline variables in trials parameter are *not* evaluated", async () => {
const spy = jest.spyOn(console, "warn");
const trial = {
type: preloadPlugin,
trials: [
{
timeline: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
data: jsPsych.timelineVariable("data"),
},
],
timeline_variables: [
{ data: { trial: 1 } },
{ data: { trial: 2 } },
{ data: { trial: 3 } },
],
},
],
};
await startTimeline([trial], jsPsych);
expect(spy).toHaveBeenCalledTimes(0);
});
});
describe("calls to pluginAPI preload functions", () => {
test("auto_preload, trials, and manual preload array parameters can be used together", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
trials: [
{
type: imageKeyboardResponse,
stimulus: "img/bar.png",
render_on_canvas: false,
},
],
images: ["img/fizz.png"],
},
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual(
expect.arrayContaining(["img/foo.png", "img/bar.png", "img/fizz.png"])
);
});
test("plugin only attempts to load duplicate files once", async () => {
const spy = spyOnPreload("Images");
await startTimeline(
[
{
type: preloadPlugin,
trials: [
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
images: ["img/foo.png"],
},
{
type: imageKeyboardResponse,
stimulus: "img/foo.png",
render_on_canvas: false,
},
],
jsPsych
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual(["img/foo.png"]);
});
});
describe("continue_after_error and error messages", () => {
test("experiment continues when image loads successfully", async () => {
jest
.spyOn(pluginAPI, "preloadImages")
.mockImplementation((x, cb_complete, cb_load, cb_error) => {
if (x.includes("image.png")) {
cb_load("image.png");
cb_complete();
}
});
const { getHTML } = await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
error_message: "foo",
max_load_time: 100,
},
{
type: imageKeyboardResponse,
stimulus: "image.png",
render_on_canvas: false,
},
],
jsPsych
);
expect(getHTML()).toContain(
' {
jest
.spyOn(pluginAPI, "preloadImages")
.mockImplementation((x, cb_complete, cb_load, cb_error) => {
cb_error({
source: x,
error: {},
});
});
const { getHTML } = await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
error_message: "foo",
max_load_time: 100,
on_error: function (e) {
expect(e).toContain("img/bar.png");
},
},
{
type: imageKeyboardResponse,
stimulus: "img/bar.png",
render_on_canvas: false,
},
],
jsPsych
);
expect(getHTML()).toContain("foo");
});
test("error_message is shown when continue_after_error is false and loading times out", async () => {
jest
.spyOn(pluginAPI, "preloadImages")
.mockImplementation((x, cb_complete, cb_load, cb_error) => {
// don't call anything here to simulate waiting forever for image to load
});
const onError = jest.fn();
const { getHTML } = await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
error_message: "foo",
max_load_time: 100,
on_error: onError,
},
{
type: imageKeyboardResponse,
stimulus: "blue.png",
render_on_canvas: false,
},
],
jsPsych
);
jest.advanceTimersByTime(101);
expect(onError).toHaveBeenCalledWith("timeout");
expect(getHTML()).toContain("foo");
});
test("experiment continues when continue_after_error is true and files fail", async () => {
jest
.spyOn(pluginAPI, "preloadImages")
.mockImplementation((x, cb_complete, cb_load, cb_error) => {
cb_error({
source: x,
error: {},
});
});
const mockFn = jest.fn();
const { getHTML } = await startTimeline(
[
{
type: preloadPlugin,
images: ["img/foo.png"],
error_message: "bar",
max_load_time: null,
continue_after_error: true,
on_error: mockFn,
},
{
type: imageKeyboardResponse,
stimulus: "blue.png",
render_on_canvas: false,
},
],
jsPsych
);
expect(mockFn).toHaveBeenCalledWith(["img/foo.png"]);
expect(getHTML()).toContain(
'
{
jest
.spyOn(pluginAPI, "preloadImages")
.mockImplementation((x, cb_complete, cb_load, cb_error) => {
// don't call anything here to simulate waiting forever for image to load
});
const mockFn = jest.fn();
const { getHTML } = await startTimeline(
[
{
type: preloadPlugin,
auto_preload: true,
error_message: "bar",
max_load_time: 100,
continue_after_error: true,
on_error: mockFn,
},
{
type: imageKeyboardResponse,
stimulus: "../media/blue.png",
render_on_canvas: false,
},
],
jsPsych
);
jest.advanceTimersByTime(101);
expect(mockFn).toHaveBeenCalledWith("timeout");
expect(getHTML()).toMatch(
'
{
jest
.spyOn(pluginAPI, "preloadImages")
.mockImplementation((x, cb_complete, cb_load, cb_error) => {
cb_error({
source: x,
error: {},
});
});
const mockFn = jest.fn();
const { getHTML } = await startTimeline(
[
{
type: preloadPlugin,
images: ["img/foo.png"],
error_message: "bar",
show_detailed_errors: true,
on_error: mockFn,
},
],
jsPsych
);
expect(mockFn).toHaveBeenCalledWith(["img/foo.png"]);
expect(getHTML()).toContain("Error details");
});
});
describe("display while loading", () => {
test("custom loading message is shown above progress bar if specified", async () => {
const { getHTML } = await startTimeline([
{
type: preloadPlugin,
images: ["img/foo.png"],
message: "baz",
max_load_time: 100,
},
]);
expect(getHTML()).toContain("baz");
expect(getHTML()).toContain('