Implement basic result data handling

This commit is contained in:
bjoluc 2022-01-13 21:54:09 +01:00
parent a876d215c0
commit 76a02685d8
6 changed files with 94 additions and 15 deletions

View File

@ -898,7 +898,7 @@ export class JsPsych {
});
};
finishTrial(data: Record<string, any> = {}) {
finishTrial(data?: Record<string, any>) {
this._resolveTrialPromise(data);
this._resetTrialPromise();
}

View File

@ -60,6 +60,9 @@ describe("Timeline", () => {
await timeline.run();
expect(loopFunction).toHaveBeenCalledTimes(2);
expect(loopFunction).toHaveBeenNthCalledWith(1, Array(3).fill({ my: "result" }));
expect(loopFunction).toHaveBeenNthCalledWith(2, Array(6).fill({ my: "result" }));
expect(timeline.children.length).toBe(6);
});
@ -327,4 +330,20 @@ describe("Timeline", () => {
expect(timeline.getParameterValue("object.childObject.childString")).toBe("bar");
});
});
describe("getResults()", () => {
it("recursively returns all results", async () => {
const timeline = new Timeline(jsPsych, exampleTimeline);
await timeline.run();
expect(timeline.getResults()).toEqual(Array(3).fill({ my: "result" }));
});
it("does not include `undefined` results", async () => {
const timeline = new Timeline(jsPsych, exampleTimeline);
await timeline.run();
jest.spyOn(timeline.children[0] as Trial, "getResult").mockReturnValue(undefined);
expect(timeline.getResults()).toEqual(Array(2).fill({ my: "result" }));
});
});
});

View File

@ -1,5 +1,4 @@
import { JsPsych } from "../JsPsych";
import { JsPsychPlugin, PluginInfo } from "../modules/plugins";
import {
repeat,
sampleWithReplacement,
@ -7,7 +6,6 @@ import {
shuffle,
shuffleAlternateGroups,
} from "../modules/randomization";
import { deepCopy } from "../modules/utils";
import { BaseTimelineNode } from "./BaseTimelineNode";
import { Trial } from "./Trial";
import {
@ -46,7 +44,7 @@ export class Timeline extends BaseTimelineNode {
await childNode.run();
}
}
} while (description.loop_function && description.loop_function([])); // TODO What data?
} while (description.loop_function && description.loop_function(this.getResults()));
}
}
}
@ -135,4 +133,23 @@ export class Timeline extends BaseTimelineNode {
}
return super.getParameterValue(parameterName, options);
}
/**
* Returns a flat array containing the results of all nested trials that have results so far
*/
public getResults() {
const results = [];
for (const child of this.children) {
if (child instanceof Trial) {
const childResult = child.getResult();
if (childResult) {
results.push(childResult);
}
} else if (child instanceof Timeline) {
results.push(...child.getResults());
}
}
return results;
}
}

View File

@ -103,10 +103,22 @@ describe("Trial", () => {
expect(onLoadCallback).toHaveBeenCalledTimes(1);
});
it("picks up the result data from the promise", async () => {
const trial = createTrial({ type: TestPlugin });
await trial.run();
expect(trial.resultData).toEqual({ promised: "result" });
it("picks up the result data from the promise or the `finishTrial()` function (where the latter one takes precedence)", async () => {
const trial1 = createTrial({ type: TestPlugin });
await trial1.run();
expect(trial1.getResult()).toEqual({ promised: "result" });
TestPluginMock.prototype.trial.mockImplementation(
async (display_element, trial, on_load) => {
on_load();
jsPsych.finishTrial({ my: "result" });
return { promised: "result" };
}
);
const trial2 = createTrial({ type: TestPlugin });
await trial2.run();
expect(trial2.getResult()).toEqual({ my: "result" });
});
});
@ -123,7 +135,7 @@ describe("Trial", () => {
const trial = createTrial({ type: TestPlugin });
await trial.run();
expect(trial.resultData).toEqual({ my: "result" });
expect(trial.getResult()).toEqual({ my: "result" });
});
});
@ -136,6 +148,12 @@ describe("Trial", () => {
expect(onFinishCallback).toHaveBeenCalledWith({ my: "result" });
});
it("includes result data from the `data` property", async () => {
const trial = createTrial({ type: TestPlugin, data: { custom: "value" } });
await trial.run();
expect(trial.getResult()).toEqual({ my: "result", custom: "value" });
});
describe("with a plugin parameter specification", () => {
const functionDefaultValue = () => {};
beforeEach(() => {

View File

@ -6,15 +6,15 @@ import { BaseTimelineNode } from "./BaseTimelineNode";
import { Timeline } from "./Timeline";
import {
GetParameterValueOptions,
TimelineNode,
TimelineVariable,
TrialDescription,
TrialResult,
isPromise,
trialDescriptionKeys,
} from ".";
export class Trial extends BaseTimelineNode {
resultData: Record<string, any>;
private result: TrialResult;
public pluginInstance: JsPsychPlugin<any>;
public readonly trialObject: TrialDescription;
@ -35,20 +35,35 @@ export class Trial extends BaseTimelineNode {
this.pluginInstance = new this.description.type(this.jsPsych);
let trialPromise = this.jsPsych._trialPromise;
/** Used as a way to figure out if `finishTrial()` has ben called without awaiting `trialPromise` */
let hasTrialPromiseBeenResolved = false;
trialPromise.then(() => {
hasTrialPromiseBeenResolved = true;
});
const trialReturnValue = this.pluginInstance.trial(
this.jsPsych.getDisplayElement() ?? document.createElement("div"), // TODO Remove this hack once getDisplayElement() returns something
this.trialObject,
this.onLoad
);
// Wait until the trial has completed and grab result data
let result: TrialResult;
if (isPromise(trialReturnValue)) {
trialPromise = trialReturnValue;
result = await Promise.race([trialReturnValue, trialPromise]);
// If `finishTrial()` was called, use the result provided to it. This may happen although
// `trialReturnValue` won the race ("run-to-completion").
if (hasTrialPromiseBeenResolved) {
result = await trialPromise;
}
} else {
this.onLoad();
result = await trialPromise;
}
// Wait until the trial has completed and grab result data
this.resultData = (await trialPromise) ?? {};
this.result = { ...this.trialObject.data, ...result };
this.onFinish();
}
@ -67,7 +82,7 @@ export class Trial extends BaseTimelineNode {
private onFinish() {
if (this.description.on_finish) {
this.description.on_finish(this.resultData);
this.description.on_finish(this.getResult());
}
}
@ -84,6 +99,13 @@ export class Trial extends BaseTimelineNode {
return super.getParameterValue(parameterName, options);
}
/**
* Returns the result object of this trial or `undefined` if the result is not yet known.
*/
public getResult() {
return this.result;
}
/**
* Checks that the parameters provided in the trial description align with the plugin's info
* object, resolves missing parameter values from the parent timeline, resolves timeline variable

View File

@ -136,3 +136,6 @@ export interface TimelineNode {
*/
getParameterValue(parameterName: string, options?: GetParameterValueOptions): any;
}
export type TrialResult = Record<string, any>;
export type TrialResults = Array<Record<string, any>>;