jsPsych/packages/jspsych/tests/core/timelines.test.ts
bjoluc 64541a451f Implement preload flag for plugin parameters
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.
2021-08-17 16:47:07 +02:00

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);
});
});
});