diff --git a/README.md b/README.md index 80af9e7c..35bdc8fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![logo](jspsych-logo-readme.jpg) -jsPsych is a JavaScript library for creating and running behavioral experiments in a web browser. jsPsych simplifies the process of coding browser-based experiments by providing a set of flexible plugins that define different kinds of tasks a subject could complete during an experiment. By assembling different plugins together and customizing the parameters of each, it is possible to create many different types of experiments. +jsPsych is a JavaScript library for creating behavioral experiments that run in a web browser. jsPsych creates a framework for defining experiments and provides a set of flexible plugins that create different kinds of tasks a subject could complete during an experiment. By assembling different plugins together and customizing the parameters of each, it is possible to create many different types of experiments. Documentation ------------- @@ -27,10 +27,10 @@ de Leeuw, J.R. (2015). jsPsych: A JavaScript library for creating behavioral exp Response times -------------- -Wondering if jsPsych can be used for research that depends on accurate response time measurement? In general, the answer is yes. Response time measurements in jsPsych (and JavaScript in general) are comparable to those taken in standard lab software like Psychophysics Toolbox and E-Prime. Response times measured in JavaScript tend to be a little bit longer (10-40ms), but have similar variance. See the following references for extensive work on this topic. +Wondering if jsPsych can be used for research that depends on accurate response time measurement? For most purposes, the answer is yes. Response time measurements in jsPsych (and JavaScript in general) are comparable to those taken in standard lab software like Psychophysics Toolbox and E-Prime. Response times measured in JavaScript tend to be a little bit longer (10-40ms), but have similar variance. See the following references for extensive work on this topic. * [de Leeuw, J. R., & Motz, B. A. (2016). Psychophysics in a Web browser? Comparing response times collected with JavaScript and Psychophysics Toolbox in a visual search task. *Behavior Research Methods*, *48*(1), 1-12.](http://link.springer.com/article/10.3758%2Fs13428-015-0567-2) -* [Hilbig, B. E. (in press). Reaction time effects in lab- versus web-based research: Experimental evidence. *Behavior Research Methods*.](http://dx.doi.org/10.3758/s13428-015-0678-9) +* [Hilbig, B. E. (2016). Reaction time effects in lab- versus web-based research: Experimental evidence. *Behavior Research Methods*, *48*(4), 1718-1724.](http://dx.doi.org/10.3758/s13428-015-0678-9) * [Pinet, S., Zielinski, C., Mathôt, S. et al. (in press). Measuring sequences of keystrokes with jsPsych: Reliability of response times and interkeystroke intervals. *Behavior Research Methods*.](http://link.springer.com/article/10.3758/s13428-016-0776-3) * [Reimers, S., & Stewart, N. (2015). Presentation and response time accuracy in Adobe Flash and HTML5/JavaScript Web experiments. *Behavior Research Methods*, *47*(2), 309-327.](http://link.springer.com/article/10.3758%2Fs13428-014-0471-1) diff --git a/contributors.md b/contributors.md index 1828c803..c57235c6 100644 --- a/contributors.md +++ b/contributors.md @@ -1,11 +1,16 @@ -The following people have contributed to the development of jsPsych by writing code, documentation, and/or suggesting major improvements (in alphabetical order): +The following people have contributed to the development of jsPsych by writing code, documentation, and/or suggesting improvements (in alphabetical order): +* Xiaolu Bai - https://github.com/lbai001 * Jason Carpenter -* Josh de Leeuw - https://github.com/jodeleeuw +* Steve Chao - https://github.com/stchao * Jana Klaus - https://github.com/janakl4us * Jonas Lambers * Shane Martin - https://github.com/shamrt * Adrian Oesch - https://github.com/adrianoesch * Junyan Qi - https://github.com/GavinQ1 +* Dan Rivas - https://github.com/rivasd * Marian Sauter - https://github.com/mariansauter * Tim Vergenz - https://github.com/vergenzt +* Matteo Visconti di Oleggio Castello - https://github.com/mvdoc +* Wolfgang Walther - https://github.com/wolfgangwalther * Erik Weitnauer - https://github.com/eweitnauer +* Rob Wilkinson - https://github.com/RobAWilkinson diff --git a/css/jspsych.css b/css/jspsych.css index 5d533a5a..6cd03b62 100644 --- a/css/jspsych.css +++ b/css/jspsych.css @@ -62,7 +62,7 @@ input[type="text"] { .jspsych-btn { display: inline-block; padding: 6px 12px; - margin: 0px 8px; + margin: 0px; font-size: 14px; font-weight: 400; font-family: 'Open Sans', 'Arial', sans-serif; @@ -98,7 +98,6 @@ input[type="text"] { #jspsych-progressbar-outer { background-color: #dedede; border-radius: 5px; - padding: 1px; width: 800px; margin: auto; } diff --git a/docs/markdown_docs/core_library/jspsych-core.md b/docs/markdown_docs/core_library/jspsych-core.md index 482e8818..3569db98 100644 --- a/docs/markdown_docs/core_library/jspsych-core.md +++ b/docs/markdown_docs/core_library/jspsych-core.md @@ -306,7 +306,6 @@ show_progress_bar | boolean | If true, then [a progress bar](../features/progres preload_audio | array | An array of audio files to preload before starting the experiment. preload_images | array | An array of image files to preload before starting the experiment. max_load_time | numeric | The maximum number of milliseconds to wait for content to preload. If the wait time is exceeded an error message is displayed and the experiment stops. The default value is 60 seconds. -fullscreen | boolean | If true, the experiment will run in fullscreen mode. See the [feature page](../features/fullscreen.md) for more details. default_iti | numeric | The default inter-trial interval in ms. The default value if none is specified is 0ms. Possible values for the exclusions parameter above. diff --git a/docs/markdown_docs/features/data.md b/docs/markdown_docs/features/data.md index 532c7e76..b6da7430 100644 --- a/docs/markdown_docs/features/data.md +++ b/docs/markdown_docs/features/data.md @@ -15,8 +15,8 @@ In most cases, data collection will be automatic and hidden. Plugins save data o Often it is useful to add a piece of data to *all* of the trials in the experiment. For example, appending the subject ID to each trial. This can be done with the `jsPsych.data.addProperties()` function. Here is an example: ```javascript -// generate a random subject ID -var subject_id = Math.floor(Math.random()*100000); +// generate a random subject ID with 15 characters +var subject_id = jsPsych.randomization.randomID(15); // pick a random condition for the subject at the start of the experiment var condition_assignment = jsPsych.randomization.sample(['conditionA', 'conditionB', 'conditionC'],1)[0]; @@ -143,28 +143,121 @@ The `file_put_contents($filename, $data)` method requires permission to write ne To use the PHP script, the JavaScript that runs jsPsych needs to send the `filename` and `filedata` information. This is done through an [AJAX](http://www.w3schools.com/xml/ajax_intro.asp) call. ```javascript -function saveData(filename, filedata){ - $.ajax({ - type:'post', - cache: false, - url: 'save_data.php', // this is the path to the above PHP script - data: {filename: filename, filedata: filedata} - }); +function saveData(name, data){ + var xhr = new XMLHttpRequest(); + xhr.open('POST', 'write_data.php'); // 'write_data.php' is the path to the php file described above. + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.send(JSON.stringify({filename: filename, filedata: filedata})); } // call the saveData function after the experiment is over jsPsych.init({ - // code to define the experiment structure would go here... - - on_finish: function(data){ saveData("filename.csv", jsPsych.data.getDataAsCSV()) } + on_finish: function(){ saveData("experiment_data.csv", jsPsych.data.get().csv()); } }); ``` -To use this in an actual experiment, it would be important to tie the filename to some unique identifier like a subject number. +To use this in an actual experiment, it would be important to tie the filename to some unique identifier like a subject number. Otherwise the file may be overwritten by collecting new data. ## Storing data permanently in a MySQL database The ideal solution for storing data generated by jsPsych is to write it to a database. -There are dozens of database options. MySQL is one of the most popular [relational databases](http://en.wikipedia.org/wiki/Relational_database), is free to use, and relatively easy [to install](https://www.google.com/search?q=how+to+install+mysql). This page will assume that you have a MySQL database installed on your server that is hosting the jsPsych experiment, and that your server is able to execute PHP code. If you are trying to run on a local machine, you'll need to install a local server environment like [XAMPP](https://www.apachefriends.org/index.html). +There are dozens of database options. MySQL is one of the most popular [relational databases](http://en.wikipedia.org/wiki/Relational_database), is free to use, and relatively easy [to install](https://www.google.com/search?q=how+to+install+mysql). This code will assume that you have a MySQL database installed on your server that is hosting the jsPsych experiment, and that your server is able to execute PHP code. If you are trying to run on a local machine, you'll need to install a local server environment like [XAMPP](https://www.apachefriends.org/index.html). + +You'll need two PHP scripts. The first is a configuration file for your database. Save it as `database_config.php` on your server. Within this file are configuration options for the database. You'll need to change these according to how you have configured your MySQL installation. + +```php + +``` + +The second PHP file will write data to the database. This script reads the database to discover what columns are in the table, and then only allows data to be entered in that matches those columns. This is a security feature. Save this file as `write_data.php` on your server. + +```php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + // First stage is to get all column names from the table and store + // them in $col_names array. + $stmt = $conn->prepare("SHOW COLUMNS FROM `$table`"); + $stmt->execute(); + $col_names = array(); + while($row = $stmt->fetchColumn()) { + $col_names[] = $row; + } + // Second stage is to create prepared SQL statement using the column + // names as a guide to what values might be in the JSON. + // If a value is missing from a particular trial, then NULL is inserted + $sql = "INSERT INTO $table VALUES("; + for($i = 0; $i < count($col_names); $i++){ + $name = $col_names[$i]; + $sql .= ":$name"; + if($i != count($col_names)-1){ + $sql .= ", "; + } + } + $sql .= ");"; + $insertstmt = $conn->prepare($sql); + for($i=0; $i < count($data_array); $i++){ + for($j = 0; $j < count($col_names); $j++){ + $colname = $col_names[$j]; + if(!isset($data_array[$i][$colname])){ + $insertstmt->bindValue(":$colname", null, PDO::PARAM_NULL); + } else { + $insertstmt->bindValue(":$colname", $data_array[$i][$colname]); + } + } + $insertstmt->execute(); + } + echo '{"success": true}'; +} catch(PDOException $e) { + echo '{"success": false, "message": ' . $e->getMessage(); +} +$conn = null; +?> +``` + +To send the data, we use an AJAX request in JavaScript. +```JavaScript +function saveData() { + var xhr = new XMLHttpRequest(); + xhr.open('POST', 'write_data.php'); // change 'write_data.php' to point to php script. + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.onload = function() { + if(xhr.status == 200){ + var response = JSON.parse(xhr.responseText); + console.log(response.success); + } + }; + xhr.send(jsPsych.data.getData().json()); +} +``` + +You can call the `saveData()` function using the `on_finish` handler for the experiment, or by using the `call-function` plugin. + +```javascript +// with on_finish handler +jsPsych.init({ + on_finish: saveData +}); + +// with call-function plugin +timeline.push({ + type: 'call-function', + func: saveData +}); +``` diff --git a/docs/markdown_docs/features/fullscreen.md b/docs/markdown_docs/features/fullscreen.md index 5e308d01..9f4a944f 100644 --- a/docs/markdown_docs/features/fullscreen.md +++ b/docs/markdown_docs/features/fullscreen.md @@ -1,14 +1,36 @@ # Fullscreen Experiments -You can run your experiment in fullscreen mode by setting the `fullscreen` parameter in the `jsPsych.init` call that launches the experiment. +You can run your experiment in fullscreen mode by using the jspsych-fullscreen plugin. ```javascript +var timeline = []; + +timeline.push({ + type: 'fullscreen', + fullscreen_mode: true +}); + +timeline.push({ + type: 'text', + text: 'This trial will be in fullscreen mode.' +}); + +// exit fullscreen mode +timeline.push({ + type: 'fullscreen', + fullscreen_mode: false +}); + +timeline.push({ + type: 'text', + text: 'This trial will NOT be in fullscreen mode.' +}); + jsPsych.init({ - timeline: timeline, - fullscreen: true + timeline: timeline }); ``` -For security reasons, launching the browser into fullscreen mode requires that the user take an action. Therefore, if fullscreen mode is requested, a button will be displayed on the page to launch the experiment into fullscreen mode. The experiment will not begin until this button is pressed. +For security reasons, web browsers require that users initiate an action to launch fullscreen mode. The fullscreen plugin displays a button that the user must click to change the display to fullscreen. -Safari does not support keyboard input when the browser is in fullscreen mode. Therefore, the function will not launch fullscreen mode on Safari. +Safari does not support keyboard input when the browser is in fullscreen mode. Therefore, the function will not launch fullscreen mode on Safari. The experiment will ignore any trials using the fullscreen plugin in Safari. diff --git a/docs/markdown_docs/features/media-preloading.md b/docs/markdown_docs/features/media-preloading.md index 836936ea..b14e3399 100644 --- a/docs/markdown_docs/features/media-preloading.md +++ b/docs/markdown_docs/features/media-preloading.md @@ -47,8 +47,8 @@ var audio = ['audio/foo.mp3']; jsPsych.init({ timeline: [trial], - audio_preload: audio, - image_preload: images + preload_audio: audio, + preload_images: images }); ``` diff --git a/docs/markdown_docs/plugins/jspsych-resize.md b/docs/markdown_docs/plugins/jspsych-resize.md new file mode 100644 index 00000000..d8b05d7c --- /dev/null +++ b/docs/markdown_docs/plugins/jspsych-resize.md @@ -0,0 +1,30 @@ +# jspsych-resize + +This plugin displays a resizable div container that allows the user to drag until the container is the same size as the item being measured. Once the user measures the item as close as possible, clicking the button sets a scaling factor for the div containing jsPsych content. This causes the stimuli that follow to have a known size, independent of monitor resolution. + +## Parameters + +This table lists the parameters associated with this plugin. Parameters with a default value of *undefined* must be specified. Other parameters can be left unspecified if the default value is acceptable. + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ +item_height | numeric | 1 | The height of the item to be measured. Any units can be used as long as you are consistent with using the same units for all parameters. +item_width | numeric | 1 | The width of the item to be measured. +pixels_per_unit | numeric | 100 | After the scaling factor is applied, this many pixels will equal one unit of measurement. +prompt | string | `''` | HTML content to display below the resizable box, and above the button. +button_text | string | `'Done'` | Label to display on the button to complete calibration. +starting_size | numeric | 100 | The initial size of the box, in pixels, along the largest dimension. The aspect ratio will be set automatically to match the item width and height. + +## Examples + +#### Measuring a credit card and resizing the display to have 150 pixels equal an inch. + +```javascript +var inputs = { + type: 'resize', + item_width: 3 + 3/8, + item_height: 2 + 1/8, + prompt: "

Click and drag the lower right corner of the box until the box is the same size as a credit card held up to the screen.

", + pixels_per_unit: 150 +}; +``` diff --git a/docs/markdown_docs/plugins/jspsych-survey-multi-picture.md b/docs/markdown_docs/plugins/jspsych-survey-multi-picture.md new file mode 100644 index 00000000..a499c6b1 --- /dev/null +++ b/docs/markdown_docs/plugins/jspsych-survey-multi-picture.md @@ -0,0 +1,56 @@ +# jspsych-survey-multi-picture plugin + +The survey-multi-picture plugin displays a set of questions with multiple picture response fields. The subject selects a single answer. + +## Parameters + +This table lists the parameters associated with this plugin. Parameters with a default value of *undefined* must be specified. Other parameters can be left unspecified if the default value is acceptable. + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ +questions | array | *undefined* | An array of strings. The strings are the prompts/questions that will be associated with a group of options (images). All questions will get presented on the same page (trial). +options | array | *undefined* | An array of arrays of objects containing url key and label key(label is optional). The innermost arrays contain a set of options to display for an individual question. The length of the outer array should be the same as the number of questions. +horizontal | boolean | false | If true, then questions are centered and options are displayed horizontally. +preamble | string | empty string | HTML formatted string to display at the top of the page above all the questions. + +## Data Generated + +In addition to the [default data collected by all plugins](overview#datacollectedbyplugins), this plugin collects the following data for each trial. + +Name | Type | Value +-----|------|------ +responses | JSON string | A string in JSON format containing the response for each question. The encoded object will have a separate variable for the response to each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as the image url for the picture clicked on. +rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the questions first appear on the screen until the subject's response. + +## Examples + +#### Basic example with multiple questions on a page. + +```javascript + // defining groups of questions that will go together. + var page_1_questions = ["I like vegetables.", "I like fruit."]; + + // definiting two different response scales that can be used. + var page_1_options = [{url: "http://www.thetechconnectioninc.com/assets/img/Twitter.png", label: "twitter"}, {url: "http://www.freeiconspng.com/uploads/facebook-logo-png-20.png", label: "facebook"}]; + var page_2_options = [{url: "http://www.gameswithwords.org/WhichEnglish/images/1_1.jpg", label: "dog chase cat"}, {url: "http://www.gameswithwords.org/WhichEnglish/images/1_2.jpg", label: "cat chase dog"}]; + + var multi_picture_block = { + type: 'survey-multi-picture', + questions: page_1_questions, + options: [page_1_options, page_2_options], // need one scale for every question on a page + }; + + var multi_picture_block_horizontal = { + type: 'survey-multi-picture', + questions: page_1_questions, + options: [page_1_options, page_2_options], // need one scale for every question on a page + horizontal: true // centers questions and makes options display horizontally + }; + + jsPsych.init({ + timeline: [multi_picture_block, multi_picture_block_horizontal], + on_finish: function() { + jsPsych.data.displayData(); + } + }); +``` diff --git a/docs/markdown_docs/plugins/jspsych-survey-multi-select.md b/docs/markdown_docs/plugins/jspsych-survey-multi-select.md new file mode 100644 index 00000000..82d7899a --- /dev/null +++ b/docs/markdown_docs/plugins/jspsych-survey-multi-select.md @@ -0,0 +1,58 @@ +# jspsych-survey-multi-select plugin + +The survey-multi-select plugin displays a set of questions with multiple select response fields. The subject could select multiple answers. + +## Parameters + +This table lists the parameters associated with this plugin. Parameters with a default value of *undefined* must be specified. Other parameters can be left unspecified if the default value is acceptable. + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ +questions | array | *undefined* | An array of strings. The strings are the prompts/questions that will be associated with a group of options (check boxes). All questions will get presented on the same page (trial). +options | array | *undefined* | An array of arrays. The innermost arrays contain a set of options to display for an individual question. The length of the outer array should be the same as the number of questions. +required | boolean | true | If true, then at least one option must be selected. +required_msg | string | `*please select at least one option!` | Message to display if required check is not met. +horizontal | boolean | false | If true, then questions are centered and options are displayed horizontally. +preamble | string | empty string | HTML formatted string to display at the top of the page above all the questions. + +## Data Generated + +In addition to the [default data collected by all plugins](overview#datacollectedbyplugins), this plugin collects the following data for each trial. + +Name | Type | Value +-----|------|------ +responses | JSON string | An array containing all selected choices in JSON format for each question. The encoded object will have a separate variable for the response to each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as the name of the option label. +rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the questions first appear on the screen until the subject's response. + +## Examples + +#### Basic example with multiple questions on a page. + +```javascript + // defining groups of questions that will go together. + var page_1_questions = ["I like vegetables.", "I like fruit."]; + + // definiting two different response scales that can be used. + var page_1_options = ["Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree"]; + var page_2_options = ["Strongly Disagree", "Disagree", "Somewhat Disagree", "Neural", "Somewhat Agree", "Agree", "Strongly Agree"]; + + var multi_select_block = { + type: 'survey-multi-select', + questions: page_1_questions, + options: [page_1_options, page_2_options], // need one scale for every question on a page + }; + + var multi_select_block_horizontal = { + type: 'survey-multi-select', + questions: page_1_questions, + options: [page_1_options, page_2_options], // need one scale for every question on a page + horizontal: true // centers questions and makes options display horizontally + }; + + jsPsych.init({ + timeline: [multi_select_block, multi_select_block_horizontal], + on_finish: function() { + jsPsych.data.displayData(); + } + }); +``` diff --git a/docs/markdown_docs/plugins/jspsych-video.md b/docs/markdown_docs/plugins/jspsych-video.md index fb29f64d..b4291dec 100644 --- a/docs/markdown_docs/plugins/jspsych-video.md +++ b/docs/markdown_docs/plugins/jspsych-video.md @@ -14,6 +14,8 @@ height | numeric | heigh of the video file | The height of the video display in prompt | string | empty string | A message (any valid HTML) to display beneath the video element. autoplay | boolean | true | If true, the video will begin playing as soon as it has loaded. controls | boolean | false | If true, controls for the video player will be available to the subject. They will be able to pause the video or move the playback to any point in the video. +start | numeric | 0 | If given a value, the video will start at this time point in seconds. +stop| numeric | *undefined* | If given a value, the video will stop at this time point in seconds. ## Data Generated diff --git a/docs/markdown_docs/plugins/overview.md b/docs/markdown_docs/plugins/overview.md index 402bbd91..7f837a1e 100644 --- a/docs/markdown_docs/plugins/overview.md +++ b/docs/markdown_docs/plugins/overview.md @@ -75,7 +75,8 @@ This table is a description of all plugins that are currently bundled with jsPsy [jspsych‑instructions](jspsych-instructions) | For displaying instructions to the subject. [jspsych‑multi‑stim‑multi‑response](jspsych-multi-stim-multi-response) | A more generalized version of the single-stim plugin. Can display multiple stimuli in a single trial, and collect multiple responses in a single trial. [jspsych‑palmer](jspsych-palmer) | Shows grid-like stimuli inspired by Stephen Palmer's work. The stimuli are editable: subjects can add and subtract parts interactively. Also contains a method for generating the HTML code to render the stimuli, allowing them to be used in other plugins. - [jspsych‑reconstruction](jspsych-reconstruction) | The subject interacts with a stimulus by modifying a parameter of the stimulus and observing the change in the stimulus in real-time. + [jspsych‑reconstruction](jspsych-reconstruction) | The subject interacts with a stimulus by modifying a parameter of the stimulus and observing the change in the stimulus in real-time. + [jspsych‑resize](jspsych-resize) | Calibrate the display so that materials display with a known physical size. [jspsych‑same‑different](jspsych-same-different) | A same-different judgment task. A stimulus is shown, followed by a brief gap, and then another stimulus is shown. The subject indicates whether the stimuli are the same or different. [jspsych‑similarity](jspsych-similarity) | Two stimuli are shown sequentially, and the subject indicates how similar they are by dragging a slider object. [jspsych‑single‑audio](jspsych-single-audio) | A basic plugin for playing an audio stimulus and getting a keyboard response. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6a223683..89cdcf2e 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -46,6 +46,7 @@ pages: - 'jspsych-multi-stim-multi-response': 'plugins/jspsych-multi-stim-multi-response.md' - 'jspsych-palmer': 'plugins/jspsych-palmer.md' - 'jspsych-reconstruction': 'plugins/jspsych-reconstruction.md' + - 'jspsych-resize': 'plugins/jspsych-resize.md' - 'jspsych-same-different': 'plugins/jspsych-same-different.md' - 'jspsych-similarity': 'plugins/jspsych-similarity.md' - 'jspsych-single-audio': 'plugins/jspsych-single-audio.md' diff --git a/examples/demo-simple-rt-task.html b/examples/demo-simple-rt-task.html index f9bcccf4..babd629e 100644 --- a/examples/demo-simple-rt-task.html +++ b/examples/demo-simple-rt-task.html @@ -14,7 +14,7 @@ var timeline = []; /* define welcome message trial */ - var welcome = { + var welcome_block = { type: "text", text: "Welcome to the experiment. Press any key to begin." }; diff --git a/examples/flex-layout-testing.html b/examples/flex-layout-testing.html new file mode 100644 index 00000000..0b088826 --- /dev/null +++ b/examples/flex-layout-testing.html @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/examples/jspsych-fullscreen.html b/examples/jspsych-fullscreen.html new file mode 100644 index 00000000..171fdc2e --- /dev/null +++ b/examples/jspsych-fullscreen.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/examples/jspsych-resize.html b/examples/jspsych-resize.html new file mode 100644 index 00000000..2b8bf251 --- /dev/null +++ b/examples/jspsych-resize.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/examples/jspsych-survey-likert.html b/examples/jspsych-survey-likert.html index 7c43acee..8441c0cc 100644 --- a/examples/jspsych-survey-likert.html +++ b/examples/jspsych-survey-likert.html @@ -1,7 +1,7 @@ - + @@ -18,6 +18,7 @@ var likert_block = { type: 'survey-likert', questions: ["I like vegetables.", "I like fruit."], + required: true, labels: [scale_1, scale_1], // need one scale for every question on a page }; diff --git a/examples/jspsych-survey-multi-picture.html b/examples/jspsych-survey-multi-picture.html new file mode 100644 index 00000000..c4fff1e7 --- /dev/null +++ b/examples/jspsych-survey-multi-picture.html @@ -0,0 +1,31 @@ + + + + + Document + + + + + + + + + diff --git a/examples/jspsych-survey-multi-select.html b/examples/jspsych-survey-multi-select.html new file mode 100644 index 00000000..e2c80cee --- /dev/null +++ b/examples/jspsych-survey-multi-select.html @@ -0,0 +1,35 @@ + + + + + Document + + + + + + + + + diff --git a/examples/jspsych-survey-text.html b/examples/jspsych-survey-text.html index 9db3b6b1..87d093c6 100644 --- a/examples/jspsych-survey-text.html +++ b/examples/jspsych-survey-text.html @@ -1,7 +1,7 @@ - + @@ -14,7 +14,9 @@ var survey_block = { type: 'survey-text', - questions: page_1_questions + questions: page_1_questions, + rows: [1, 8], + columns: [50, 20] }; jsPsych.init({ diff --git a/jspsych.js b/jspsych.js index d0ba50dd..4b51bc44 100755 --- a/jspsych.js +++ b/jspsych.js @@ -56,7 +56,7 @@ window.jsPsych = (function() { 'on_finish': function(data) { return undefined; }, - 'on_trial_start': function() { + 'on_trial_start': function(trial) { return undefined; }, 'on_trial_finish': function() { @@ -74,7 +74,6 @@ window.jsPsych = (function() { 'show_progress_bar': false, 'auto_preload': true, 'max_load_time': 60000, - 'fullscreen': false, 'default_iti': 0 }; @@ -360,6 +359,12 @@ window.jsPsych = (function() { // set the order for going through the timeline variables array // TODO: this is where all the sampling options can be implemented this.setTimelineVariablesOrder = function() { + + // check to make sure this node has variables + if(typeof timeline_parameters === 'undefined' || typeof timeline_parameters.timeline_variables === 'undefined'){ + return; + } + var order = []; for(var i=0; i'; + display_element.innerHTML = ''; } else { - display_element.innerHTML = '
'+trial.stimulus+'
'; + display_element.innerHTML = '
'+trial.stimulus+'
'; } //display buttons @@ -106,11 +108,11 @@ jsPsych.plugins["button-response"] = (function() { buttons.push(trial.button_html); } } - display_element.innerHTML += '
'; + display_element.innerHTML += '
'; for (var i = 0; i < trial.choices.length; i++) { var str = buttons[i].replace(/%choice%/g, trial.choices[i]); display_element.querySelector('#jspsych-button-response-btngroup').insertAdjacentHTML('beforeend', - '
'+str+'
'); + '
'+str+'
'); display_element.querySelector('#jspsych-button-response-button-' + i).addEventListener('click', function(e){ var choice = e.currentTarget.dataset.choice; after_response(choice); diff --git a/plugins/jspsych-free-sort.js b/plugins/jspsych-free-sort.js index 97cb3fb2..183b94ae 100644 --- a/plugins/jspsych-free-sort.js +++ b/plugins/jspsych-free-sort.js @@ -60,6 +60,12 @@ jsPsych.plugins['free-sort'] = (function() { default: 'above', no_function: false, description: '' + }, + button_label: { + type: [jsPsych.plugins.parameterType.STRING], + default: 'Done', + no_function: false, + description: '' } } } @@ -73,6 +79,7 @@ jsPsych.plugins['free-sort'] = (function() { trial.prompt_location = trial.prompt_location || "above"; trial.sort_area_width = trial.sort_area_width || 800; trial.sort_area_height = trial.sort_area_height || 800; + trial.button_label = typeof trial.button_label === 'undefined' ? 'Done' : trial.button_label; // if any trial variables are functions // this evaluates the function and replaces @@ -118,7 +125,7 @@ jsPsych.plugins['free-sort'] = (function() { }); } - display_element.innerHTML += ''; + display_element.innerHTML += ''; var maxz = 1; diff --git a/plugins/jspsych-fullscreen.js b/plugins/jspsych-fullscreen.js new file mode 100644 index 00000000..940a938a --- /dev/null +++ b/plugins/jspsych-fullscreen.js @@ -0,0 +1,114 @@ +/* jspsych-fullscreen.js + * Josh de Leeuw + * + * toggle fullscreen mode in the browser + * + */ + +jsPsych.plugins.fullscreen = (function() { + + var plugin = {}; + + plugin.info = { + name: 'fullscreen', + description: '', + parameters: { + fullscreen_mode: { + type: [jsPsych.plugins.parameterType.BOOL], + default: true, + array: false, + no_function: false, + description: '' + }, + message: { + type: [jsPsych.plugins.parameterType.STRING], + default: '

The experiment will switch to full screen mode when you press the button below

', + array: false, + no_function: false, + description: '' + }, + button_label: { + type: [jsPsych.plugins.parameterType.STRING], + default: "Go", + array: false, + no_function: false, + description: '' + }, + delay_after: { + type: [jsPsych.plugins.parameterType.INT], + default: 1000, + array: false, + no_function: false, + description: '' + }, + } + } + + plugin.trial = function(display_element, trial) { + + trial.fullscreen_mode = typeof trial.fullscreen_mode === 'undefined' ? true : trial.fullscreen_mode; + trial.message = trial.message || '

The experiment will switch to full screen mode when you press the button below

'; + trial.button_label = trial.button_label || 'Go'; + trial.delay_after = trial.delay_after || 1000; + + // if any trial variables are functions + // this evaluates the function and replaces + // it with the output of the function + trial = jsPsych.pluginAPI.evaluateFunctionParameters(trial); + + // check if keys are allowed in fullscreen mode + var keyboardNotAllowed = typeof Element !== 'undefined' && 'ALLOW_KEYBOARD_INPUT' in Element; + if (keyboardNotAllowed) { + // This is Safari, and keyboard events will be disabled. Don't allow fullscreen here. + // do something else? + endTrial(); + } else { + if(trial.fullscreen_mode){ + display_element.innerHTML = trial.message + ''; + var listener = display_element.querySelector('#jspsych-fullscreen-btn').addEventListener('click', function() { + var element = document.documentElement; + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen(); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); + } + endTrial(); + }); + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + endTrial(); + } + } + + function endTrial() { + + display_element.innerHTML = ''; + + jsPsych.pluginAPI.setTimeout(function(){ + + var trial_data = { + success: !keyboardNotAllowed + }; + + jsPsych.finishTrial(trial_data); + + }, trial.delay_after); + + } + + }; + + return plugin; +})(); diff --git a/plugins/jspsych-instructions.js b/plugins/jspsych-instructions.js index c889fcfe..80d39e5c 100644 --- a/plugins/jspsych-instructions.js +++ b/plugins/jspsych-instructions.js @@ -53,6 +53,18 @@ jsPsych.plugins.instructions = (function() { default: false, no_function: false, description: '' + }, + button_label_previous: { + type: [jsPsych.plugins.parameterType.STRING], + default: 'Previous', + no_function: false, + description: '' + }, + button_label_next: { + type: [jsPsych.plugins.parameterType.STRING], + default: 'Next', + no_function: false, + description: '' } } } @@ -64,6 +76,8 @@ jsPsych.plugins.instructions = (function() { trial.allow_backward = (typeof trial.allow_backward === 'undefined') ? true : trial.allow_backward; trial.allow_keys = (typeof trial.allow_keys === 'undefined') ? true : trial.allow_keys; trial.show_clickable_nav = (typeof trial.show_clickable_nav === 'undefined') ? false : trial.show_clickable_nav; + trial.button_label_previous = (typeof trial.button_label_previous === 'undefined') ? 'Previous' : trial.button_label_previous; + trial.button_label_next = (typeof trial.button_label_next === 'undefined') ? 'Next' : trial.button_label_next; // if any trial variables are functions // this evaluates the function and replaces @@ -78,6 +92,16 @@ jsPsych.plugins.instructions = (function() { var last_page_update_time = start_time; + function btnListener(evt){ + evt.target.removeEventListener('click', btnListener); + if(this.id === "jspsych-instructions-back"){ + back(); + } + else if(this.id === 'jspsych-instructions-next'){ + next(); + } + } + function show_current_page() { display_element.innerHTML = trial.pages[current_page]; @@ -85,50 +109,41 @@ jsPsych.plugins.instructions = (function() { var nav_html = "
"; if (current_page != 0 && trial.allow_backward) { - nav_html += ""; + nav_html += ""; } - nav_html += "
" + nav_html += "" display_element.innerHTML += nav_html; if (current_page != 0 && trial.allow_backward) { - display_element.querySelector('#jspsych-instructions-back').addEventListener('click', function() { - clear_button_handlers(); - }); + display_element.querySelector('#jspsych-instructions-back').addEventListener('click', btnListener); } - - display_element.querySelector('#jspsych-instructions-next').addEventListener('click', function() { - clear_button_handlers(); - }); + + display_element.querySelector('#jspsych-instructions-next').addEventListener('click', btnListener); } } - function clear_button_handlers() { - display_element.querySelector('#jspsych-instructions-next').removeEventListener('click'); - display_element.querySelector('#jspsych-instructions-back').removeEventListener('click'); - } - function next() { - + add_current_page_to_view_history() - + current_page++; - + // if done, finish up... if (current_page >= trial.pages.length) { endTrial(); } else { show_current_page(); } - + } - + function back() { - + add_current_page_to_view_history() - + current_page--; - + show_current_page(); } diff --git a/plugins/jspsych-palmer.js b/plugins/jspsych-palmer.js index 30dbd024..a6c15951 100644 --- a/plugins/jspsych-palmer.js +++ b/plugins/jspsych-palmer.js @@ -66,6 +66,12 @@ jsPsych.plugins.palmer = (function() { default: '', no_function: false, description: '' + }, + button_label: { + type: [jsPsych.plugins.parameterType.STRING], + default: 'Done', + no_function: false, + description: '' } } } @@ -80,6 +86,7 @@ jsPsych.plugins.palmer = (function() { trial.timing_item = trial.timing_item || 1000; trial.timing_feedback = trial.timing_feedback || 1000; trial.prompt = (typeof trial.prompt === 'undefined') ? "" : trial.prompt; + trial.button_label = typeof trial.button_label === 'undefined' ? 'Submit Answers' : trial.button_label; // if any trial variables are functions // this evaluates the function and replaces @@ -246,7 +253,7 @@ jsPsych.plugins.palmer = (function() { // start recording the time var startTime = (new Date()).getTime(); - display_element.innerHTML += '

'; + display_element.innerHTML += '

'; display_element.querySelector('#jspsych-palmer-submitButton').addEventListener('click', function() { save_data(); }); diff --git a/plugins/jspsych-reconstruction.js b/plugins/jspsych-reconstruction.js index bb8bde3d..6cc35a33 100644 --- a/plugins/jspsych-reconstruction.js +++ b/plugins/jspsych-reconstruction.js @@ -47,6 +47,12 @@ jsPsych.plugins['reconstruction'] = (function() { default: 'g', no_function: false, description: '' + }, + button_label: { + type: [jsPsych.plugins.parameterType.STRING], + default: '', + no_function: false, + description: 'Submit Answers' } } } @@ -58,6 +64,7 @@ jsPsych.plugins['reconstruction'] = (function() { trial.step_size = trial.step_size || 0.05; trial.key_increase = trial.key_increase || 'h'; trial.key_decrease = trial.key_decrease || 'g'; + trial.button_label = typeof trial.button_label === 'undefined' ? 'Submit Answers' : trial.button_label; // if any trial variables are functions // this evaluates the function and replaces @@ -102,10 +109,10 @@ jsPsych.plugins['reconstruction'] = (function() { //console.log(param); - display_element.innerHTML = '
'+trial.stim_function(param)+'
'); + display_element.innerHTML = '
'+trial.stim_function(param)+'
'; // add submit button - display_element.innerHTML += ''; + display_element.innerHTML += ''; display_element.querySelector('#jspsych-reconstruction-next').addEventListener('click', endTrial); } diff --git a/plugins/jspsych-resize.js b/plugins/jspsych-resize.js new file mode 100644 index 00000000..ff7e2154 --- /dev/null +++ b/plugins/jspsych-resize.js @@ -0,0 +1,135 @@ +/** +* jspsych-resize +* Steve Chao +* +* plugin for controlling the real world size of the display +* +* documentation: docs.jspsych.org +* +**/ + +jsPsych.plugins["resize"] = (function() { + + var plugin = {}; + + plugin.trial = function(display_element, trial) { + + // if any trial variables are functions + // this evaluates the function and replaces + // it with the output of the function + trial = jsPsych.pluginAPI.evaluateFunctionParameters(trial); + + // default trial paramters + trial.item_height = trial.item_height || 1; + trial.item_width = trial.item_width || 1; + trial.prompt = trial.prompt || ' '; + trial.pixels_per_unit = trial.pixels_per_unit || 100; + trial.starting_size = trial.starting_size || 100; + trial.button_label = trial.button_label || "Done"; + + var aspect_ratio = trial.item_width / trial.item_height; + + // variables to determine div size + if(trial.item_width >= trial.item_height){ + var start_div_width = trial.starting_size; + var start_div_height = Math.round(trial.starting_size / aspect_ratio); + } else { + var start_div_height = trial.starting_size; + var start_div_width = Math.round(trial.starting_size * aspect_ratio); + } + + // create html for display + var html ='
'; + html += '
'; + html += '
'; + html += trial.prompt; + html += ''+trial.button_label+''; + + // render + display_element.innerHTML = html; + + // listens for the click + document.getElementById("jspsych-resize-btn").addEventListener('click', function() { + scale(); + end_trial(); + }); + + var dragging = false; + var origin_x, origin_y; + var cx, cy; + + var mousedownevent = function(e){ + e.preventDefault(); + e.stopPropagation(); + dragging = true; + origin_x = e.pageX; + origin_y = e.pageY; + cx = parseInt(scale_div.style.width); + cy = parseInt(scale_div.style.height); + } + + display_element.querySelector('#jspsych-resize-handle').addEventListener('mousedown', mousedownevent); + + var mouseupevent = function(e){ + dragging = false; + } + + document.addEventListener('mouseup', mouseupevent); + + var scale_div = display_element.querySelector('#jspsych-resize-div'); + + var resizeevent = function(e){ + if(dragging){ + var dx = (e.pageX - origin_x)*2; + var dy = (e.pageY - origin_y)*2; + + if(dx >= dy){ + scale_div.style.width = Math.max(10, cx+dx) + "px"; + scale_div.style.height = Math.round(Math.max(10, cx+dx) / aspect_ratio ) + "px"; + } else { + scale_div.style.height = Math.max(10, cy+dy) + "px"; + scale_div.style.width = Math.round(aspect_ratio * Math.max(10, cy+dy)) + "px"; + } + } + } + + document.addEventListener('mousemove', resizeevent); + + // scales the stimulus + var scale_factor; + var final_height_px, final_width_px; + function scale() { + final_width_px = scale_div.offsetWidth; + final_height_px = scale_div.offsetHeight; + + var pixels_unit_screen = final_width_px / trial.item_width; + + scale_factor = pixels_unit_screen / trial.pixels_per_unit; + document.getElementById("jspsych-content").style.transform = "scale(" + scale_factor + ")"; + }; + + + // function to end trial + function end_trial() { + + // clear document event listeners + document.removeEventListener('mousemove', resizeevent); + document.removeEventListener('mouseup', mouseupevent); + + // clear the screen + display_element.innerHTML = ''; + + // finishes trial + + var trial_data = { + 'final_height_px': final_height_px, + 'final_width_px': final_width_px, + 'scale_factor': scale_factor + } + + jsPsych.finishTrial(trial_data); + } + }; + + return plugin; +})(); diff --git a/plugins/jspsych-same-different.js b/plugins/jspsych-same-different.js index fbf71c88..7552a99c 100644 --- a/plugins/jspsych-same-different.js +++ b/plugins/jspsych-same-different.js @@ -82,6 +82,7 @@ jsPsych.plugins['same-different'] = (function() { // default parameters trial.same_key = trial.same_key || 81; // default is 'q' trial.different_key = trial.different_key || 80; // default is 'p' + trial.advance_key = trial.advance_key || jsPsych.ALL_KEYS // timing parameters trial.timing_first_stim = trial.timing_first_stim || 1000; // if -1, the first stim is shown until any key is pressed trial.timing_second_stim = trial.timing_second_stim || 1000; // if -1, then second stim is shown until response. @@ -112,11 +113,17 @@ jsPsych.plugins['same-different'] = (function() { first_stim_info = info; showBlankScreen(); } - jsPsych.pluginAPI.getKeyboardResponse(afterKeyboardResponse, [], 'date', false); + jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: afterKeyboardResponse, + valid_responses: trial.advance_key, + rt_method: 'date', + persist: false, + allow_held_key: false + }); } function showBlankScreen() { - display_element.querySelector('.jspsych-same-different-stimulus').outerHTML = ''; + display_element.innerHTML = ''; jsPsych.pluginAPI.setTimeout(function() { showSecondStim(); @@ -125,14 +132,14 @@ jsPsych.plugins['same-different'] = (function() { function showSecondStim() { if (!trial.is_html) { - display_element.innerHTML += ''; + display_element.innerHTML += ''; } else { - display_element.innerHTML += '
'+trial.stimuli[1]+'
'; + display_element.innerHTML += '
'+trial.stimuli[1]+'
'; } if (trial.timing_second_stim > 0) { jsPsych.pluginAPI.setTimeout(function() { - display_element.querySelector('#jspsych-same-different-second-stimulus').style.visibility = 'hidden'; + display_element.querySelector('.jspsych-same-different-stimulus').style.visibility = 'hidden'; }, trial.timing_second_stim); } diff --git a/plugins/jspsych-serial-reaction-time.js b/plugins/jspsych-serial-reaction-time.js index eed8ed52..3f69dd0d 100644 --- a/plugins/jspsych-serial-reaction-time.js +++ b/plugins/jspsych-serial-reaction-time.js @@ -19,6 +19,7 @@ jsPsych.plugins["serial-reaction-time"] = (function() { trial.grid = trial.grid || [[1,1,1,1]]; trial.choices = trial.choices || [['3','5','7','9']]; trial.grid_square_size = trial.grid_square_size || 100; + trial.target_color = trial.target_color || "#999"; trial.response_ends_trial = (typeof trial.response_ends_trial === 'undefined') ? true : trial.response_ends_trial; trial.timing_pre_target = (typeof trial.timing_pre_target === 'undefined') ? 0 : trial.timing_pre_target; trial.timing_max_duration = trial.timing_max_duration || -1; // if -1, then wait for response forever @@ -28,7 +29,10 @@ jsPsych.plugins["serial-reaction-time"] = (function() { trial.prompt = (typeof trial.prompt === 'undefined') ? "" : trial.prompt; // create a flattened version of the choices array - var flat_choices = flatten(trial.choices); + var flat_choices = jsPsych.utils.flatten(trial.choices); + while(flat_choices.indexOf('') > -1){ + flat_choices.splice(flat_choices.indexOf(''),1); + } // display stimulus var stimulus = this.stimulus(trial.grid, trial.grid_square_size); @@ -51,15 +55,15 @@ jsPsych.plugins["serial-reaction-time"] = (function() { function showTarget(){ if(trial.fade_duration == -1){ - display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+trial.target[0]+'-'+trial.target[1]).style.backgroundColor = '#999'; + display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+trial.target[0]+'-'+trial.target[1]).style.backgroundColor = trial.target_color; } else { display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+trial.target[0]+'-'+trial.target[1]).style.transition = "background-color "+trial.fade_duration; - display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+trial.target[0]+'-'+trial.target[1]).style.backgroundColor = '#999'; + display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+trial.target[0]+'-'+trial.target[1]).style.backgroundColor = trial.target_color; } keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({ callback_function: after_response, - valid_responses: trial.flat_choices, + valid_responses: flat_choices, allow_held_key: false }); @@ -124,8 +128,8 @@ jsPsych.plugins["serial-reaction-time"] = (function() { if (trial.show_response_feedback){ var color = response.correct ? '#0f0' : '#f00'; - display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+responseLoc[1]+'-'+responseLoc[0]).style.transition = ""; - display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+responseLoc[1]+'-'+responseLoc[0]).style.backgroundColor = color; + display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+responseLoc[0]+'-'+responseLoc[1]).style.transition = ""; + display_element.querySelector('#jspsych-serial-reaction-time-stimulus-cell-'+responseLoc[0]+'-'+responseLoc[1]).style.backgroundColor = color; } if (trial.response_ends_trial) { diff --git a/plugins/jspsych-similarity.js b/plugins/jspsych-similarity.js index 1433f4a3..3d7341ee 100644 --- a/plugins/jspsych-similarity.js +++ b/plugins/jspsych-similarity.js @@ -81,6 +81,12 @@ jsPsych.plugins.similarity = (function() { default: '', no_function: false, description: '' + }, + button_label: { + type: [jsPsych.plugins.parameterType.STRING], + default: '', + no_function: false, + description: 'Submit Answers' } } } @@ -100,6 +106,8 @@ jsPsych.plugins.similarity = (function() { trial.is_html = (typeof trial.is_html === 'undefined') ? false : trial.is_html; trial.prompt = (typeof trial.prompt === 'undefined') ? '' : trial.prompt; + + trial.button_label = typeof trial.button_label === 'undefined' ? 'Submit Answers' : trial.button_label; // if any trial variables are functions // this evaluates the function and replaces @@ -231,7 +239,7 @@ jsPsych.plugins.similarity = (function() { display_element.append($(''; + form_element.innerHTML += ''; - display_element.querySelector('#jspsych-survey-likert-next').addEventListener('click', function(){ + form_element.addEventListener('submit', function(e){ + e.preventDefault(); // measure response time var endTime = (new Date()).getTime(); var response_time = endTime - startTime; @@ -86,10 +99,12 @@ jsPsych.plugins['survey-likert'] = (function() { var question_data = {}; var matches = display_element.querySelectorAll('#jspsych-survey-likert-form .jspsych-survey-likert-opts'); for(var index = 0; index < matches.length; index++){ - var id = matches[index].dataset['radio-group']; - var response = display_element.querySelector('input[name="' + id + '"]:checked').value; - if (typeof response == 'undefined') { - response = -1; + var id = matches[index].dataset['radioGroup']; + var el = display_element.querySelector('input[name="' + id + '"]:checked'); + if (el === null) { + var response = ""; + } else { + var response = parseInt(el.value); } var obje = {}; obje[id] = response; diff --git a/plugins/jspsych-survey-multi-choice.js b/plugins/jspsych-survey-multi-choice.js index 934282d1..d894849c 100644 --- a/plugins/jspsych-survey-multi-choice.js +++ b/plugins/jspsych-survey-multi-choice.js @@ -48,6 +48,12 @@ jsPsych.plugins['survey-multi-choice'] = (function() { default: '', no_function: false, description: '' + }, + button_label: { + type: [jsPsych.plugins.parameterType.STRING], + default: '', + no_function: false, + description: '' } } } @@ -63,6 +69,8 @@ jsPsych.plugins['survey-multi-choice'] = (function() { trial.preamble = typeof trial.preamble == 'undefined' ? "" : trial.preamble; trial.required = typeof trial.required == 'undefined' ? null : trial.required; trial.horizontal = typeof trial.required == 'undefined' ? false : trial.horizontal; + //If button_label is empty, the browser's language will be used to determine the button label. + trial.button_label = typeof trial.button_label === 'undefined' ? '' : trial.button_label; // if any trial variables are functions // this evaluates the function and replaces @@ -95,11 +103,11 @@ jsPsych.plugins['survey-multi-choice'] = (function() { if (trial.horizontal) { question_classes.push(_join(plugin_id_name, 'horizontal')); } - + trial_form.innerHTML += '
'; - + var question_selector = _join(plugin_id_selector, i); - + // add question text display_element.querySelector(question_selector).innerHTML += '

' + trial.questions[i] + '

'; @@ -107,38 +115,39 @@ jsPsych.plugins['survey-multi-choice'] = (function() { for (var j = 0; j < trial.options[i].length; j++) { var option_id_name = _join(plugin_id_name, "option", i, j), option_id_selector = '#' + option_id_name; - + // add radio button container display_element.querySelector(question_selector).innerHTML += '
'; - + // add label and question text var form = document.getElementById(option_id_name) - var input_id_name = _join(plugin_id_name, 'response', i); + var input_name = _join(plugin_id_name, 'response', i); + var input_id = _join(plugin_id_name, 'response', i, j); var label = document.createElement('label'); label.setAttribute('class', plugin_id_name+'-text'); label.innerHTML = trial.options[i][j]; - label.setAttribute('for', input_id_name) - + label.setAttribute('for', input_id) + // create radio button var input = document.createElement('input'); input.setAttribute('type', "radio"); - input.setAttribute('name', input_id_name); - input.setAttribute('value', trial.options[i][j]) - label.prepend(input); - form.appendChild(label) + input.setAttribute('name', input_name); + input.setAttribute('id', input_id); + input.setAttribute('value', trial.options[i][j]); + form.appendChild(label); + form.insertBefore(input, label); } if (trial.required && trial.required[i]) { // add "question required" asterisk display_element.querySelector(question_selector + " p").innerHMTL += "*"; - + // add required property display_element.querySelector(question_selector + " input[type=radio]").required = true; } } // add submit button - trial_form.innerHTML += ''; - + trial_form.innerHTML += ''; trial_form.addEventListener('submit', function(event) { event.preventDefault(); var matches = display_element.querySelectorAll("div." + plugin_id_name + "-question"); @@ -149,24 +158,29 @@ jsPsych.plugins['survey-multi-choice'] = (function() { // create object to hold responses var question_data = {}; var matches = display_element.querySelectorAll("div." + plugin_id_name + "-question"); - matches.forEach(function(match, index) { - var id = "Q" + index; - var val = match.querySelector("input[type=radio]:checked").value; + for(var i=0; i'; + var trial_form = display_element.querySelector("#" + trial_form_id); + // show preamble text + var preamble_id_name = _join(plugin_id_name, 'preamble'); + trial_form.innerHTML += '
'+trial.preamble+'
'; + + // add multiple-picture questions + for (var i = 0; i < trial.questions.length; i++) { + // create question container + var question_classes = [_join(plugin_id_name, 'question')]; + if (trial.horizontal) { + question_classes.push(_join(plugin_id_name, 'horizontal')); + } + + trial_form.innerHTML += '
'; + + var question_selector = _join(plugin_id_selector, i); + + // add question text + display_element.querySelector(question_selector).innerHTML += '

' + trial.questions[i] + '

'; + + // create option clickble images + for (var j = 0; j < trial.options[i].length; j++) { + var option_id_name = _join(plugin_id_name, "option", i, j), + option_id_selector = '#' + option_id_name; + + // add image container + display_element.querySelector(question_selector).innerHTML += '
'; + + // add label + if(trial.options[i][j].label){ + var label = trial.options[i][j].label; + var option_label = ''; + display_element.querySelector(option_id_selector).innerHTML += option_label; + } else { + var option_label = ''; + display_element.querySelector(option_id_selector).innerHTML += option_label; + } + display_element.querySelector(option_id_selector + " label").innerHTML = + '' + + display_element.querySelector(option_id_selector + " label").innerHTML; + } + } + var matches = display_element.querySelectorAll(".jspsych-survey-multi-picture-option"); + for(var index=0; index'; + var trial_form = display_element.querySelector("#" + trial_form_id); + // show preamble text + var preamble_id_name = _join(plugin_id_name, 'preamble'); + trial_form.innerHTML += '
'+trial.preamble+'
'; + + // add multiple-select questions + for (var i = 0; i < trial.questions.length; i++) { + // create question container + var question_classes = [_join(plugin_id_name, 'question')]; + if (trial.horizontal) { + question_classes.push(_join(plugin_id_name, 'horizontal')); + } + + trial_form.innerHTML += '
'; + + var question_selector = _join(plugin_id_selector, i); + + // add question text + display_element.querySelector(question_selector).innerHTML += '

' + trial.questions[i] + '

'; + + // create option check boxes + for (var j = 0; j < trial.options[i].length; j++) { + var option_id_name = _join(plugin_id_name, "option", i, j), + option_id_selector = '#' + option_id_name; + + // add check box container + display_element.querySelector(question_selector).innerHTML += '
'; + + // add label and question text + var form = document.getElementById(option_id_name) + var input_name = _join(plugin_id_name, 'response', i); + var input_id = _join(plugin_id_name, 'response', i, j); + var label = document.createElement('label'); + label.setAttribute('class', plugin_id_name+'-text'); + label.innerHTML = trial.options[i][j]; + label.setAttribute('for', input_id) + + // create checkboxes + var input = document.createElement('input'); + input.setAttribute('type', "checkbox"); + input.setAttribute('name', input_name); + input.setAttribute('id', input_id); + input.setAttribute('value', trial.options[i][j]) + form.appendChild(label) + form.insertBefore(input, label) + } + } + // add submit button + trial_form.innerHTML +='
' + trial_form.innerHTML += ''; + + trial_form.addEventListener('submit', function(event) { + event.preventDefault(); + // measure response time + var endTime = (new Date()).getTime(); + var response_time = endTime - startTime; + + // create object to hold responses + var matches = display_element.querySelectorAll("div." + plugin_id_name + "-question"); + var question_data = {}; + var has_response = []; + for(var index=0; index'; + } else { + // save data + var trial_data = { + "rt": response_time, + "responses": JSON.stringify(question_data) + }; + display_element.innerHTML = ''; + + // next trial + jsPsych.finishTrial(trial_data); + } + }); + + var startTime = (new Date()).getTime(); + }; + + return plugin; +})(); diff --git a/plugins/jspsych-survey-text.js b/plugins/jspsych-survey-text.js index ac473abc..5d4c3f9f 100644 --- a/plugins/jspsych-survey-text.js +++ b/plugins/jspsych-survey-text.js @@ -24,7 +24,7 @@ jsPsych.plugins['survey-text'] = (function() { no_function: false, description: '' }, - premable: { + preamble: { type: [jsPsych.plugins.parameterType.STRING], default: '', no_function: false, @@ -43,6 +43,19 @@ jsPsych.plugins['survey-text'] = (function() { default: 40, no_function: false, description: '' + }, + values: { + type: [jsPsych.plugins.parameterType.STRING], + array: true, + default: '', + no_function: false, + description: '' + }, + button_label: { + type: [jsPsych.plugins.parameterType.STRING], + default: '', + no_function: false, + description: 'Submit Answers' } } } @@ -50,6 +63,8 @@ jsPsych.plugins['survey-text'] = (function() { plugin.trial = function(display_element, trial) { trial.preamble = typeof trial.preamble == 'undefined' ? "" : trial.preamble; + trial.button_label = typeof trial.button_label === 'undefined' ? 'Submit Answers' : trial.button_label; + if (typeof trial.rows == 'undefined') { trial.rows = []; for (var i = 0; i < trial.questions.length; i++) { @@ -62,6 +77,12 @@ jsPsych.plugins['survey-text'] = (function() { trial.columns.push(40); } } + if (typeof trial.values == 'undefined') { + trial.values = []; + for (var i = 0; i < trial.questions.length; i++) { + trial.values.push(""); + } + } // if any trial variables are functions // this evaluates the function and replaces @@ -69,18 +90,24 @@ jsPsych.plugins['survey-text'] = (function() { trial = jsPsych.pluginAPI.evaluateFunctionParameters(trial); // show preamble text - display_element.innerHTML += '
'+trial.preamble+'
'; + var html = '
'+trial.preamble+'
'; // add questions for (var i = 0; i < trial.questions.length; i++) { - display_element.innerHTML += '
'+ - '

' + trial.questions[i] + '

'+ - ''+ - '
'; + html += '
'; + html += '

' + trial.questions[i] + '

'; + if(trial.rows[i] == 1){ + html += ''+trial.values[i]+''; + } else { + html += ''; + } + html += '
'; } // add submit button - display_element.innerHTML += ''; + html += ''; + + display_element.innerHTML = html; display_element.querySelector('#jspsych-survey-text-next').addEventListener('click', function() { // measure response time @@ -92,7 +119,7 @@ jsPsych.plugins['survey-text'] = (function() { var matches = display_element.querySelectorAll('div.jspsych-survey-text-question'); for(var index=0; index'; - } + + // adding start stop parameters if specified + video_html+=''; + } video_html +="" display_element.innerHTML += video_html; diff --git a/plugins/jspsych-vsl-grid-scene.js b/plugins/jspsych-vsl-grid-scene.js index aaf330c8..6ded60ce 100644 --- a/plugins/jspsych-vsl-grid-scene.js +++ b/plugins/jspsych-vsl-grid-scene.js @@ -90,7 +90,7 @@ jsPsych.plugins['vsl-grid-scene'] = (function() { for (var col = 0; col < ncols; col++) { display_element.querySelector('#jspsych-vsl-grid-scene-table-row-' + row).innerHTML += ''+ - '
'; + '
'; } } diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..391f9018 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,7 @@ +# Testing jsPsych + +Tests are written with [Jest](https://facebook.github.io/jest/). + +To run the tests, install Node and npm. Run `npm install` in the root jsPsych directory. Then run `npm test`. + +To add tests, follow examples contained in this folder. diff --git a/tests/events.test.js b/tests/events.test.js new file mode 100644 index 00000000..d330d3d5 --- /dev/null +++ b/tests/events.test.js @@ -0,0 +1,297 @@ +describe('on_finish (trial)', function(){ + test('should get an object of data generated by the trial', function(){ + + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + return (new Promise(function(resolve, reject){ + + var key_data = null; + + var trial = { + type: 'text', + text: 'hello', + on_finish: function(data){ + key_data = data.key_press; + } + } + + jsPsych.init({ + timeline: [trial], + on_finish: function() { + resolve({key_data}); + } + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + })).then(function(data) { expect(data.key_data).toBe(32) }); + }); + + test('should be able to write to the data', function(){ + + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + return (new Promise(function(resolve, reject){ + + var promise_data = {}; + + var trial = { + type: 'text', + text: 'hello', + on_finish: function(data){ + data.key_press = 1; + } + } + + jsPsych.init({ + timeline: [trial], + on_finish: function() { + promise_data.final_key_press = jsPsych.data.get().values()[0].key_press; + resolve(promise_data); + } + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + //resolve(); + })).then(function(pd) { + expect(pd.final_key_press).toBe(1); + }); + }); +}); + +describe('on_trial_finish (experiment level)', function(){ + test('should get an object containing the trial data', function(){ + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + return (new Promise(function(resolve, reject){ + + var promise_data = {}; + + var trial = { + type: 'text', + text: 'hello' + } + + jsPsych.init({ + timeline: [trial], + on_trial_finish: function(data){ + promise_data.key = data.key_press; + }, + on_finish: function(){ + resolve(promise_data); + } + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + //resolve(); + })).then(function(pd) { + expect(pd.key).toBe(32); + }); + }); + + test('should allow writing to the data object', function(){ + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + return (new Promise(function(resolve, reject){ + + var promise_data = {}; + + var trial = { + type: 'text', + text: 'hello' + } + + jsPsych.init({ + timeline: [trial], + on_trial_finish: function(data){ + data.write = true; + }, + on_finish: function(data){ + promise_data.write = data.values()[0].write; + resolve(promise_data); + } + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + //resolve(); + })).then(function(pd) { + expect(pd.write).toBe(true); + }); + }); +}); + +describe('on_data_update', function(){ + test('should get an object containing the trial data', function(){ + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + return (new Promise(function(resolve, reject){ + + var promise_data = {}; + + var trial = { + type: 'text', + text: 'hello' + } + + jsPsych.init({ + timeline: [trial], + on_data_update: function(data){ + promise_data.key = data.key_press; + }, + on_finish: function(){ + resolve(promise_data); + } + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + //resolve(); + })).then(function(pd) { + expect(pd.key).toBe(32); + }); + }); + + test('should contain data added with on_finish (trial level)', function(){ + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + return (new Promise(function(resolve, reject){ + + var promise_data = {}; + + var trial = { + type: 'text', + text: 'hello', + on_finish: function(data){ + data.trial_level = true; + } + } + + jsPsych.init({ + timeline: [trial], + on_data_update: function(data){ + promise_data.trial_level = data.trial_level; + }, + on_finish: function(){ + resolve(promise_data); + } + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + //resolve(); + })).then(function(pd) { + expect(pd.trial_level).toBe(true); + }); + }); + + test('should contain data added with on_trial_finish (experiment level)', function(){ + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + return (new Promise(function(resolve, reject){ + + var promise_data = {}; + + var trial = { + type: 'text', + text: 'hello' + } + + jsPsych.init({ + timeline: [trial], + on_trial_finish: function(data){ + data.experiment_level = true; + }, + on_data_update: function(data){ + promise_data.experiment_level = data.experiment_level; + }, + on_finish: function(){ + resolve(promise_data); + } + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + //resolve(); + })).then(function(pd) { + expect(pd.experiment_level).toBe(true); + }); + }); +}); + +describe('on_trial_start', function(){ + + test('should get an object containing the trial properties', function(){ + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + return (new Promise(function(resolve, reject){ + + var promise_data = {}; + + var trial = { + type: 'text', + text: 'hello' + } + + jsPsych.init({ + timeline: [trial], + on_trial_start: function(trial){ + promise_data.text = trial.text; + }, + on_finish: function(){ + resolve(promise_data); + } + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + //resolve(); + })).then(function(pd) { + expect(pd.text).toBe('hello'); + }); + }); + + test('should allow modification of the trial properties', function(){ + require('../jspsych.js'); + require('../plugins/jspsych-text.js'); + + var trial = { + type: 'text', + text: 'hello' + } + + jsPsych.init({ + timeline: [trial], + on_trial_start: function(trial){ + trial.text = 'goodbye'; + }, + on_finish: function(){ + resolve(promise_data); + } + }); + + var display_element = jsPsych.getDisplayElement(); + expect(display_element.innerHTML).toBe('goodbye'); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + }); +}); diff --git a/tests/jsPsych.data/datacollection.test.js b/tests/jsPsych.data/datacollection.test.js index 07bfd23e..cdfdb631 100644 --- a/tests/jsPsych.data/datacollection.test.js +++ b/tests/jsPsych.data/datacollection.test.js @@ -79,4 +79,16 @@ describe('DataCollection', function(){ expect(data.count()).toBe(3); expect(data.values()[2].rt).toBe(500); }); + test('#unqiueNames', function(){ + var data = [ + {rt: 100, filter: true}, + {rt: 200, filter: false}, + {rt: 300, filter: true, v1: false}, + {rt: 400, filter: false, v2: true}, + {rt: 500, filter: false, v1: false} + ]; + + jsPsych.data._customInsert(data); + expect(jsPsych.data.get().uniqueNames().length).toBe(4); + }) }); diff --git a/tests/jsPsych.utils/utils.test.js b/tests/jsPsych.utils/utils.test.js new file mode 100644 index 00000000..506fe536 --- /dev/null +++ b/tests/jsPsych.utils/utils.test.js @@ -0,0 +1,32 @@ +const root = '../../'; + +require(root + 'jspsych.js'); + +describe('unique', function(){ + test('generates unique array when there are duplicates', function(){ + var arr = [1,1,2,2,3,3]; + var out = jsPsych.utils.unique(arr); + expect(out).toEqual([1,2,3]); + expect(out).not.toEqual(arr); + }); + + test('generates same array when there are no duplicates', function(){ + var arr = [1,2,3]; + var out = jsPsych.utils.unique(arr); + expect(out).toEqual(arr); + }) +}); + +describe('flatten', function(){ + test('generates flat array from flat input', function(){ + var arr = [1,1,2,2,3,3]; + var out = jsPsych.utils.flatten(arr); + expect(out).toEqual(arr); + }); + + test('generates flat array from nested input', function(){ + var arr = [1,[1,2,2],[3],3]; + var out = jsPsych.utils.flatten(arr); + expect(out).toEqual([1,1,2,2,3,3]); + }); +}); diff --git a/tests/loads.test.js b/tests/loads.test.js new file mode 100644 index 00000000..fa1275db --- /dev/null +++ b/tests/loads.test.js @@ -0,0 +1,7 @@ +const root = '../'; + +require(root + 'jspsych.js'); + +test('jsPsych should be in the window object', function(){ + expect(typeof window.jsPsych).not.toBe('undefined'); +}); diff --git a/tests/media/blue.png b/tests/media/blue.png new file mode 100644 index 00000000..820bdce8 Binary files /dev/null and b/tests/media/blue.png differ diff --git a/tests/media/orange.png b/tests/media/orange.png new file mode 100644 index 00000000..108e6e57 Binary files /dev/null and b/tests/media/orange.png differ diff --git a/tests/on_finish-trial-level.test.js b/tests/on_finish-trial-level.test.js deleted file mode 100644 index 4b62ab2f..00000000 --- a/tests/on_finish-trial-level.test.js +++ /dev/null @@ -1,132 +0,0 @@ -describe('The on_finish trial level event handler', function(){ - test('should get an object of data generated by the trial', function(){ - - require('../jspsych.js'); - require('../plugins/jspsych-text.js'); - - return (new Promise(function(resolve, reject){ - - var key_data = null; - - var trial = { - type: 'text', - text: 'hello', - on_finish: function(data){ - key_data = data.key_press; - } - } - - jsPsych.init({ - timeline: [trial], - on_finish: function() { - resolve({key_data}); - } - }); - - document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); - document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); - - //resolve(); - })).then(function(data) { expect(data.key_data).toBe(32) }); - }); - - test('data should be writeable', function(){ - - require('../jspsych.js'); - require('../plugins/jspsych-text.js'); - - return (new Promise(function(resolve, reject){ - - var promise_data = {}; - - var trial = { - type: 'text', - text: 'hello', - on_finish: function(data){ - data.key_press = 1; - } - } - - jsPsych.init({ - timeline: [trial], - on_finish: function() { - promise_data.final_key_press = jsPsych.data.get().values()[0].key_press; - resolve(promise_data); - } - }); - - document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); - document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); - - //resolve(); - })).then(function(pd) { - expect(pd.final_key_press).toBe(1); - }); - }); -}); - -describe('The on_trial_finish handler for trials set at the experiment level', function(){ - test('should get an object containing the trial data', function(){ - require('../jspsych.js'); - require('../plugins/jspsych-text.js'); - - return (new Promise(function(resolve, reject){ - - var promise_data = {}; - - var trial = { - type: 'text', - text: 'hello' - } - - jsPsych.init({ - timeline: [trial], - on_trial_finish: function(data){ - promise_data.key = data.key_press; - }, - on_finish: function(){ - resolve(promise_data); - } - }); - - document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); - document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); - - //resolve(); - })).then(function(pd) { - expect(pd.key).toBe(32); - }); - }); - test('should allow writing to the data object', function(){ - require('../jspsych.js'); - require('../plugins/jspsych-text.js'); - - return (new Promise(function(resolve, reject){ - - var promise_data = {}; - - var trial = { - type: 'text', - text: 'hello' - } - - jsPsych.init({ - timeline: [trial], - on_trial_finish: function(data){ - data.write = true; - }, - on_finish: function(data){ - promise_data.write = data.values()[0].write; - resolve(promise_data); - } - }); - - document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); - document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); - - //resolve(); - })).then(function(pd) { - expect(pd.write).toBe(true); - }); - }); -}); diff --git a/tests/plugins/plugin-fullscreen.test.js b/tests/plugins/plugin-fullscreen.test.js new file mode 100644 index 00000000..8a725ccb --- /dev/null +++ b/tests/plugins/plugin-fullscreen.test.js @@ -0,0 +1,41 @@ +const root = '../../'; + +jest.useFakeTimers(); + +describe('fullscreen plugin', function(){ + + beforeEach(function(){ + require(root + 'jspsych.js'); + require(root + 'plugins/jspsych-fullscreen.js'); + require(root + 'plugins/jspsych-text.js'); + }); + + test('loads correctly', function(){ + expect(typeof window.jsPsych.plugins['fullscreen']).not.toBe('undefined'); + }); + + // can't test this right now because jsdom doesn't support fullscreen API. + + /*test('launches fullscreen mode by default', function(){ + var trial = { + type: 'fullscreen', + delay_after: 0 + } + + var text = { + type: 'text', + text: 'fullscreen' + } + + jsPsych.init({ + timeline: [trial, text] + }); + + expect(document.fullscreenElement).toBeUndefined(); + console.log(jsPsych.getDisplayElement().requestFullscreen); + document.querySelector('#jspsych-fullscreen-btn').dispatchEvent(new MouseEvent('click', {})); + + expect(document.fullscreenElement).not.toBeUndefined(); + });*/ + +}); diff --git a/tests/plugins/plugin-same-different.test.js b/tests/plugins/plugin-same-different.test.js new file mode 100644 index 00000000..bf15e0a2 --- /dev/null +++ b/tests/plugins/plugin-same-different.test.js @@ -0,0 +1,70 @@ +const root = '../../'; + +jest.useFakeTimers(); + +describe('same-different plugin', function(){ + + beforeEach(function(){ + require(root + 'jspsych.js'); + require(root + 'plugins/jspsych-same-different.js'); + }); + + test('loads correctly', function(){ + expect(window.jsPsych.plugins['same-different']).not.toBeUndefined(); + }); + + test('works with default parameters', function(){ + var trial = { + type: 'same-different', + stimuli: ['../media/blue.png','../media/blue.png'], + answer: 'same' + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + jest.runTimersToTime(1000); + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + jest.runTimersToTime(500); + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 81})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 81})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + + expect(jsPsych.data.get().values()[0].correct).toBe(true); + + }); + + test('check when timing_first_stim is -1', function(){ + var trial = { + type: 'same-different', + stimuli: ['../media/blue.png','../media/blue.png'], + answer: 'same', + timing_first_stim: -1 + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 81})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 81})); + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + jest.runTimersToTime(500); + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 81})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 81})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + + expect(jsPsych.data.get().values()[0].correct).toBe(true); + + }); + +}); diff --git a/tests/plugins/plugin-single-stim.test.js b/tests/plugins/plugin-single-stim.test.js index 28034c2e..c8f99394 100644 --- a/tests/plugins/plugin-single-stim.test.js +++ b/tests/plugins/plugin-single-stim.test.js @@ -1,34 +1,223 @@ const root = '../../'; -require(root + 'jspsych.js'); -require(root + 'plugins/jspsych-single-stim.js'); +jest.useFakeTimers(); -test('jsPsych should be in the window object', function(){ - expect(typeof window.jsPsych).not.toBe('undefined'); +describe('single-stim plugin', function(){ + + beforeEach(function(){ + require(root + 'jspsych.js'); + require(root + 'plugins/jspsych-single-stim.js'); + }); + + test('loads correctly', function(){ + expect(typeof window.jsPsych.plugins['single-stim']).not.toBe('undefined'); + }); + + test('displays image by default', function(){ + var trial = { + type: 'single-stim', + stimulus: '../media/blue.png' + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + }); + + test('displays html when is_html is true', function(){ + var trial = { + type: 'single-stim', + stimulus: '

hello

', + is_html: true + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

'); + }); + + test('display should clear after key press', function(){ + var trial = { + type: 'single-stim', + stimulus: '

hello

', + is_html: true + } + + jsPsych.init({ + timeline: [trial] + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + }); + + test('display should not clear after key press when choices is jsPsych.NO_KEYS', function(){ + var trial = { + type: 'single-stim', + stimulus: '

hello

', + is_html: true, + choices: jsPsych.NO_KEYS + } + + jsPsych.init({ + timeline: [trial] + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

'); + }); + + test('display should only clear when key is in choices array', function(){ + var trial = { + type: 'single-stim', + stimulus: '

hello

', + is_html: true, + choices: ['f'] + } + + jsPsych.init({ + timeline: [trial] + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

'); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 70})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 70})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + }); + + test('prompt should append html below stimulus', function(){ + var trial = { + type: 'single-stim', + stimulus: '

hello

', + is_html: true, + prompt: '
this is the prompt
' + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

this is the prompt
'); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 70})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 70})); + + }); + + test('timing_stim should set visibility of content to hidden after time has elapsed', function(){ + + var trial = { + type: 'single-stim', + stimulus: '

hello

', + is_html: true, + timing_stim: 500 + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

'); + + jest.runAllTimers(); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 70})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 70})); + + }); + + test('timing_response should end trial after time has elapsed', function(){ + + var trial = { + type: 'single-stim', + stimulus: '

hello

', + is_html: true, + timing_response: 500 + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

'); + + jest.runAllTimers(); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + + }); + + test('trial should not end when response_ends_trial is false and stimulus should get responded class', function(){ + var trial = { + type: 'single-stim', + stimulus: '

hello

', + is_html: true, + response_ends_trial: false, + timing_response: 500 + } + + jsPsych.init({ + timeline: [trial] + }); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 70})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 70})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

'); + + jest.runAllTimers(); + }); + + test('should accept functions as parameters', function(){ + + var trial = { + type: 'single-stim', + stimulus: function(){ return '

hello

'; }, + is_html: function(){ return true; }, + choices: function(){ return ['j']; }, + prompt: function(){ return '
prompt
'; }, + timing_response: function(){ return 1000; }, + timing_stim: function(){ return 500; }, + response_ends_trial: function(){ return false; } + } + + jsPsych.init({ + timeline: [trial] + }); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

prompt
'); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

prompt
'); + + document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 70})); + document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 70})); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('

hello

prompt
'); + + jest.runTimersToTime(500); + + expect(jsPsych.getDisplayElement().innerHTML).toBe('
prompt
'); + + jest.runTimersToTime(1000); + + expect(jsPsych.getDisplayElement().innerHTML).toBe(''); + }); }); - -test('the single-stim plugin should be loaded', function(){ - expect(typeof window.jsPsych.plugins['single-stim']).not.toBe('undefined'); -}) - -var trial = { - type: 'single-stim', - stimulus: '

Hello

', - is_html: true -} - -jsPsych.init({ - timeline: [trial] -}); - -test('HMTL stimulus should display', function(){ - var display_element = jsPsych.getDisplayElement(); - expect(display_element.innerHTML).toBe('

Hello

'); -}); - -test('Display should clear after keypress', function(){ - document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 32})); - document.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 32})); - var display_element = jsPsych.getDisplayElement(); - expect(display_element.innerHTML).toBe(''); -}) diff --git a/tests/timeline-variables.test.js b/tests/timeline-variables.test.js new file mode 100644 index 00000000..4dccf3e1 --- /dev/null +++ b/tests/timeline-variables.test.js @@ -0,0 +1,69 @@ +const root = '../'; + +beforeEach(function(){ + require(root + 'jspsych.js'); + require(root + 'plugins/jspsych-text.js'); +}); + +describe('randomize order', function(){ + test('holder', function(){ + expect(true).toBe(true); + }); +}); + +describe('repetitons', function(){ + test('holder', function(){ + expect(true).toBe(true); + }); +}); + +describe('sampling', function(){ + test('holder', function(){ + expect(true).toBe(true); + }); + + test('sampling functions run when timeline loops', function(){ + + var count = 0; + const reps = 100; + + var trial = { + timeline: [{ + type: 'text', + text: jsPsych.timelineVariable('text') + }], + timeline_variables: [ + {text: '1'}, + {text: '2'}, + {text: '3'} + ], + sample: { + type: 'without-replacement', + size: 1 + }, + loop_function: function(){ + count++; + return(count < reps); + } + } + + jsPsych.init({ + timeline: [trial] + }); + + var result_1 = []; + var result_2 = []; + for(var i=0; i