mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 19:20:55 +00:00
Remove JsPsych
dependency from timeline nodes
This commit is contained in:
parent
f23fb33a53
commit
771ee6671e
2
package-lock.json
generated
2
package-lock.json
generated
@ -24644,7 +24644,7 @@
|
|||||||
"@jspsych/test-utils": "^1.1.1",
|
"@jspsych/test-utils": "^1.1.1",
|
||||||
"@types/dom-mediacapture-record": "^1.0.11",
|
"@types/dom-mediacapture-record": "^1.0.11",
|
||||||
"@types/lodash.get": "^4.4.6",
|
"@types/lodash.get": "^4.4.6",
|
||||||
"@types/lodash.has": "*",
|
"@types/lodash.has": "^4.5.7",
|
||||||
"@types/lodash.set": "^4.3.7",
|
"@types/lodash.set": "^4.3.7",
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import autoBind from "auto-bind";
|
import autoBind from "auto-bind";
|
||||||
|
import { Class } from "type-fest";
|
||||||
|
|
||||||
import { version } from "../package.json";
|
import { version } from "../package.json";
|
||||||
import { JsPsychData } from "./modules/data";
|
import { JsPsychData } from "./modules/data";
|
||||||
import { PluginAPI, createJointPluginAPIObject } from "./modules/plugin-api";
|
import { PluginAPI, createJointPluginAPIObject } from "./modules/plugin-api";
|
||||||
|
import { JsPsychPlugin, PluginInfo } from "./modules/plugins";
|
||||||
import * as randomization from "./modules/randomization";
|
import * as randomization from "./modules/randomization";
|
||||||
import * as turk from "./modules/turk";
|
import * as turk from "./modules/turk";
|
||||||
import * as utils from "./modules/utils";
|
import * as utils from "./modules/utils";
|
||||||
import {
|
import {
|
||||||
GlobalTimelineNodeCallbacks,
|
|
||||||
TimelineArray,
|
TimelineArray,
|
||||||
TimelineDescription,
|
TimelineDescription,
|
||||||
|
TimelineNodeDependencies,
|
||||||
TimelineVariable,
|
TimelineVariable,
|
||||||
TrialResult,
|
TrialResult,
|
||||||
} from "./timeline";
|
} from "./timeline";
|
||||||
@ -67,7 +69,7 @@ export class JsPsych {
|
|||||||
*/
|
*/
|
||||||
private simulation_options;
|
private simulation_options;
|
||||||
|
|
||||||
private timelineNodeCallbacks = new (class implements GlobalTimelineNodeCallbacks {
|
private timelineDependencies = new (class implements TimelineNodeDependencies {
|
||||||
constructor(private jsPsych: JsPsych) {
|
constructor(private jsPsych: JsPsych) {
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
}
|
}
|
||||||
@ -101,6 +103,16 @@ export class JsPsych {
|
|||||||
this.jsPsych.removeCssClasses(cssClasses);
|
this.jsPsych.removeCssClasses(cssClasses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instantiatePlugin<Info extends PluginInfo>(pluginClass: Class<JsPsychPlugin<Info>>) {
|
||||||
|
return new pluginClass(this.jsPsych);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultIti = this.jsPsych.options.default_iti;
|
||||||
|
|
||||||
|
displayElement = this.jsPsych.getDisplayElement();
|
||||||
|
|
||||||
|
finishTrialPromise = this.jsPsych.finishTrialPromise;
|
||||||
})(this);
|
})(this);
|
||||||
|
|
||||||
constructor(options?) {
|
constructor(options?) {
|
||||||
@ -175,7 +187,7 @@ export class JsPsych {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create experiment timeline
|
// create experiment timeline
|
||||||
this.timeline = new Timeline(this, this.timelineNodeCallbacks, timeline);
|
this.timeline = new Timeline(this.timelineDependencies, timeline);
|
||||||
|
|
||||||
await this.prepareDom();
|
await this.prepareDom();
|
||||||
await this.loadExtensions(this.options.extensions);
|
await this.loadExtensions(this.options.extensions);
|
||||||
@ -416,7 +428,7 @@ export class JsPsych {
|
|||||||
|
|
||||||
// New stuff as replacements for old methods:
|
// New stuff as replacements for old methods:
|
||||||
|
|
||||||
finishTrialPromise = new PromiseWrapper<TrialResult>();
|
private finishTrialPromise = new PromiseWrapper<TrialResult | void>();
|
||||||
finishTrial(data?: TrialResult) {
|
finishTrial(data?: TrialResult) {
|
||||||
this.finishTrialPromise.resolve(data);
|
this.finishTrialPromise.resolve(data);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { GlobalTimelineNodeCallbacks } from "src/timeline";
|
|
||||||
|
|
||||||
import { JsPsych } from "../../JsPsych";
|
import { JsPsych } from "../../JsPsych";
|
||||||
import { DataCollection } from "./DataCollection";
|
import { DataCollection } from "./DataCollection";
|
||||||
import { getQueryString } from "./utils";
|
import { getQueryString } from "./utils";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { SetRequired } from "type-fest";
|
import { SetRequired } from "type-fest";
|
||||||
|
|
||||||
import { TrialDescription } from "../timeline";
|
import { TrialDescription, TrialResult } from "../timeline";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parameter types for plugins
|
* Parameter types for plugins
|
||||||
@ -136,8 +136,6 @@ export const universalPluginParameters = <const>{
|
|||||||
|
|
||||||
export type UniversalPluginParameters = InferredParameters<typeof universalPluginParameters>;
|
export type UniversalPluginParameters = InferredParameters<typeof universalPluginParameters>;
|
||||||
|
|
||||||
type test = undefined extends null ? "a" : "b";
|
|
||||||
|
|
||||||
export interface PluginInfo {
|
export interface PluginInfo {
|
||||||
name: string;
|
name: string;
|
||||||
parameters: ParameterInfos;
|
parameters: ParameterInfos;
|
||||||
@ -148,7 +146,7 @@ export interface JsPsychPlugin<I extends PluginInfo> {
|
|||||||
display_element: HTMLElement,
|
display_element: HTMLElement,
|
||||||
trial: TrialType<I>,
|
trial: TrialType<I>,
|
||||||
on_load?: () => void
|
on_load?: () => void
|
||||||
): void | Promise<any>;
|
): void | Promise<TrialResult | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TrialType<I extends PluginInfo> = InferredParameters<I["parameters"]> &
|
export type TrialType<I extends PluginInfo> = InferredParameters<I["parameters"]> &
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import has from "lodash.has";
|
import has from "lodash.has";
|
||||||
|
|
||||||
import { JsPsych } from "../JsPsych";
|
|
||||||
import { Timeline } from "./Timeline";
|
import { Timeline } from "./Timeline";
|
||||||
import {
|
import {
|
||||||
GetParameterValueOptions,
|
GetParameterValueOptions,
|
||||||
GlobalTimelineNodeCallbacks,
|
|
||||||
TimelineDescription,
|
TimelineDescription,
|
||||||
TimelineNode,
|
TimelineNode,
|
||||||
|
TimelineNodeDependencies,
|
||||||
TimelineNodeStatus,
|
TimelineNodeStatus,
|
||||||
TimelineVariable,
|
TimelineVariable,
|
||||||
TrialDescription,
|
TrialDescription,
|
||||||
@ -24,10 +23,7 @@ export abstract class BaseTimelineNode implements TimelineNode {
|
|||||||
|
|
||||||
protected status = TimelineNodeStatus.PENDING;
|
protected status = TimelineNodeStatus.PENDING;
|
||||||
|
|
||||||
constructor(
|
constructor(protected readonly dependencies: TimelineNodeDependencies) {}
|
||||||
protected readonly jsPsych: JsPsych,
|
|
||||||
protected readonly globalCallbacks: GlobalTimelineNodeCallbacks
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return this.status;
|
return this.status;
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { flushPromises } from "@jspsych/test-utils";
|
import { flushPromises } from "@jspsych/test-utils";
|
||||||
import { JsPsych, initJsPsych } from "jspsych";
|
|
||||||
import { mocked } from "ts-jest/utils";
|
import { mocked } from "ts-jest/utils";
|
||||||
|
|
||||||
import { GlobalCallbacks, mockDomRelatedJsPsychMethods } from "../../tests/test-utils";
|
import { MockTimelineNodeDependencies } from "../../tests/test-utils";
|
||||||
import TestPlugin from "../../tests/TestPlugin";
|
import TestPlugin from "../../tests/TestPlugin";
|
||||||
import {
|
import {
|
||||||
repeat,
|
repeat,
|
||||||
@ -30,19 +29,15 @@ const exampleTimeline: TimelineDescription = {
|
|||||||
timeline: [{ type: TestPlugin }, { type: TestPlugin }, { timeline: [{ type: TestPlugin }] }],
|
timeline: [{ type: TestPlugin }, { type: TestPlugin }, { timeline: [{ type: TestPlugin }] }],
|
||||||
};
|
};
|
||||||
|
|
||||||
const globalCallbacks = new GlobalCallbacks();
|
const dependencies = new MockTimelineNodeDependencies();
|
||||||
|
|
||||||
describe("Timeline", () => {
|
describe("Timeline", () => {
|
||||||
let jsPsych: JsPsych;
|
|
||||||
|
|
||||||
const createTimeline = (description: TimelineDescription | TimelineArray, parent?: Timeline) =>
|
const createTimeline = (description: TimelineDescription | TimelineArray, parent?: Timeline) =>
|
||||||
new Timeline(jsPsych, globalCallbacks, description, parent);
|
new Timeline(dependencies, description, parent);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
globalCallbacks.reset();
|
dependencies.reset();
|
||||||
TestPlugin.reset();
|
TestPlugin.reset();
|
||||||
jsPsych = initJsPsych();
|
|
||||||
mockDomRelatedJsPsychMethods(jsPsych);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("run()", () => {
|
describe("run()", () => {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { JsPsych } from "../JsPsych";
|
|
||||||
import {
|
import {
|
||||||
repeat,
|
repeat,
|
||||||
sampleWithReplacement,
|
sampleWithReplacement,
|
||||||
@ -11,10 +10,10 @@ import { Trial } from "./Trial";
|
|||||||
import { PromiseWrapper } from "./util";
|
import { PromiseWrapper } from "./util";
|
||||||
import {
|
import {
|
||||||
GetParameterValueOptions,
|
GetParameterValueOptions,
|
||||||
GlobalTimelineNodeCallbacks,
|
|
||||||
TimelineArray,
|
TimelineArray,
|
||||||
TimelineDescription,
|
TimelineDescription,
|
||||||
TimelineNode,
|
TimelineNode,
|
||||||
|
TimelineNodeDependencies,
|
||||||
TimelineNodeStatus,
|
TimelineNodeStatus,
|
||||||
TimelineVariable,
|
TimelineVariable,
|
||||||
TrialDescription,
|
TrialDescription,
|
||||||
@ -28,13 +27,12 @@ export class Timeline extends BaseTimelineNode {
|
|||||||
public readonly description: TimelineDescription;
|
public readonly description: TimelineDescription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
jsPsych: JsPsych,
|
dependencies: TimelineNodeDependencies,
|
||||||
globalCallbacks: GlobalTimelineNodeCallbacks,
|
|
||||||
description: TimelineDescription | TimelineArray,
|
description: TimelineDescription | TimelineArray,
|
||||||
protected readonly parent?: Timeline,
|
protected readonly parent?: Timeline,
|
||||||
public readonly index = 0
|
public readonly index = 0
|
||||||
) {
|
) {
|
||||||
super(jsPsych, globalCallbacks);
|
super(dependencies);
|
||||||
this.description = Array.isArray(description) ? { timeline: description } : description;
|
this.description = Array.isArray(description) ? { timeline: description } : description;
|
||||||
this.nextChildNodeIndex = index;
|
this.nextChildNodeIndex = index;
|
||||||
}
|
}
|
||||||
@ -143,8 +141,8 @@ export class Timeline extends BaseTimelineNode {
|
|||||||
const newChildNodes = this.description.timeline.map((childDescription) => {
|
const newChildNodes = this.description.timeline.map((childDescription) => {
|
||||||
const childNodeIndex = this.nextChildNodeIndex++;
|
const childNodeIndex = this.nextChildNodeIndex++;
|
||||||
return isTimelineDescription(childDescription)
|
return isTimelineDescription(childDescription)
|
||||||
? new Timeline(this.jsPsych, this.globalCallbacks, childDescription, this, childNodeIndex)
|
? new Timeline(this.dependencies, childDescription, this, childNodeIndex)
|
||||||
: new Trial(this.jsPsych, this.globalCallbacks, childDescription, this, childNodeIndex);
|
: new Trial(this.dependencies, childDescription, this, childNodeIndex);
|
||||||
});
|
});
|
||||||
this.children.push(...newChildNodes);
|
this.children.push(...newChildNodes);
|
||||||
return newChildNodes;
|
return newChildNodes;
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { flushPromises } from "@jspsych/test-utils";
|
import { flushPromises } from "@jspsych/test-utils";
|
||||||
import { JsPsych, initJsPsych } from "jspsych";
|
|
||||||
import { mocked } from "ts-jest/utils";
|
import { mocked } from "ts-jest/utils";
|
||||||
|
|
||||||
import { GlobalCallbacks, mockDomRelatedJsPsychMethods } from "../../tests/test-utils";
|
import { MockTimelineNodeDependencies } from "../../tests/test-utils";
|
||||||
import TestPlugin from "../../tests/TestPlugin";
|
import TestPlugin from "../../tests/TestPlugin";
|
||||||
import { ParameterType } from "../modules/plugins";
|
import { ParameterType } from "../modules/plugins";
|
||||||
import { Timeline } from "./Timeline";
|
import { Timeline } from "./Timeline";
|
||||||
@ -14,23 +13,20 @@ jest.useFakeTimers();
|
|||||||
|
|
||||||
jest.mock("./Timeline");
|
jest.mock("./Timeline");
|
||||||
|
|
||||||
const globalCallbacks = new GlobalCallbacks();
|
const dependencies = new MockTimelineNodeDependencies();
|
||||||
|
|
||||||
describe("Trial", () => {
|
describe("Trial", () => {
|
||||||
let jsPsych: JsPsych;
|
|
||||||
let timeline: Timeline;
|
let timeline: Timeline;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
globalCallbacks.reset();
|
dependencies.reset();
|
||||||
TestPlugin.reset();
|
TestPlugin.reset();
|
||||||
|
|
||||||
jsPsych = initJsPsych();
|
timeline = new Timeline(dependencies, { timeline: [] });
|
||||||
mockDomRelatedJsPsychMethods(jsPsych);
|
|
||||||
timeline = new Timeline(jsPsych, globalCallbacks, { timeline: [] });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const createTrial = (description: TrialDescription) =>
|
const createTrial = (description: TrialDescription) =>
|
||||||
new Trial(jsPsych, globalCallbacks, description, timeline, 0);
|
new Trial(dependencies, description, timeline, 0);
|
||||||
|
|
||||||
describe("run()", () => {
|
describe("run()", () => {
|
||||||
it("instantiates the corresponding plugin", async () => {
|
it("instantiates the corresponding plugin", async () => {
|
||||||
@ -49,8 +45,8 @@ describe("Trial", () => {
|
|||||||
|
|
||||||
expect(onStartCallback).toHaveBeenCalledTimes(1);
|
expect(onStartCallback).toHaveBeenCalledTimes(1);
|
||||||
expect(onStartCallback).toHaveBeenCalledWith(description);
|
expect(onStartCallback).toHaveBeenCalledWith(description);
|
||||||
expect(globalCallbacks.onTrialStart).toHaveBeenCalledTimes(1);
|
expect(dependencies.onTrialStart).toHaveBeenCalledTimes(1);
|
||||||
expect(globalCallbacks.onTrialStart).toHaveBeenCalledWith(trial);
|
expect(dependencies.onTrialStart).toHaveBeenCalledWith(trial);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("properly invokes the plugin's `trial` method", async () => {
|
it("properly invokes the plugin's `trial` method", async () => {
|
||||||
@ -107,7 +103,7 @@ describe("Trial", () => {
|
|||||||
.spyOn(TestPlugin.prototype, "trial")
|
.spyOn(TestPlugin.prototype, "trial")
|
||||||
.mockImplementation(async (display_element, trial, on_load) => {
|
.mockImplementation(async (display_element, trial, on_load) => {
|
||||||
on_load();
|
on_load();
|
||||||
jsPsych.finishTrial({ finishTrial: "result" });
|
dependencies.finishTrialPromise.resolve({ finishTrial: "result" });
|
||||||
return { my: "result" };
|
return { my: "result" };
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -120,7 +116,7 @@ describe("Trial", () => {
|
|||||||
describe("if `trial` returns no promise", () => {
|
describe("if `trial` returns no promise", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
TestPlugin.prototype.trial.mockImplementation(() => {
|
TestPlugin.prototype.trial.mockImplementation(() => {
|
||||||
jsPsych.finishTrial({ my: "result" });
|
dependencies.finishTrialPromise.resolve({ my: "result" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -130,7 +126,7 @@ describe("Trial", () => {
|
|||||||
await trial.run();
|
await trial.run();
|
||||||
|
|
||||||
expect(onLoadCallback).toHaveBeenCalledTimes(1);
|
expect(onLoadCallback).toHaveBeenCalledTimes(1);
|
||||||
expect(globalCallbacks.onTrialLoaded).toHaveBeenCalledTimes(1);
|
expect(dependencies.onTrialLoaded).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("picks up the result data from the `finishTrial()` function", async () => {
|
it("picks up the result data from the `finishTrial()` function", async () => {
|
||||||
@ -154,8 +150,8 @@ describe("Trial", () => {
|
|||||||
const trial = createTrial({ type: TestPlugin });
|
const trial = createTrial({ type: TestPlugin });
|
||||||
await trial.run();
|
await trial.run();
|
||||||
|
|
||||||
expect(globalCallbacks.onTrialFinished).toHaveBeenCalledTimes(1);
|
expect(dependencies.onTrialFinished).toHaveBeenCalledTimes(1);
|
||||||
expect(globalCallbacks.onTrialFinished).toHaveBeenCalledWith(trial);
|
expect(dependencies.onTrialFinished).toHaveBeenCalledWith(trial);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes result data from the `data` property", async () => {
|
it("includes result data from the `data` property", async () => {
|
||||||
@ -417,7 +413,7 @@ describe("Trial", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("respects `default_iti` and `post_trial_gap``", async () => {
|
it("respects `default_iti` and `post_trial_gap``", async () => {
|
||||||
jest.spyOn(jsPsych, "getInitSettings").mockReturnValue({ default_iti: 100 });
|
dependencies.defaultIti = 100;
|
||||||
TestPlugin.setManualFinishTrialMode();
|
TestPlugin.setManualFinishTrialMode();
|
||||||
|
|
||||||
const trial1 = createTrial({ type: TestPlugin });
|
const trial1 = createTrial({ type: TestPlugin });
|
||||||
@ -456,10 +452,10 @@ describe("Trial", () => {
|
|||||||
|
|
||||||
describe("evaluateTimelineVariable()", () => {
|
describe("evaluateTimelineVariable()", () => {
|
||||||
it("defers to the parent node", () => {
|
it("defers to the parent node", () => {
|
||||||
const timeline = new Timeline(jsPsych, globalCallbacks, { timeline: [] });
|
const timeline = new Timeline(dependencies, { timeline: [] });
|
||||||
mocked(timeline).evaluateTimelineVariable.mockReturnValue(1);
|
mocked(timeline).evaluateTimelineVariable.mockReturnValue(1);
|
||||||
|
|
||||||
const trial = new Trial(jsPsych, globalCallbacks, { type: TestPlugin }, timeline, 0);
|
const trial = new Trial(dependencies, { type: TestPlugin }, timeline, 0);
|
||||||
|
|
||||||
const variable = new TimelineVariable("x");
|
const variable = new TimelineVariable("x");
|
||||||
expect(trial.evaluateTimelineVariable(variable)).toEqual(1);
|
expect(trial.evaluateTimelineVariable(variable)).toEqual(1);
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { JsPsych, JsPsychPlugin, ParameterType, PluginInfo } from "jspsych";
|
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import set from "lodash.set";
|
import set from "lodash.set";
|
||||||
import { ParameterInfos } from "src/modules/plugins";
|
import { ParameterInfos } from "src/modules/plugins";
|
||||||
import { Class } from "type-fest";
|
import { Class } from "type-fest";
|
||||||
|
|
||||||
|
import { JsPsychPlugin, ParameterType, PluginInfo } from "../";
|
||||||
import { deepCopy } from "../modules/utils";
|
import { deepCopy } from "../modules/utils";
|
||||||
import { BaseTimelineNode } from "./BaseTimelineNode";
|
import { BaseTimelineNode } from "./BaseTimelineNode";
|
||||||
import { Timeline } from "./Timeline";
|
import { Timeline } from "./Timeline";
|
||||||
import { delay, parameterPathArrayToString } from "./util";
|
import { delay, parameterPathArrayToString } from "./util";
|
||||||
import {
|
import {
|
||||||
GetParameterValueOptions,
|
GetParameterValueOptions,
|
||||||
GlobalTimelineNodeCallbacks,
|
TimelineNodeDependencies,
|
||||||
TimelineNodeStatus,
|
TimelineNodeStatus,
|
||||||
TimelineVariable,
|
TimelineVariable,
|
||||||
TrialDescription,
|
TrialDescription,
|
||||||
@ -25,16 +25,14 @@ export class Trial extends BaseTimelineNode {
|
|||||||
|
|
||||||
private result: TrialResult;
|
private result: TrialResult;
|
||||||
private readonly pluginInfo: PluginInfo;
|
private readonly pluginInfo: PluginInfo;
|
||||||
private cssClasses?: string[];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
jsPsych: JsPsych,
|
dependencies: TimelineNodeDependencies,
|
||||||
globalCallbacks: GlobalTimelineNodeCallbacks,
|
|
||||||
public readonly description: TrialDescription,
|
public readonly description: TrialDescription,
|
||||||
protected readonly parent: Timeline,
|
protected readonly parent: Timeline,
|
||||||
public readonly index: number
|
public readonly index: number
|
||||||
) {
|
) {
|
||||||
super(jsPsych, globalCallbacks);
|
super(dependencies);
|
||||||
this.trialObject = deepCopy(description);
|
this.trialObject = deepCopy(description);
|
||||||
this.pluginClass = this.getParameterValue("type", { evaluateFunctions: false });
|
this.pluginClass = this.getParameterValue("type", { evaluateFunctions: false });
|
||||||
this.pluginInfo = this.pluginClass["info"];
|
this.pluginInfo = this.pluginClass["info"];
|
||||||
@ -46,7 +44,7 @@ export class Trial extends BaseTimelineNode {
|
|||||||
|
|
||||||
this.onStart();
|
this.onStart();
|
||||||
|
|
||||||
this.pluginInstance = new this.pluginClass(this.jsPsych);
|
this.pluginInstance = this.dependencies.instantiatePlugin(this.pluginClass);
|
||||||
|
|
||||||
const result = await this.executeTrial();
|
const result = await this.executeTrial();
|
||||||
|
|
||||||
@ -59,8 +57,7 @@ export class Trial extends BaseTimelineNode {
|
|||||||
|
|
||||||
this.onFinish();
|
this.onFinish();
|
||||||
|
|
||||||
const gap =
|
const gap = this.getParameterValue("post_trial_gap") ?? this.dependencies.defaultIti;
|
||||||
this.getParameterValue("post_trial_gap") ?? this.jsPsych.getInitSettings().default_iti;
|
|
||||||
if (gap !== 0) {
|
if (gap !== 0) {
|
||||||
await delay(gap);
|
await delay(gap);
|
||||||
}
|
}
|
||||||
@ -69,7 +66,7 @@ export class Trial extends BaseTimelineNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async executeTrial() {
|
private async executeTrial() {
|
||||||
let trialPromise = this.jsPsych.finishTrialPromise.get();
|
const trialPromise = this.dependencies.finishTrialPromise.get();
|
||||||
|
|
||||||
/** Used as a way to figure out if `finishTrial()` has ben called without awaiting `trialPromise` */
|
/** Used as a way to figure out if `finishTrial()` has ben called without awaiting `trialPromise` */
|
||||||
let hasTrialPromiseBeenResolved = false;
|
let hasTrialPromiseBeenResolved = false;
|
||||||
@ -78,13 +75,13 @@ export class Trial extends BaseTimelineNode {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const trialReturnValue = this.pluginInstance.trial(
|
const trialReturnValue = this.pluginInstance.trial(
|
||||||
this.jsPsych.getDisplayElement(),
|
this.dependencies.displayElement,
|
||||||
this.trialObject,
|
this.trialObject,
|
||||||
this.onLoad
|
this.onLoad
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait until the trial has completed and grab result data
|
// Wait until the trial has completed and grab result data
|
||||||
let result: TrialResult;
|
let result: TrialResult | void;
|
||||||
if (isPromise(trialReturnValue)) {
|
if (isPromise(trialReturnValue)) {
|
||||||
result = await Promise.race([trialReturnValue, trialPromise]);
|
result = await Promise.race([trialReturnValue, trialPromise]);
|
||||||
|
|
||||||
@ -115,18 +112,18 @@ export class Trial extends BaseTimelineNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onStart() {
|
private onStart() {
|
||||||
this.globalCallbacks.onTrialStart(this);
|
this.dependencies.onTrialStart(this);
|
||||||
this.runParameterCallback("on_start", this.trialObject);
|
this.runParameterCallback("on_start", this.trialObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onLoad = () => {
|
private onLoad = () => {
|
||||||
this.globalCallbacks.onTrialLoaded(this);
|
this.dependencies.onTrialLoaded(this);
|
||||||
this.runParameterCallback("on_load");
|
this.runParameterCallback("on_load");
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFinish() {
|
private onFinish() {
|
||||||
this.runParameterCallback("on_finish", this.getResult());
|
this.runParameterCallback("on_finish", this.getResult());
|
||||||
this.globalCallbacks.onTrialFinished(this);
|
this.dependencies.onTrialFinished(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluateTimelineVariable(variable: TimelineVariable) {
|
public evaluateTimelineVariable(variable: TimelineVariable) {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Class } from "type-fest";
|
import { Class } from "type-fest";
|
||||||
|
|
||||||
import { JsPsychPlugin } from "../modules/plugins";
|
import { JsPsychPlugin, PluginInfo } from "../modules/plugins";
|
||||||
import { Trial } from "./Trial";
|
import { Trial } from "./Trial";
|
||||||
|
import { PromiseWrapper } from "./util";
|
||||||
|
|
||||||
export function isPromise(value: any): value is Promise<any> {
|
export function isPromise(value: any): value is Promise<any> {
|
||||||
return value && typeof value["then"] === "function";
|
return value && typeof value["then"] === "function";
|
||||||
@ -112,12 +113,11 @@ export enum TimelineNodeStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callbacks that get invoked by `TimelineNode`s. The callbacks are provided by the `JsPsych` class
|
* Functions and options needed by `TimelineNode`s, provided by the `JsPsych` instance. This
|
||||||
* itself to avoid numerous `JsPsych` instance method calls from within timeline nodes, and to keep
|
* approach allows to keep the public `JsPsych` API slim and decouples the `JsPsych` and timeline
|
||||||
* the public `JsPsych` API slim. This approach helps to decouple the `JsPsych` and timeline node
|
* node classes, simplifying unit testing.
|
||||||
* classes and thus simplifies unit testing.
|
|
||||||
*/
|
*/
|
||||||
export interface GlobalTimelineNodeCallbacks {
|
export interface TimelineNodeDependencies {
|
||||||
/**
|
/**
|
||||||
* Called at the start of a trial, prior to invoking the plugin's trial method.
|
* Called at the start of a trial, prior to invoking the plugin's trial method.
|
||||||
*/
|
*/
|
||||||
@ -132,6 +132,29 @@ export interface GlobalTimelineNodeCallbacks {
|
|||||||
* Called after a trial has finished.
|
* Called after a trial has finished.
|
||||||
*/
|
*/
|
||||||
onTrialFinished: (trial: Trial) => void;
|
onTrialFinished: (trial: Trial) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a plugin class, creates a new instance of it and returns it.
|
||||||
|
*/
|
||||||
|
instantiatePlugin: <Info extends PluginInfo>(
|
||||||
|
pluginClass: Class<JsPsychPlugin<Info>>
|
||||||
|
) => JsPsychPlugin<Info>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default inter-trial interval as provided to `initJsPsych`
|
||||||
|
*/
|
||||||
|
defaultIti: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JsPsych's display element which is provided to plugins
|
||||||
|
*/
|
||||||
|
displayElement: HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `PromiseWrapper` whose promise is resolved with result data whenever `jsPsych.finishTrial()`
|
||||||
|
* is called.
|
||||||
|
*/
|
||||||
|
finishTrialPromise: PromiseWrapper<TrialResult | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetParameterValueOptions = {
|
export type GetParameterValueOptions = {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { flushPromises } from "@jspsych/test-utils";
|
import { flushPromises } from "@jspsych/test-utils";
|
||||||
import { JsPsych, JsPsychPlugin, TrialType } from "jspsych";
|
import { JsPsych, JsPsychPlugin, TrialType } from "jspsych";
|
||||||
|
import { TrialResult } from "src/timeline";
|
||||||
|
|
||||||
import { ParameterInfos } from "../src/modules/plugins";
|
import { ParameterInfos } from "../src/modules/plugins";
|
||||||
import { PromiseWrapper } from "../src/timeline/util";
|
import { PromiseWrapper } from "../src/timeline/util";
|
||||||
@ -69,7 +70,7 @@ class TestPlugin implements JsPsychPlugin<typeof testPluginInfo> {
|
|||||||
|
|
||||||
// For convenience, `trial` is set to a `jest.fn` below using `TestPlugin.prototype` and
|
// For convenience, `trial` is set to a `jest.fn` below using `TestPlugin.prototype` and
|
||||||
// `defaultTrialImplementation`
|
// `defaultTrialImplementation`
|
||||||
trial: jest.Mock<Promise<Record<string, any> | void> | void>;
|
trial: jest.Mock<Promise<TrialResult | void> | void>;
|
||||||
|
|
||||||
defaultTrialImplementation(
|
defaultTrialImplementation(
|
||||||
display_element: HTMLElement,
|
display_element: HTMLElement,
|
||||||
|
@ -1,28 +1,46 @@
|
|||||||
import { JsPsych } from "../src";
|
import { Class } from "type-fest";
|
||||||
import { GlobalTimelineNodeCallbacks } from "../src/timeline";
|
|
||||||
|
|
||||||
export function mockDomRelatedJsPsychMethods(jsPsychInstance: JsPsych) {
|
import { JsPsych, JsPsychPlugin } from "../src";
|
||||||
const displayElement = document.createElement("div");
|
import { TimelineNodeDependencies, TrialResult } from "../src/timeline";
|
||||||
const displayContainerElement = document.createElement("div");
|
import { PromiseWrapper } from "../src/timeline/util";
|
||||||
jest.spyOn(jsPsychInstance, "getDisplayElement").mockImplementation(() => displayElement);
|
|
||||||
jest
|
jest.mock("../src/JsPsych");
|
||||||
.spyOn(jsPsychInstance, "getDisplayContainerElement")
|
|
||||||
.mockImplementation(() => displayContainerElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to instantiate mocked `GlobalTimelineNodeCallbacks` objects that have additional
|
* A class to instantiate mocked `TimelineNodeDependencies` objects that have additional
|
||||||
* testing-related functions.
|
* testing-related functions.
|
||||||
*/
|
*/
|
||||||
export class GlobalCallbacks implements GlobalTimelineNodeCallbacks {
|
export class MockTimelineNodeDependencies implements TimelineNodeDependencies {
|
||||||
onTrialStart = jest.fn();
|
onTrialStart = jest.fn();
|
||||||
onTrialLoaded = jest.fn();
|
onTrialLoaded = jest.fn();
|
||||||
onTrialFinished = jest.fn();
|
onTrialFinished = jest.fn();
|
||||||
|
|
||||||
|
instantiatePlugin = jest.fn(
|
||||||
|
(pluginClass: Class<JsPsychPlugin<any>>) => new pluginClass(this.jsPsych)
|
||||||
|
);
|
||||||
|
|
||||||
|
defaultIti: number;
|
||||||
|
displayElement: HTMLDivElement;
|
||||||
|
finishTrialPromise: PromiseWrapper<TrialResult>;
|
||||||
|
jsPsych: JsPsych; // So we have something for plugins in `instantiatePlugin`
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initializeProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeProperties() {
|
||||||
|
this.defaultIti = 0;
|
||||||
|
this.displayElement = document.createElement("div");
|
||||||
|
this.finishTrialPromise = new PromiseWrapper<TrialResult>();
|
||||||
|
this.jsPsych = new JsPsych();
|
||||||
|
}
|
||||||
|
|
||||||
// Test utility functions
|
// Test utility functions
|
||||||
reset() {
|
reset() {
|
||||||
this.onTrialStart.mockReset();
|
this.onTrialStart.mockReset();
|
||||||
this.onTrialLoaded.mockReset();
|
this.onTrialLoaded.mockReset();
|
||||||
this.onTrialFinished.mockReset();
|
this.onTrialFinished.mockReset();
|
||||||
|
this.instantiatePlugin.mockClear();
|
||||||
|
this.initializeProperties();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user