Merge pull request #1494 from jspsych/feature-timeline-events

merge timeline events into all timeline vars branch
This commit is contained in:
Josh de Leeuw 2021-02-09 14:26:36 -05:00 committed by GitHub
commit 0272250bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 323 additions and 28 deletions

View File

@ -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 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 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,11 +574,23 @@ 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...
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.
else if (typeof timeline_parameters.loop_function !== 'undefined') {
if (typeof timeline_parameters.loop_function !== 'undefined') {
jsPsych.internal.call_immediate = true;
if (timeline_parameters.loop_function(this.generatedData())) {
this.reset();
@ -583,12 +603,12 @@ window.jsPsych = (function() {
}
}
// no more loops on this timeline, we're done!
else {
progress.done = true;
return true;
}
// no more loops on this timeline, we're done!
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

View File

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

View File

@ -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',