diff --git a/.changeset/clean-insects-juggle.md b/.changeset/clean-insects-juggle.md new file mode 100644 index 00000000..4a55d011 --- /dev/null +++ b/.changeset/clean-insects-juggle.md @@ -0,0 +1,5 @@ +--- +"jspsych": patch +--- + +Fix error in how nested parameters were handled in simulation mode, #2911 diff --git a/.changeset/cuddly-mails-bathe.md b/.changeset/cuddly-mails-bathe.md new file mode 100644 index 00000000..a34fc673 --- /dev/null +++ b/.changeset/cuddly-mails-bathe.md @@ -0,0 +1,5 @@ +--- +"@jspsych/plugin-instructions": patch +--- + +Fix simulation mode behavior so that setting RT and/or view_history correctly sets the other parameter diff --git a/.changeset/few-badgers-rescue.md b/.changeset/few-badgers-rescue.md new file mode 100644 index 00000000..95dba77c --- /dev/null +++ b/.changeset/few-badgers-rescue.md @@ -0,0 +1,5 @@ +--- +"jspsych": patch +--- + +Fixed how simulation mode handles `setTimeout` calls to ensure that timeouts are cleared at the end of a trial, even in cases where the user interacts with a simulated trial when the simulation is being run in `visual` mode. diff --git a/.changeset/funny-guests-rhyme.md b/.changeset/funny-guests-rhyme.md new file mode 100644 index 00000000..60e5f774 --- /dev/null +++ b/.changeset/funny-guests-rhyme.md @@ -0,0 +1,5 @@ +--- +"@jspsych/plugin-serial-reaction-time": patch +--- + +Fixed issue that caused `pre_target_duration` parameter to not work correctly diff --git a/.changeset/mean-ads-clap.md b/.changeset/mean-ads-clap.md new file mode 100644 index 00000000..fd254593 --- /dev/null +++ b/.changeset/mean-ads-clap.md @@ -0,0 +1,5 @@ +--- +"@jspsych/plugin-fullscreen": minor +--- + +Plugin now records RT of the button press to launch fullscreen mode and simulation mode supports setting this property diff --git a/.changeset/soft-cameras-lie.md b/.changeset/soft-cameras-lie.md new file mode 100644 index 00000000..71f4e6bf --- /dev/null +++ b/.changeset/soft-cameras-lie.md @@ -0,0 +1,5 @@ +--- +"jspsych": patch +--- + +Fixed issue where a trial's `on_load` was not called when using simulation mode but setting a trial's `simulate` option to `false`. diff --git a/.changeset/three-kangaroos-speak.md b/.changeset/three-kangaroos-speak.md new file mode 100644 index 00000000..2293f8e7 --- /dev/null +++ b/.changeset/three-kangaroos-speak.md @@ -0,0 +1,5 @@ +--- +"jspsych": patch +--- + +Fix target of simulation `dispatchEvent` so that simulation mode works with custom `display_element` diff --git a/docs/plugins/fullscreen.md b/docs/plugins/fullscreen.md index e056dc82..6a5a039a 100644 --- a/docs/plugins/fullscreen.md +++ b/docs/plugins/fullscreen.md @@ -25,6 +25,7 @@ In addition to the [default data collected by all plugins](../overview/plugins.m Name | Type | Value -----|------|------ success | boolean | true if the browser supports fullscreen mode (i.e., is not Safari) +rt | number | Response time to click the button that launches fullscreen mode ## Simulation Mode diff --git a/packages/jspsych/src/JsPsych.ts b/packages/jspsych/src/JsPsych.ts index 7463750d..b1395318 100644 --- a/packages/jspsych/src/JsPsych.ts +++ b/packages/jspsych/src/JsPsych.ts @@ -627,13 +627,14 @@ export class JsPsych { }; let trial_complete; + let trial_sim_opts; + let trial_sim_opts_merged; if (!this.simulation_mode) { trial_complete = trial.type.trial(this.DOM_target, trial, load_callback); } if (this.simulation_mode) { // check if the trial supports simulation if (trial.type.simulate) { - let trial_sim_opts; if (!trial.simulation_options) { trial_sim_opts = this.simulation_options.default; } @@ -656,16 +657,23 @@ export class JsPsych { trial_sim_opts = trial.simulation_options; } } - trial_sim_opts = this.utils.deepCopy(trial_sim_opts); - trial_sim_opts = this.replaceFunctionsWithValues(trial_sim_opts, null); + // merge in default options that aren't overriden by the trial's simulation_options + // including nested parameters in the simulation_options + trial_sim_opts_merged = this.utils.deepMerge( + this.simulation_options.default, + trial_sim_opts + ); - if (trial_sim_opts?.simulate === false) { + trial_sim_opts_merged = this.utils.deepCopy(trial_sim_opts_merged); + trial_sim_opts_merged = this.replaceFunctionsWithValues(trial_sim_opts_merged, null); + + if (trial_sim_opts_merged?.simulate === false) { trial_complete = trial.type.trial(this.DOM_target, trial, load_callback); } else { trial_complete = trial.type.simulate( trial, - trial_sim_opts?.mode || this.simulation_mode, - trial_sim_opts, + trial_sim_opts_merged?.mode || this.simulation_mode, + trial_sim_opts_merged, load_callback ); } @@ -678,8 +686,13 @@ export class JsPsych { // see if trial_complete is a Promise by looking for .then() function const is_promise = trial_complete && typeof trial_complete.then == "function"; - // in simulation mode we let the simulate function call the load_callback always. - if (!is_promise && !this.simulation_mode) { + // in simulation mode we let the simulate function call the load_callback always, + // so we don't need to call it here. however, if we are in simulation mode but not simulating + // this particular trial we need to call it. + if ( + !is_promise && + (!this.simulation_mode || (this.simulation_mode && trial_sim_opts_merged?.simulate === false)) + ) { load_callback(); } diff --git a/packages/jspsych/src/modules/plugin-api/SimulationAPI.ts b/packages/jspsych/src/modules/plugin-api/SimulationAPI.ts index 94288c16..124a5905 100644 --- a/packages/jspsych/src/modules/plugin-api/SimulationAPI.ts +++ b/packages/jspsych/src/modules/plugin-api/SimulationAPI.ts @@ -1,6 +1,11 @@ export class SimulationAPI { + constructor( + private getDisplayContainerElement: () => HTMLElement, + private setJsPsychTimeout: (callback: () => void, delay: number) => number + ) {} + dispatchEvent(event: Event) { - document.body.dispatchEvent(event); + this.getDisplayContainerElement().dispatchEvent(event); } /** @@ -26,7 +31,7 @@ export class SimulationAPI { */ pressKey(key: string, delay = 0) { if (delay > 0) { - setTimeout(() => { + this.setJsPsychTimeout(() => { this.keyDown(key); this.keyUp(key); }, delay); @@ -43,7 +48,7 @@ export class SimulationAPI { */ clickTarget(target: Element, delay = 0) { if (delay > 0) { - setTimeout(() => { + this.setJsPsychTimeout(() => { target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true })); target.dispatchEvent(new MouseEvent("click", { bubbles: true })); @@ -63,7 +68,7 @@ export class SimulationAPI { */ fillTextInput(target: HTMLInputElement, text: string, delay = 0) { if (delay > 0) { - setTimeout(() => { + this.setJsPsychTimeout(() => { target.value = text; }, delay); } else { diff --git a/packages/jspsych/src/modules/plugin-api/TimeoutAPI.ts b/packages/jspsych/src/modules/plugin-api/TimeoutAPI.ts index 913aca23..aebec16b 100644 --- a/packages/jspsych/src/modules/plugin-api/TimeoutAPI.ts +++ b/packages/jspsych/src/modules/plugin-api/TimeoutAPI.ts @@ -1,13 +1,25 @@ +/** + * A class that provides a wrapper around the global setTimeout and clearTimeout functions. + */ export class TimeoutAPI { - private timeout_handlers = []; + private timeout_handlers: number[] = []; - setTimeout(callback, delay) { + /** + * Calls a function after a specified delay, in milliseconds. + * @param callback The function to call after the delay. + * @param delay The number of milliseconds to wait before calling the function. + * @returns A handle that can be used to clear the timeout with clearTimeout. + */ + setTimeout(callback: () => void, delay: number): number { const handle = window.setTimeout(callback, delay); this.timeout_handlers.push(handle); return handle; } - clearAllTimeouts() { + /** + * Clears all timeouts that have been created with setTimeout. + */ + clearAllTimeouts(): void { for (const handler of this.timeout_handlers) { clearTimeout(handler); } diff --git a/packages/jspsych/src/modules/plugin-api/index.ts b/packages/jspsych/src/modules/plugin-api/index.ts index 5da34514..fc66a500 100644 --- a/packages/jspsych/src/modules/plugin-api/index.ts +++ b/packages/jspsych/src/modules/plugin-api/index.ts @@ -9,19 +9,22 @@ import { TimeoutAPI } from "./TimeoutAPI"; export function createJointPluginAPIObject(jsPsych: JsPsych) { const settings = jsPsych.getInitSettings(); + const keyboardListenerAPI = autoBind( + new KeyboardListenerAPI( + jsPsych.getDisplayContainerElement, + settings.case_sensitive_responses, + settings.minimum_valid_rt + ) + ); + const timeoutAPI = autoBind(new TimeoutAPI()); + const mediaAPI = autoBind(new MediaAPI(settings.use_webaudio, jsPsych.webaudio_context)); + const hardwareAPI = autoBind(new HardwareAPI()); + const simulationAPI = autoBind( + new SimulationAPI(jsPsych.getDisplayContainerElement, timeoutAPI.setTimeout) + ); return Object.assign( {}, - ...[ - new KeyboardListenerAPI( - jsPsych.getDisplayContainerElement, - settings.case_sensitive_responses, - settings.minimum_valid_rt - ), - new TimeoutAPI(), - new MediaAPI(settings.use_webaudio, jsPsych.webaudio_context), - new HardwareAPI(), - new SimulationAPI(), - ].map((object) => autoBind(object)) + ...[keyboardListenerAPI, timeoutAPI, mediaAPI, hardwareAPI, simulationAPI] ) as KeyboardListenerAPI & TimeoutAPI & MediaAPI & HardwareAPI & SimulationAPI; } diff --git a/packages/jspsych/src/modules/utils.ts b/packages/jspsych/src/modules/utils.ts index 19dbdb20..15e6efd1 100644 --- a/packages/jspsych/src/modules/utils.ts +++ b/packages/jspsych/src/modules/utils.ts @@ -28,3 +28,34 @@ export function deepCopy(obj) { return obj; } } + +/** + * Merges two objects, recursively. + * @param obj1 Object to merge + * @param obj2 Object to merge + */ +export function deepMerge(obj1: any, obj2: any): any { + let merged = {}; + for (const key in obj1) { + if (obj1.hasOwnProperty(key)) { + if (typeof obj1[key] === "object" && obj2.hasOwnProperty(key)) { + merged[key] = deepMerge(obj1[key], obj2[key]); + } else { + merged[key] = obj1[key]; + } + } + } + for (const key in obj2) { + if (obj2.hasOwnProperty(key)) { + if (!merged.hasOwnProperty(key)) { + merged[key] = obj2[key]; + } else if (typeof obj2[key] === "object") { + merged[key] = deepMerge(merged[key], obj2[key]); + } else { + merged[key] = obj2[key]; + } + } + } + + return merged; +} diff --git a/packages/jspsych/tests/core/simulation-mode.test.ts b/packages/jspsych/tests/core/simulation-mode.test.ts index 3567d560..da1a3b03 100644 --- a/packages/jspsych/tests/core/simulation-mode.test.ts +++ b/packages/jspsych/tests/core/simulation-mode.test.ts @@ -414,4 +414,200 @@ describe("data simulation mode", () => { expect(getData().values().length).toBe(2); }); + + test("Custom display_element in initJsPsych does not prevent simulation events #3008", async () => { + const target = document.createElement("div"); + target.id = "target"; + document.body.appendChild(target); + + const jsPsych = initJsPsych({ + display_element: target, + }); + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + }, + ]; + + const { expectRunning, expectFinished, getHTML } = await simulateTimeline( + timeline, + "visual", + {}, + jsPsych + ); + + await expectRunning(); + + expect(getHTML()).toContain("foo"); + + jest.runAllTimers(); + + await expectFinished(); + }); + + test("Data parameters should be merged when setting trial-level simulation options, #2911", async () => { + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + trial_duration: 1000, + response_ends_trial: true, + }, + { + type: htmlKeyboardResponse, + stimulus: "bar", + trial_duration: 1000, + response_ends_trial: true, + simulation_options: { + data: { + response: "a", + }, + }, + }, + ]; + + const { expectRunning, expectFinished, getData } = await simulateTimeline(timeline, "visual", { + default: { data: { rt: 200 } }, + }); + + jest.runAllTimers(); + + await expectFinished(); + + const data = getData().values(); + + expect(data[0].rt).toBe(200); + expect(data[1].rt).toBe(200); + expect(data[1].response).toBe("a"); + }); + + test("Simulation mode set via string should work, #2912", async () => { + const simulation_options = { + default: { + simulate: false, + data: { + rt: 200, + }, + }, + long_response: { + simulate: true, + }, + }; + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + }, + { + type: htmlKeyboardResponse, + stimulus: "bar", + trial_duration: 1000, + simulation_options: "long_response", + }, + ]; + + const { expectRunning, expectFinished, getData, getHTML } = await simulateTimeline( + timeline, + "visual", + simulation_options + ); + + await expectRunning(); + + expect(getHTML()).toContain("foo"); + + jest.runAllTimers(); + + expect(getHTML()).toContain("foo"); + + pressKey("a"); + + expect(getHTML()).toContain("bar"); + + jest.runAllTimers(); + + await expectFinished(); + + const data = getData().values()[1]; + + console.log(data); + + expect(data.rt).toBeGreaterThan(0); + expect(data.response).toBeDefined(); + }); + + test("Simulation timeouts are handled correctly when user interacts with simulation, #2862", async () => { + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + simulation_options: { + data: { + rt: 1000, + }, + }, + }, + { + type: htmlKeyboardResponse, + stimulus: "bar", + simulation_options: { + data: { + rt: 1000, + }, + }, + }, + ]; + + const { expectRunning, expectFinished, getHTML } = await simulateTimeline(timeline, "visual"); + + await expectRunning(); + + expect(getHTML()).toContain("foo"); + + jest.advanceTimersByTime(500); + + expect(getHTML()).toContain("foo"); + + pressKey("a"); // this is the user responding instead of letting the simulation handle it. + + expect(getHTML()).toContain("bar"); + + jest.advanceTimersByTime(800); + + // if the timeout from the first trial is blocked, this trial shouldn't finish yet. + expect(getHTML()).toContain("bar"); + + // this should be the end of the experiment + jest.advanceTimersByTime(201); + + await expectFinished(); + }); + + test("`on_load` function should be called when in simulation mode and `simulate` is `false`, #2859", async () => { + const on_load = jest.fn(); + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + simulation_options: { + simulate: false, + }, + on_load, + }, + ]; + + const { expectRunning, expectFinished } = await simulateTimeline(timeline, "visual"); + + await expectRunning(); + + expect(on_load).toHaveBeenCalled(); + + pressKey("a"); + + await expectFinished(); + }); }); diff --git a/packages/jspsych/tests/utils/utils.test.ts b/packages/jspsych/tests/utils/utils.test.ts index dac8bd36..e1227f96 100644 --- a/packages/jspsych/tests/utils/utils.test.ts +++ b/packages/jspsych/tests/utils/utils.test.ts @@ -1,4 +1,4 @@ -import { deepCopy, unique } from "../../src/modules/utils"; +import { deepCopy, deepMerge, unique } from "../../src/modules/utils"; describe("unique", () => { test("generates unique array when there are duplicates", () => { @@ -44,3 +44,53 @@ describe("deepCopy", () => { expect(o2.b()).toBe(1); }); }); + +describe("deepMerge", () => { + it("should merge two objects with nested properties", () => { + const obj1 = { a: 1, b: { c: { d: 1 } } }; + const obj2 = { b: { c: { e: 2 } }, f: 3 }; + const expected = { a: 1, b: { c: { d: 1, e: 2 } }, f: 3 }; + const result = deepMerge(obj1, obj2); + expect(result).toEqual(expected); + }); + + it("should overwrite properties in obj1 with properties in obj2", () => { + const obj1 = { a: 1, b: { c: { d: 1 } } }; + const obj2 = { a: 2, b: { c: { d: 2 } } }; + const expected = { a: 2, b: { c: { d: 2 } } }; + const result = deepMerge(obj1, obj2); + expect(result).toEqual(expected); + }); + + it("should handle null and undefined values", () => { + const obj1 = { a: null, b: { c: undefined } }; + const obj2 = { a: 1, b: { c: { d: 1 } } }; + const expected = { a: 1, b: { c: { d: 1 } } }; + const result = deepMerge(obj1, obj2); + expect(result).toEqual(expected); + }); + + it("should handle empty objects", () => { + const obj1 = { a: 1, b: {} }; + const obj2 = { b: { c: 2 } }; + const expected = { a: 1, b: { c: 2 } }; + const result = deepMerge(obj1, obj2); + expect(result).toEqual(expected); + }); + + it("should handle when one property is an object and the corresponding property is not", () => { + const obj1 = { a: 1, b: { c: { d: 1 } } }; + const obj2 = { a: 2, b: 3 }; + const expected = { a: 2, b: 3 }; + const result = deepMerge(obj1, obj2); + expect(result).toEqual(expected); + }); + + it("should handle when one property is an object and the corresponding property is not, reversed", () => { + const obj1 = { a: 1, b: { c: { d: 1 } } }; + const obj2 = { a: 2, b: 3 }; + const expected = { a: 1, b: { c: { d: 1 } } }; + const result = deepMerge(obj2, obj1); + expect(result).toEqual(expected); + }); +}); diff --git a/packages/plugin-fullscreen/src/index.spec.ts b/packages/plugin-fullscreen/src/index.spec.ts index 3a3a4828..19f0af7d 100644 --- a/packages/plugin-fullscreen/src/index.spec.ts +++ b/packages/plugin-fullscreen/src/index.spec.ts @@ -23,6 +23,23 @@ describe("fullscreen plugin", () => { clickTarget(document.querySelector("#jspsych-fullscreen-btn")); expect(document.documentElement.requestFullscreen).toHaveBeenCalled(); }); + + test("records RT of click", async () => { + const { getData, expectFinished } = await startTimeline([ + { + type: fullscreen, + delay_after: 0, + }, + ]); + + expect(document.documentElement.requestFullscreen).not.toHaveBeenCalled(); + jest.advanceTimersByTime(1000); + clickTarget(document.querySelector("#jspsych-fullscreen-btn")); + expect(document.documentElement.requestFullscreen).toHaveBeenCalled(); + jest.runAllTimers(); + await expectFinished(); + expect(getData().values()[0].rt).toBeGreaterThanOrEqual(1000); + }); }); describe("fullscreen plugin simulation", () => { @@ -63,5 +80,6 @@ describe("fullscreen plugin simulation", () => { await expectFinished(); expect(getData().values()[0].success).toBe(true); + expect(getData().values()[0].rt).toBeGreaterThan(0); }); }); diff --git a/packages/plugin-fullscreen/src/index.ts b/packages/plugin-fullscreen/src/index.ts index 2d0e638e..4978d8df 100644 --- a/packages/plugin-fullscreen/src/index.ts +++ b/packages/plugin-fullscreen/src/index.ts @@ -47,6 +47,8 @@ type Info = typeof info; */ class FullscreenPlugin implements JsPsychPlugin { static info = info; + private rt = null; + private start_time = 0; constructor(private jsPsych: JsPsych) {} @@ -73,9 +75,11 @@ class FullscreenPlugin implements JsPsychPlugin { `; display_element.querySelector("#jspsych-fullscreen-btn").addEventListener("click", () => { + this.rt = Math.round(performance.now() - this.start_time); this.enterFullScreen(); this.endTrial(display_element, true, trial); }); + this.start_time = performance.now(); } private endTrial(display_element, success, trial) { @@ -84,6 +88,7 @@ class FullscreenPlugin implements JsPsychPlugin { this.jsPsych.pluginAPI.setTimeout(() => { var trial_data = { success: success, + rt: this.rt, }; this.jsPsych.finishTrial(trial_data); @@ -137,8 +142,11 @@ class FullscreenPlugin implements JsPsychPlugin { } private create_simulation_data(trial: TrialType, simulation_options) { + const rt = this.jsPsych.randomization.sampleExGaussian(1000, 100, 1 / 200, true); + const default_data = { success: true, + rt: rt, }; const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options); @@ -164,7 +172,7 @@ class FullscreenPlugin implements JsPsychPlugin { load_callback(); this.jsPsych.pluginAPI.clickTarget( display_element.querySelector("#jspsych-fullscreen-btn"), - this.jsPsych.randomization.sampleExGaussian(1000, 100, 1 / 200, true) + data.rt ); } } diff --git a/packages/plugin-instructions/src/index.spec.ts b/packages/plugin-instructions/src/index.spec.ts index 2480b3df..d43b4c92 100644 --- a/packages/plugin-instructions/src/index.spec.ts +++ b/packages/plugin-instructions/src/index.spec.ts @@ -104,4 +104,63 @@ describe("instructions plugin simulation", () => { expect(data.view_history.length).toBeGreaterThanOrEqual(6); expect(data.view_history[data.view_history.length - 1].page_index).toBe(5); }); + + test("Setting RT correctly sets the total length of the trial, #2462", async () => { + const timeline = [ + { + type: instructions, + pages: ["page 1", "page 2", "page 3"], + simulation_options: { + data: { + rt: 4000, + }, + }, + }, + ]; + + const { getData, expectFinished } = await simulateTimeline(timeline); + + await expectFinished(); + + const data = getData().values()[0]; + + console.log(data.view_history); + + expect(data.rt).toBe(4000); + + let sum_view_history_rt = 0; + for (const view of data.view_history) { + sum_view_history_rt += view.viewing_time; + } + + // this may not be exactly 4000 due to rounding errors + + expect(Math.abs(sum_view_history_rt - 4000)).toBeLessThan(10); + }); + + test("Setting view history correctly sets the total RT, #2462", async () => { + const timeline = [ + { + type: instructions, + pages: ["page 1", "page 2", "page 3"], + simulation_options: { + data: { + view_history: [ + { page_index: 0, viewing_time: 1000 }, + { page_index: 1, viewing_time: 1000 }, + { page_index: 2, viewing_time: 1000 }, + ], + }, + }, + }, + ]; + + const { getData, expectFinished } = await simulateTimeline(timeline); + + await expectFinished(); + + const data = getData().values()[0]; + + expect(data.rt).toBe(3000); + }); }); diff --git a/packages/plugin-instructions/src/index.ts b/packages/plugin-instructions/src/index.ts index 832944c6..3dbb25e2 100644 --- a/packages/plugin-instructions/src/index.ts +++ b/packages/plugin-instructions/src/index.ts @@ -258,23 +258,74 @@ class InstructionsPlugin implements JsPsychPlugin { private create_simulation_data(trial: TrialType, simulation_options) { let curr_page = 0; let rt = 0; - const view_history = []; + let view_history = []; - while (curr_page !== trial.pages.length) { - const view_time = this.jsPsych.randomization.sampleExGaussian(3000, 300, 1 / 300); - view_history.push({ page_index: curr_page, viewing_time: view_time }); - rt += view_time; - if (curr_page == 0 || !trial.allow_backward) { - curr_page++; - } else { - if (this.jsPsych.randomization.sampleBernoulli(0.9) == 1) { + // if there is no view history and no RT, simulate a random walk through the pages + if (!simulation_options.data?.view_history && !simulation_options.data?.rt) { + while (curr_page !== trial.pages.length) { + const view_time = Math.round( + this.jsPsych.randomization.sampleExGaussian(3000, 300, 1 / 300) + ); + view_history.push({ page_index: curr_page, viewing_time: view_time }); + rt += view_time; + if (curr_page == 0 || !trial.allow_backward) { curr_page++; } else { - curr_page--; + if (this.jsPsych.randomization.sampleBernoulli(0.9) == 1) { + curr_page++; + } else { + curr_page--; + } } } } + // if there is an RT but no view history, simulate a random walk through the pages + // that ends on the final page when the RT is reached + if (!simulation_options.data?.view_history && simulation_options.data?.rt) { + rt = simulation_options.data.rt; + while (curr_page !== trial.pages.length) { + view_history.push({ page_index: curr_page, viewing_time: null }); + if (curr_page == 0 || !trial.allow_backward) { + curr_page++; + } else { + if (this.jsPsych.randomization.sampleBernoulli(0.9) == 1) { + curr_page++; + } else { + curr_page--; + } + } + } + const avg_rt_per_page = simulation_options.data.rt / view_history.length; + let total_time = 0; + for (const page of view_history) { + const t = Math.round( + this.jsPsych.randomization.sampleExGaussian( + avg_rt_per_page, + avg_rt_per_page / 10, + 1 / (avg_rt_per_page / 10) + ) + ); + page.viewing_time = t; + total_time += t; + } + const diff = simulation_options.data.rt - total_time; + // remove equal diff from each page + const diff_per_page = Math.round(diff / view_history.length); + for (const page of view_history) { + page.viewing_time += diff_per_page; + } + } + + // if there is a view history but no RT, make the RT equal the sum of the view history + if (simulation_options.data?.view_history && !simulation_options.data?.rt) { + view_history = simulation_options.data.view_history; + rt = 0; + for (const page of simulation_options.data.view_history) { + rt += page.viewing_time; + } + } + const default_data = { view_history: view_history, rt: rt, diff --git a/packages/plugin-serial-reaction-time/src/index.ts b/packages/plugin-serial-reaction-time/src/index.ts index 0b092de2..c605832d 100644 --- a/packages/plugin-serial-reaction-time/src/index.ts +++ b/packages/plugin-serial-reaction-time/src/index.ts @@ -216,7 +216,7 @@ class SerialReactionTimePlugin implements JsPsychPlugin { if (trial.pre_target_duration <= 0) { showTarget(); } else { - this.jsPsych.pluginAPI.setTimeout(showTarget(), trial.pre_target_duration); + this.jsPsych.pluginAPI.setTimeout(showTarget, trial.pre_target_duration); } //show prompt if there is one