mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 19:20:55 +00:00

It defaults to `true` for parameters of type IMAGE, VIDEO, and AUDIO, and is ignored for any other parameter type. Example: Using `parameterType.IMAGE` for a parameter `param` in a plugin named `my-plugin` is equivalent to calling `jspsych.pluginAPI.registerPreload("my-plugin", "param", "image")` before `jsPsych.run()`. Setting `preload: false` for the parameter would disable this invocation.
598 lines
14 KiB
TypeScript
598 lines
14 KiB
TypeScript
import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
|
|
|
|
import { initJsPsych, parameterType } from "../../src";
|
|
import { TimelineNode } from "../../src/TimelineNode";
|
|
import { pressKey, startTimeline } from "../utils";
|
|
|
|
describe("loop function", () => {
|
|
test("repeats a timeline when returns true", async () => {
|
|
let count = 0;
|
|
|
|
const trial = {
|
|
timeline: [{ type: htmlKeyboardResponse, stimulus: "foo" }],
|
|
loop_function: () => {
|
|
if (count < 1) {
|
|
count++;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
};
|
|
|
|
const { jsPsych } = await startTimeline([trial]);
|
|
|
|
// first trial
|
|
pressKey("a");
|
|
expect(jsPsych.data.get().count()).toBe(1);
|
|
|
|
// second trial
|
|
pressKey("a");
|
|
expect(jsPsych.data.get().count()).toBe(2);
|
|
});
|
|
|
|
test("does not repeat when returns false", async () => {
|
|
const { jsPsych } = await startTimeline([
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
loop_function: () => false,
|
|
},
|
|
]);
|
|
|
|
// first trial
|
|
pressKey("a");
|
|
expect(jsPsych.data.get().count()).toBe(1);
|
|
|
|
// second trial
|
|
pressKey("a");
|
|
expect(jsPsych.data.get().count()).toBe(1);
|
|
});
|
|
|
|
test("gets the data from the most recent iteration", async () => {
|
|
let data_count = [];
|
|
let count = 0;
|
|
|
|
const { jsPsych } = await startTimeline([
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
loop_function: (data) => {
|
|
data_count.push(data.count());
|
|
if (count < 2) {
|
|
count++;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
},
|
|
]);
|
|
|
|
// first trial
|
|
pressKey("a");
|
|
|
|
// second trial
|
|
pressKey("a");
|
|
|
|
// third trial
|
|
pressKey("a");
|
|
|
|
expect(data_count).toEqual([1, 1, 1]);
|
|
expect(jsPsych.data.get().count()).toBe(3);
|
|
});
|
|
|
|
test("timeline variables from nested timelines are available in loop function", async () => {
|
|
let counter = 0;
|
|
|
|
const jsPsych = initJsPsych();
|
|
const { getHTML } = await startTimeline(
|
|
[
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: jsPsych.timelineVariable("word"),
|
|
},
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
loop_function: () => {
|
|
if (jsPsych.timelineVariable("word") == "b" && counter < 2) {
|
|
counter++;
|
|
return true;
|
|
} else {
|
|
counter = 0;
|
|
return false;
|
|
}
|
|
},
|
|
},
|
|
],
|
|
timeline_variables: [{ word: "a" }, { word: "b" }, { word: "c" }],
|
|
},
|
|
],
|
|
jsPsych
|
|
);
|
|
|
|
expect(getHTML()).toMatch("a");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("b");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("c");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
});
|
|
|
|
test("only runs once when timeline variables are used", async () => {
|
|
let count = 0;
|
|
|
|
await startTimeline([
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
timeline_variables: [{ a: 1 }, { a: 2 }],
|
|
loop_function: () => {
|
|
count++;
|
|
return false;
|
|
},
|
|
},
|
|
]);
|
|
|
|
// first trial
|
|
pressKey("a");
|
|
expect(count).toBe(0);
|
|
|
|
// second trial
|
|
pressKey("a");
|
|
expect(count).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("conditional function", () => {
|
|
test("skips the timeline when returns false", async () => {
|
|
const conditional = {
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
conditional_function: () => false,
|
|
};
|
|
|
|
const trial = {
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "bar",
|
|
};
|
|
|
|
const { getHTML } = await startTimeline([conditional, trial]);
|
|
|
|
expect(getHTML()).toMatch("bar");
|
|
|
|
// clear
|
|
pressKey("a");
|
|
});
|
|
|
|
test("completes the timeline when returns true", async () => {
|
|
const conditional = {
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
conditional_function: () => {
|
|
return true;
|
|
},
|
|
};
|
|
|
|
const trial = {
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "bar",
|
|
};
|
|
|
|
const { getHTML } = await startTimeline([conditional, trial]);
|
|
|
|
expect(getHTML()).toMatch("foo");
|
|
|
|
// next
|
|
pressKey("a");
|
|
|
|
expect(getHTML()).toMatch("bar");
|
|
|
|
// clear
|
|
pressKey("a");
|
|
});
|
|
|
|
test("executes on every loop of the timeline", async () => {
|
|
let count = 0;
|
|
let conditional_count = 0;
|
|
|
|
await startTimeline([
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
loop_function: () => {
|
|
if (count < 1) {
|
|
count++;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
conditional_function: () => {
|
|
conditional_count++;
|
|
return true;
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(conditional_count).toBe(1);
|
|
|
|
// first trial
|
|
pressKey("a");
|
|
|
|
expect(conditional_count).toBe(2);
|
|
|
|
// second trial
|
|
pressKey("a");
|
|
|
|
expect(conditional_count).toBe(2);
|
|
});
|
|
|
|
test("executes only once even when repetitions is > 1", async () => {
|
|
var conditional_count = 0;
|
|
|
|
await startTimeline([
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
repetitions: 2,
|
|
conditional_function: () => {
|
|
conditional_count++;
|
|
return true;
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(conditional_count).toBe(1);
|
|
|
|
// first trial
|
|
pressKey("a");
|
|
|
|
expect(conditional_count).toBe(1);
|
|
|
|
// second trial
|
|
pressKey("a");
|
|
|
|
expect(conditional_count).toBe(1);
|
|
});
|
|
|
|
test("executes only once when timeline variables are used", async () => {
|
|
let conditional_count = 0;
|
|
|
|
await startTimeline([
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
timeline_variables: [{ a: 1 }, { a: 2 }],
|
|
conditional_function: () => {
|
|
conditional_count++;
|
|
return true;
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(conditional_count).toBe(1);
|
|
|
|
// first trial
|
|
pressKey("a");
|
|
|
|
expect(conditional_count).toBe(1);
|
|
|
|
// second trial
|
|
pressKey("a");
|
|
|
|
expect(conditional_count).toBe(1);
|
|
});
|
|
|
|
test("timeline variables from nested timelines are available", async () => {
|
|
const jsPsych = initJsPsych();
|
|
const { getHTML } = await startTimeline(
|
|
[
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: jsPsych.timelineVariable("word"),
|
|
},
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
],
|
|
conditional_function: () => {
|
|
if (jsPsych.timelineVariable("word") == "b") {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
},
|
|
},
|
|
],
|
|
timeline_variables: [{ word: "a" }, { word: "b" }, { word: "c" }],
|
|
},
|
|
],
|
|
jsPsych
|
|
);
|
|
|
|
expect(getHTML()).toMatch("a");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("b");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("c");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
});
|
|
});
|
|
|
|
describe("endCurrentTimeline", () => {
|
|
test("stops the current timeline, skipping to the end after the trial completes", async () => {
|
|
const jsPsych = initJsPsych();
|
|
const { getHTML } = await startTimeline(
|
|
[
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
on_finish: () => {
|
|
jsPsych.endCurrentTimeline();
|
|
},
|
|
},
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "bar",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "woo",
|
|
},
|
|
],
|
|
jsPsych
|
|
);
|
|
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("woo");
|
|
pressKey("a");
|
|
});
|
|
|
|
test("works inside nested timelines", async () => {
|
|
const jsPsych = initJsPsych();
|
|
const { getHTML } = await startTimeline(
|
|
[
|
|
{
|
|
timeline: [
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
on_finish: () => {
|
|
jsPsych.endCurrentTimeline();
|
|
},
|
|
},
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "skip me!",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "bar",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "woo",
|
|
},
|
|
],
|
|
jsPsych
|
|
);
|
|
|
|
expect(getHTML()).toMatch("foo");
|
|
|
|
pressKey("a");
|
|
|
|
expect(getHTML()).toMatch("bar");
|
|
|
|
pressKey("a");
|
|
|
|
expect(getHTML()).toMatch("woo");
|
|
|
|
pressKey("a");
|
|
});
|
|
});
|
|
|
|
describe("nested timelines", () => {
|
|
test("works without other parameters", async () => {
|
|
const { getHTML } = await startTimeline([
|
|
{
|
|
timeline: [
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
},
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "bar",
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("bar");
|
|
pressKey("a");
|
|
});
|
|
});
|
|
|
|
describe("add node to end of timeline", () => {
|
|
test("adds node to end of timeline", async () => {
|
|
const jsPsych = initJsPsych();
|
|
const { getHTML } = await startTimeline(
|
|
[
|
|
{
|
|
type: htmlKeyboardResponse,
|
|
stimulus: "foo",
|
|
on_start: () => {
|
|
jsPsych.addNodeToEndOfTimeline({
|
|
timeline: [{ type: htmlKeyboardResponse, stimulus: "bar" }],
|
|
});
|
|
},
|
|
},
|
|
],
|
|
jsPsych
|
|
);
|
|
|
|
expect(getHTML()).toMatch("foo");
|
|
pressKey("a");
|
|
expect(getHTML()).toMatch("bar");
|
|
pressKey("a");
|
|
});
|
|
});
|
|
|
|
describe("TimelineNode", () => {
|
|
const createTimelineNode = (parameters) => new TimelineNode(initJsPsych(), parameters);
|
|
|
|
describe("extractPreloadParameters", () => {
|
|
it("works for a single trial", () => {
|
|
const preloadMap = createTimelineNode({
|
|
type: {
|
|
info: {
|
|
name: "my-plugin",
|
|
parameters: {
|
|
one: { type: parameterType.IMAGE },
|
|
two: { type: parameterType.VIDEO },
|
|
three: { type: parameterType.AUDIO },
|
|
four: { type: parameterType.IMAGE, preload: true },
|
|
five: { type: parameterType.IMAGE, preload: false },
|
|
six: {
|
|
type: parameterType.STRING,
|
|
// This is illegal! But it should still not be added
|
|
preload: true,
|
|
},
|
|
seven: {},
|
|
},
|
|
},
|
|
},
|
|
}).extractPreloadParameters();
|
|
|
|
expect(preloadMap.get("my-plugin")).toEqual({
|
|
one: "image",
|
|
two: "video",
|
|
three: "audio",
|
|
four: "image",
|
|
});
|
|
});
|
|
|
|
it("works for a nested timeline", () => {
|
|
const preloadMap = createTimelineNode({
|
|
timeline: [
|
|
{
|
|
type: {
|
|
info: {
|
|
name: "plugin1",
|
|
parameters: { one: { type: parameterType.STRING } },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: {
|
|
info: {
|
|
name: "plugin2",
|
|
parameters: { one: { type: parameterType.AUDIO } },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
timeline: [
|
|
{
|
|
type: {
|
|
info: {
|
|
name: "plugin3",
|
|
parameters: {
|
|
one: { type: parameterType.VIDEO },
|
|
two: { type: parameterType.IMAGE },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}).extractPreloadParameters();
|
|
|
|
expect(preloadMap.get("plugin1")).toEqual({});
|
|
expect(preloadMap.get("plugin2")).toEqual({ one: "audio" });
|
|
expect(preloadMap.get("plugin3")).toEqual({ one: "video", two: "image" });
|
|
});
|
|
|
|
it("ignores trials with a function type", () => {
|
|
const preloadMap = createTimelineNode({
|
|
type: () => ({ info: { name: "my-dynamic-trial-type" } }),
|
|
}).extractPreloadParameters();
|
|
|
|
expect(preloadMap.size).toEqual(0);
|
|
});
|
|
});
|
|
});
|