diff --git a/jspsych.js b/jspsych.js index 941ae0d4..50c06581 100755 --- a/jspsych.js +++ b/jspsych.js @@ -505,10 +505,11 @@ window.jsPsych = (function() { // if node has not started yet (progress.current_location == -1), // then try to start the node. if (progress.current_location == -1) { - // check for conditonal function on nodes with timelines + // check for on_timeline_start and conditonal function on nodes with timelines if (typeof timeline_parameters != 'undefined') { - if (typeof timeline_parameters.conditional_function !== 'undefined') { - jsPsych.internal.call_immediate = true; + // only run the conditional function if this is the first repetition of the timeline when + // repetitions > 1, and only when on the first variable set + if (typeof timeline_parameters.conditional_function !== 'undefined' && progress.current_repetition==0 && progress.current_variable_set == 0) { var conditional_result = timeline_parameters.conditional_function(); jsPsych.internal.call_immediate = false; // if the conditional_function() returns false, then the timeline @@ -517,17 +518,23 @@ window.jsPsych = (function() { progress.done = true; return true; } - // if the conditonal_function() returns true, then the node can start - else { - progress.current_location = 0; - } + // // if the conditonal_function() returns true, then the node can start + // else { + // progress.current_location = 0; + // } } - // if there is no conditional_function, then the node can start - else { - progress.current_location = 0; + // if we reach this point then the node has its own timeline and will start + // so we need to check if there is an on_timeline_start function + if (typeof timeline_parameters.on_timeline_start !== 'undefined'){ + timeline_parameters.on_timeline_start(); } + // // if there is no conditional_function, then the node can start + // else { + // progress.current_location = 0; + // } } - // if the node does not have a timeline, then it can start + // if we reach this point, then either the node doesn't have a timeline of the + // conditional function returned true and it can start progress.current_location = 0; // call advance again on this node now that it is pointing to a new location return this.advance(); @@ -552,6 +559,7 @@ window.jsPsych = (function() { } // if we've reached the end of the timeline (which, if the code is here, we have) + // there are a few steps to see what to do next... // first, check the timeline_variables to see if we need to loop through again @@ -566,29 +574,41 @@ window.jsPsych = (function() { // if we're all done with the timeline_variables, then check to see if there are more repetitions else if (progress.current_repetition < timeline_parameters.repetitions - 1) { this.nextRepetiton(); + // check to see if there is an on_timeline_finish function + if (typeof timeline_parameters.on_timeline_finish !== 'undefined'){ + timeline_parameters.on_timeline_finish(); + } return this.advance(); } - // if we're all done with the repetitions, check if there is a loop function. - else if (typeof timeline_parameters.loop_function !== 'undefined') { - jsPsych.internal.call_immediate = true; - if (timeline_parameters.loop_function(this.generatedData())) { - this.reset(); - jsPsych.internal.call_immediate = false; - return parent_node.advance(); - } else { - progress.done = true; - jsPsych.internal.call_immediate = false; - return true; + + // if we're all done with the repetitions... + else { + // check to see if there is an on_timeline_finish function + if (typeof timeline_parameters.on_timeline_finish !== 'undefined'){ + timeline_parameters.on_timeline_finish(); } + + // if we're all done with the repetitions, check if there is a loop function. + if (typeof timeline_parameters.loop_function !== 'undefined') { + jsPsych.internal.call_immediate = true; + if (timeline_parameters.loop_function(this.generatedData())) { + this.reset(); + jsPsych.internal.call_immediate = false; + return parent_node.advance(); + } else { + progress.done = true; + jsPsych.internal.call_immediate = false; + return true; + } + } + + } // no more loops on this timeline, we're done! - else { - progress.done = true; - return true; - } - + progress.done = true; + return true; } } @@ -818,12 +838,16 @@ window.jsPsych = (function() { sample: parameters.sample, randomize_order: typeof parameters.randomize_order == 'undefined' ? false : parameters.randomize_order, repetitions: typeof parameters.repetitions == 'undefined' ? 1 : parameters.repetitions, - timeline_variables: typeof parameters.timeline_variables == 'undefined' ? [{}] : parameters.timeline_variables + timeline_variables: typeof parameters.timeline_variables == 'undefined' ? [{}] : parameters.timeline_variables, + on_timeline_finish: parameters.on_timeline_finish, + on_timeline_start: parameters.on_timeline_start, }; self.setTimelineVariablesOrder(); // extract all of the node level data and parameters + // but remove all of the timeline-level specific information + // since this will be used to copy things down hierarchically var node_data = Object.assign({}, parameters); delete node_data.timeline; delete node_data.conditional_function; @@ -832,6 +856,8 @@ window.jsPsych = (function() { delete node_data.repetitions; delete node_data.timeline_variables; delete node_data.sample; + delete node_data.on_timeline_start; + delete node_data.on_timeline_finish; node_trial_data = node_data; // store for later... // create a TimelineNode for each element in the timeline diff --git a/tests/jsPsych/events.test.js b/tests/jsPsych/events.test.js index d2c778e2..d88cd6a1 100644 --- a/tests/jsPsych/events.test.js +++ b/tests/jsPsych/events.test.js @@ -367,3 +367,175 @@ describe('on_trial_start', function(){ utils.pressKey('a'); }); }); + +describe('on_timeline_finish', function(){ + test('should fire once when timeline is complete', function(){ + + var on_finish_fn = jest.fn(); + + var mini_timeline = { + timeline: [ + { + type: 'html-keyboard-response', + stimulus: 'foo' + }, + { + type: 'html-keyboard-response', + stimulus: 'foo' + }, + { + type: 'html-keyboard-response', + stimulus: 'foo' + } + ], + on_timeline_finish: on_finish_fn + } + + jsPsych.init({timeline: [mini_timeline]}); + + utils.pressKey(32); + expect(on_finish_fn).not.toHaveBeenCalled(); + utils.pressKey(32); + expect(on_finish_fn).not.toHaveBeenCalled(); + utils.pressKey(32); + expect(on_finish_fn).toHaveBeenCalled(); + }); + + test('should fire once even with timeline variables', function(){ + + var on_finish_fn = jest.fn(); + + var tvs = [{ + x: 1, + x: 2, + }] + + var mini_timeline = { + timeline: [ + { + type: 'html-keyboard-response', + stimulus: 'foo' + } + ], + on_timeline_finish: on_finish_fn, + timeline_variables: tvs + } + + jsPsych.init({timeline: [mini_timeline]}); + + utils.pressKey(32); + utils.pressKey(32); + expect(on_finish_fn.mock.calls.length).toBe(1); + + }) + + test('should fire on every repetition', function(){ + + var on_finish_fn = jest.fn(); + + var mini_timeline = { + timeline: [ + { + type: 'html-keyboard-response', + stimulus: 'foo' + } + ], + on_timeline_finish: on_finish_fn, + repetitions: 2 + } + + jsPsych.init({timeline: [mini_timeline]}); + + utils.pressKey(32); + utils.pressKey(32); + expect(on_finish_fn.mock.calls.length).toBe(2); + + }) +}) + +describe('on_timeline_start', function(){ + test('should fire once when timeline starts', function(){ + + var on_start_fn = jest.fn(); + + var mini_timeline = { + timeline: [ + { + type: 'html-keyboard-response', + stimulus: 'foo' + }, + { + type: 'html-keyboard-response', + stimulus: 'foo' + }, + { + type: 'html-keyboard-response', + stimulus: 'foo' + } + ], + on_timeline_start: on_start_fn + } + + jsPsych.init({timeline: [mini_timeline]}); + + expect(on_start_fn).toHaveBeenCalled(); + utils.pressKey(32); + utils.pressKey(32); + utils.pressKey(32); + expect(on_start_fn.mock.calls.length).toBe(1); + + }) + + test('should fire once even with timeline variables', function(){ + + var on_start_fn = jest.fn(); + + var tvs = [{ + x: 1, + x: 2, + }] + + var mini_timeline = { + timeline: [ + { + type: 'html-keyboard-response', + stimulus: 'foo' + } + ], + on_timeline_start: on_start_fn, + timeline_variables: tvs + } + + jsPsych.init({timeline: [mini_timeline]}); + + expect(on_start_fn).toHaveBeenCalled(); + utils.pressKey(32); + utils.pressKey(32); + expect(on_start_fn.mock.calls.length).toBe(1); + + }) + + test('should fire on every repetition', function(){ + + var on_start_fn = jest.fn(); + + var mini_timeline = { + timeline: [ + { + type: 'html-keyboard-response', + stimulus: 'foo' + } + ], + on_timeline_start: on_start_fn, + repetitions: 2 + } + + jsPsych.init({timeline: [mini_timeline]}); + + expect(on_start_fn).toHaveBeenCalled(); + utils.pressKey(32); + utils.pressKey(32); + expect(on_start_fn.mock.calls.length).toBe(2); + + }) +}) \ No newline at end of file diff --git a/tests/jsPsych/timelines.test.js b/tests/jsPsych/timelines.test.js index 36082186..71d0c0fd 100644 --- a/tests/jsPsych/timelines.test.js +++ b/tests/jsPsych/timelines.test.js @@ -166,6 +166,36 @@ describe('loop function', function(){ utils.pressKey('a'); }); + test('only runs once when timeline variables are used', function(){ + var count = 0; + + var trial = { + timeline: [{ + type: 'html-keyboard-response', + stimulus: 'foo' + }], + timeline_variables:[{a:1},{a:2}], + loop_function: function(){ + count++; + return false + } + } + + jsPsych.init({ + timeline: [trial] + }); + + // first trial + utils.pressKey(32); + + expect(count).toBe(0); + + // second trial + utils.pressKey(32); + + expect(count).toBe(1); + }) + }); describe('conditional function', function(){ @@ -269,6 +299,73 @@ describe('conditional function', function(){ expect(conditional_count).toBe(2); }); + test('executes only once even when repetitions is > 1', function(){ + var conditional_count = 0; + + var trial = { + timeline: [{ + type: 'html-keyboard-response', + stimulus: 'foo' + }], + repetitions: 2, + conditional_function: function(){ + conditional_count++; + return true; + } + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(conditional_count).toBe(1); + + // first trial + utils.pressKey(32); + + expect(conditional_count).toBe(1); + + // second trial + utils.pressKey(32); + + expect(conditional_count).toBe(1); + }) + + test('executes only once when timeline variables are used', function(){ + var conditional_count = 0; + + var trial = { + timeline: [{ + type: 'html-keyboard-response', + stimulus: 'foo' + }], + timeline_variables: [ + {a:1}, + {a:2} + ], + conditional_function: function(){ + conditional_count++; + return true; + } + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(conditional_count).toBe(1); + + // first trial + utils.pressKey(32); + + expect(conditional_count).toBe(1); + + // second trial + utils.pressKey(32); + + expect(conditional_count).toBe(1); + }) + test('timeline variables from nested timelines are available', function(){ var trial = { type: 'html-keyboard-response',