diff --git a/.changeset/eighty-chairs-happen.md b/.changeset/eighty-chairs-happen.md
new file mode 100644
index 00000000..f164852a
--- /dev/null
+++ b/.changeset/eighty-chairs-happen.md
@@ -0,0 +1,5 @@
+---
+"@jspsych/test-utils": minor
+---
+
+Add `simulateTimeline()` testing utility that mimics startTimeline but calls `jsPsych.simulate()` instead.
diff --git a/.changeset/few-teachers-beg.md b/.changeset/few-teachers-beg.md
new file mode 100644
index 00000000..a7005863
--- /dev/null
+++ b/.changeset/few-teachers-beg.md
@@ -0,0 +1,5 @@
+---
+"jspsych": patch
+---
+
+The weights argument for `randomization.sampleWithReplacement()` is now explicitly marked as optional in TypeScript. This has no impact on usage, as the implementation was already treating this argument as optional.
diff --git a/.changeset/fifty-cameras-remember.md b/.changeset/fifty-cameras-remember.md
new file mode 100644
index 00000000..96f60cc8
--- /dev/null
+++ b/.changeset/fifty-cameras-remember.md
@@ -0,0 +1,5 @@
+---
+"@jspsych/plugin-animation": patch
+---
+
+Fixed a bug that caused a crash when `frame_isi` was > 0. This bug was introduced in 1.0.0.
diff --git a/.changeset/hungry-bats-hide.md b/.changeset/hungry-bats-hide.md
new file mode 100644
index 00000000..2639a47d
--- /dev/null
+++ b/.changeset/hungry-bats-hide.md
@@ -0,0 +1,5 @@
+---
+"jspsych": minor
+---
+
+Added `randomInt(lower, upper)`, `sampleBernoulli(p)`, `sampleNormal(mean, std)`, `sampleExponential(rate)`, and `sampleExGaussian(mean, std, rate, positive=false)` to `jsPsych.randomization`.
diff --git a/.changeset/khaki-fishes-pull.md b/.changeset/khaki-fishes-pull.md
new file mode 100644
index 00000000..db9be390
--- /dev/null
+++ b/.changeset/khaki-fishes-pull.md
@@ -0,0 +1,5 @@
+---
+"jspsych": minor
+---
+
+Added the ability to run the experiment in simulation mode using `jsPsych.simulate()`. See the [simulation mode](https://www.jspsych.org/latest/overview/simulation) documentation for information about how to get started.
diff --git a/.changeset/large-lions-leap.md b/.changeset/large-lions-leap.md
new file mode 100644
index 00000000..8287c928
--- /dev/null
+++ b/.changeset/large-lions-leap.md
@@ -0,0 +1,5 @@
+---
+"jspsych": minor
+---
+
+Added methods to assist with simulation (e.g., `pressKey` for dispatching a keyboard event and `clickTarget` for dispatching a click event) to the PluginAPI module.
diff --git a/.changeset/lazy-parents-sort.md b/.changeset/lazy-parents-sort.md
new file mode 100644
index 00000000..30037f0a
--- /dev/null
+++ b/.changeset/lazy-parents-sort.md
@@ -0,0 +1,5 @@
+---
+"@jspsych/plugin-canvas-button-response": patch
+---
+
+Fixed a bug that resulted in `data.response` being `NaN` instead of the index of the button.
diff --git a/.changeset/proud-rings-warn.md b/.changeset/proud-rings-warn.md
new file mode 100644
index 00000000..973bd4a0
--- /dev/null
+++ b/.changeset/proud-rings-warn.md
@@ -0,0 +1,44 @@
+---
+"@jspsych/plugin-animation": minor
+"@jspsych/plugin-audio-button-response": minor
+"@jspsych/plugin-audio-keyboard-response": minor
+"@jspsych/plugin-audio-slider-response": minor
+"@jspsych/plugin-browser-check": minor
+"@jspsych/plugin-call-function": minor
+"@jspsych/plugin-canvas-button-response": minor
+"@jspsych/plugin-canvas-keyboard-response": minor
+"@jspsych/plugin-canvas-slider-response": minor
+"@jspsych/plugin-categorize-animation": minor
+"@jspsych/plugin-categorize-html": minor
+"@jspsych/plugin-categorize-image": minor
+"@jspsych/plugin-cloze": minor
+"@jspsych/plugin-external-html": minor
+"@jspsych/plugin-fullscreen": minor
+"@jspsych/plugin-html-button-response": minor
+"@jspsych/plugin-html-keyboard-response": minor
+"@jspsych/plugin-html-slider-response": minor
+"@jspsych/plugin-iat-html": minor
+"@jspsych/plugin-iat-image": minor
+"@jspsych/plugin-image-button-response": minor
+"@jspsych/plugin-image-keyboard-response": minor
+"@jspsych/plugin-image-slider-response": minor
+"@jspsych/plugin-instructions": minor
+"@jspsych/plugin-maxdiff": minor
+"@jspsych/plugin-preload": minor
+"@jspsych/plugin-reconstruction": minor
+"@jspsych/plugin-same-different-html": minor
+"@jspsych/plugin-same-different-image": minor
+"@jspsych/plugin-serial-reaction-time": minor
+"@jspsych/plugin-serial-reaction-time-mouse": minor
+"@jspsych/plugin-survey-likert": minor
+"@jspsych/plugin-survey-multi-choice": minor
+"@jspsych/plugin-survey-multi-select": minor
+"@jspsych/plugin-survey-text": minor
+"@jspsych/plugin-video-button-response": minor
+"@jspsych/plugin-video-keyboard-response": minor
+"@jspsych/plugin-video-slider-response": minor
+"@jspsych/plugin-visual-search-circle": minor
+"@jspsych/test-utils": minor
+---
+
+Added support for `data-only` and `visual` simulation modes.
diff --git a/.changeset/strong-crabs-nail.md b/.changeset/strong-crabs-nail.md
new file mode 100644
index 00000000..06366807
--- /dev/null
+++ b/.changeset/strong-crabs-nail.md
@@ -0,0 +1,5 @@
+---
+"@jspsych/plugin-same-different-image": patch
+---
+
+Fixed a bug where the blank screen would not show for the correct duration. Instead it would show very briefly, if at all.
diff --git a/.changeset/tidy-tomatoes-cross.md b/.changeset/tidy-tomatoes-cross.md
new file mode 100644
index 00000000..7388dc53
--- /dev/null
+++ b/.changeset/tidy-tomatoes-cross.md
@@ -0,0 +1,5 @@
+---
+"jspsych": minor
+---
+
+Added several functions to the `pluginAPI` module in order to support the new simulation feature.
diff --git a/.changeset/tricky-vans-sell.md b/.changeset/tricky-vans-sell.md
new file mode 100644
index 00000000..f5622731
--- /dev/null
+++ b/.changeset/tricky-vans-sell.md
@@ -0,0 +1,5 @@
+---
+"@jspsych/plugin-categorize-image": patch
+---
+
+Fixed a bug where the default value of `incorrect_text` was not defined.
diff --git a/docs/developers/plugin-development.md b/docs/developers/plugin-development.md
index f8857c6e..1cede3f7 100644
--- a/docs/developers/plugin-development.md
+++ b/docs/developers/plugin-development.md
@@ -216,6 +216,29 @@ trial(display_element, trial){
The data recorded will be that `correct` is `true` and that `rt` is `350`. [Additional data for the trial](../overview/plugins.md#data-collected-by-all-plugins) will also be collected automatically.
+## Simulation mode
+
+Plugins can optionally support [simulation modes](../overview/simulation.md).
+
+To add simulation support, a plugin needs a `simulate()` function that accepts four arguments
+
+`simulate(trial, simulation_mode, simulation_options, load_callback)`
+
+* `trial`: This is the same as the `trial` parameter passed to the plugin's `trial()` method. It contains an object of the parameters for the trial.
+* `simulation_mode`: A string, either `"data-only"` or `"visual"`. This specifies which simulation mode is being requested. Plugins can optionally support `"visual"` mode. If `"visual"` mode is not supported, the plugin should default to `"data-only"` mode when `"visual"` mode is requested.
+* `simulation_options`: An object of simulation-specific options.
+* `load_callback`: A function handle to invoke when the simulation is ready to trigger the `on_load` event for the trial. It is important to invoke this at the correct time during the simulation so that any `on_load` events in the experiment execute as expected.
+
+Typically the flow for supporting simulation mode involves:
+
+1. Generating artificial data that is consistent with the `trial` parameters.
+2. Merging that data with any data specified by the user in `simulation_options`.
+3. Verifying that the final data object is still consistent with the `trial` parameters. For example, checking that RTs are not longer than the duration of the trial.
+4. In `data-only` mode, call `jsPsych.finishTrial()` with the artificial data.
+5. In `visual` mode, invoke the `trial()` method of the plugin and then use the artificial data to trigger the appropriate events. There are a variety of methods in the [Plugin API module](../reference/jspsych-pluginAPI.md) to assist with things like simulating key presses and mouse clicks.
+
+We plan to add a longer guide about simulation development in the future. For now, we recommend browsing the source code of plugins that support simulation mode to see how the flow described above is implemented.
+
## Advice for writing plugins
If you are developing a plugin with the aim of including it in the main jsPsych repository we encourage you to follow the [contribution guidelines](contributing.md#contributing-to-the-codebase).
diff --git a/docs/overview/simulation.md b/docs/overview/simulation.md
new file mode 100644
index 00000000..10bc1046
--- /dev/null
+++ b/docs/overview/simulation.md
@@ -0,0 +1,177 @@
+# Simulation Modes
+*Added in 7.1*
+
+Simulation mode allows you run your experiment automatically and generate artificial data.
+
+## Getting Started
+
+To use simulation mode, replace `jsPsych.run()` with `jsPsych.simulate()`.
+
+```js
+jsPsych.simulate(timeline);
+```
+
+This will run jsPsych in the default `data-only` simulation mode.
+To use the `visual` simulation mode you can specify the second parameter.
+
+```js
+jsPsych.simulate(timeline, "data-only");
+jsPsych.simulate(timeline, "visual");
+```
+
+## What happens in simulation mode
+
+In simulation mode, plugins call their `simulate()` method instead of calling their `trial()` method.
+If a plugin doesn't implement a `simulate()` method, then the trial will run as usual (using the `trial()` method) and any interaction that is needed to advance to the next trial will be required.
+If a plugin doesn't support `visual` mode, then it will run in `data-only` mode.
+
+### `data-only` mode
+
+In `data-only` mode plugins typically generate resonable artificial data given the parameters specified for the trial.
+For example, if the `trial_duration` parameter is set to 2,000 ms, then any response times generated will be capped at this value.
+Generally the default data generated by the plugin randomly selects any available options (e.g., buttons to click) with equal probability.
+Response times are usually generated by sampling from an exponentially-modified Gaussian distribution truncated to positive values using `jsPsych.randomization.sampleExGaussian()`.
+
+In `data-only` mode, the plugin's `trial()` method usually does not run.
+The data are simply calculated based on trial parameters and the `finishTrial()` method is called immediately with the simulated data.
+
+### `visual` mode
+
+In `visual` mode a plugin will typically generate simulated data for the trial and then use that data to mimic the kinds of actions that a participant would do.
+The plugin's `trial()` method is called by the simulation, and you'll see the experiment progress in real time.
+Mouse, keyboard, and touch events are simulated to control the experiment.
+
+In `visual` mode each plugin will generate simulated data in the same manner as `data-only` mode, but this data will instead be used to generate actions in the experiment and the plugin's `trial()` method will ultimately be responsible for generating the data.
+This can create some subtle differences in data between the two modes.
+For example, if the simulated data generates a response time of 500 ms, the `data.rt` value will be exactly `500` in `data-only` mode, but may be `501` or another slightly larger value in `visual` mode.
+This is because the simulated response is triggered at `500` ms and small delays due to JavaScript's event loop might add a few ms to the measure.
+
+## Controlling simulation mode with `simulation_options`
+
+The parameters for simulation mode can be set using the `simulation_options` parameter in both `jsPsych.simulate()` and at the level of individual trials.
+
+### Trial-level options
+
+You can specify simulation options for an individual trial by setting the `simulation_options` parameter.
+
+```js
+const trial = {
+ type: jsPsychHtmlKeyboardResponse,
+ stimulus: '
Hello!
',
+ simulation_options: {
+ data: {
+ rt: 500
+ }
+ }
+}
+```
+
+Currently the three options that are available are `data`, `mode`, and `simulate`.
+
+#### `data`
+
+Setting the `data` option will replace the default data generated by the plugin with whatever data you specify.
+You can specify some or all of the `data` parameters.
+Any parameters you do not specify will be generated by the plugin.
+
+In most cases plugins will try to ensure that the data generated is consistent with the trial parameters.
+For example, if a trial has a `trial_duration` parameter of `2000` but the `simulation_options` specify a `rt` of `2500`, this creates an impossible situation because the trial would have ended before the response at 2,500ms.
+In most cases, the plugin will act as if a response was attempted at `2500`, which will mean that no response is generated for the trial.
+As you might imagine, there are a lot of parameter combinations that can generate peculiar cases where data may be inconsistent with trial parameters.
+We recommend double checking the simulation output, and please [alert us](https://github.com/jspsych/jspsych/issues) if you discover a situation where the simulation produces inconsistent data.
+
+#### `mode`
+
+You can override the simulation mode specified in `jsPsych.simulate()` for any trial. Setting `mode: 'data-only'` will run the trial in data-only mode and setting `mode: 'visual'` will run the trial in visual mode.
+
+#### `simulate`
+
+If you want to turn off simulation mode for a trial, set `simulate: false`.
+
+#### Functions and timeline variables
+
+The `simulation_options` parameter is compatible with both [dynamic parameters](dynamic-parameters.md) and [timeline variables](timeline.md#timeline-variables).
+Dynamic parameters can be especially useful if you want to randomize the data for each run of the simulation.
+For example, you can specify the `rt` as a sample from an ExGaussian distribution.
+
+```js
+const trial = {
+ type: jsPsychHtmlKeyboardResponse,
+ stimulus: '
Hello!
',
+ simulation_options: {
+ data: {
+ rt: ()=>{
+ return jsPsych.randomization.sampleExGaussian(500, 50, 1/100, true)
+ }
+ }
+ }
+}
+```
+
+### Experiment-level options
+
+You can also control the parameters for simulation by passing in an object to the `simulation_options` argument of `jsPsych.simulate()`.
+
+```js
+const simulation_options = {
+ default: {
+ data: {
+ rt: 200
+ }
+ }
+}
+
+jsPsych.simulate(timeline, "visual", simulation_options)
+```
+
+The above example will set the `rt` for any trial that doesn't have its own `simulation_options` specified to `200`.
+This could be useful, for example, to create a very fast preview of the experiment to verify that everything is displaying correctly without having to wait through longer trials.
+
+You can also specify sets of parameters by name using the experiment-level simulation options.
+
+```js
+const simulation_options = {
+ default: {
+ data: {
+ rt: 200
+ }
+ },
+ long_response: {
+ data: {
+ rt: () => {
+ return jsPsych.randomization.sampleExGaussian(5000, 500, 1/500, true)
+ }
+ }
+ }
+}
+
+const trial = {
+ type: jsPsychHtmlKeyboardResponse,
+ stimulus: '
This is gonna take a bit.
',
+ simulation_options: 'long_response'
+}
+timeline.push(trial);
+
+jsPsych.simulate(timeline, "visual", simulation_options)
+```
+
+In the example above, we specified the `simulation_options` for `trial` using a string (`'long_response'`).
+This will look up the corresponding set of options in the experiment-level `simulation_options`.
+
+We had a few use cases in mind with this approach:
+
+1. You could group together trials with similar behavior without needing to specify unique options for each trial.
+2. You could easily swap out different simulation options to test different kinds of behavior. For example, if you want to test that a timeline with a `conditional_function` is working as expected, you could have one set of simulation options where the data will cause the `conditional_function` to evaluate to `true` and another to `false`. By using string-based identifiers, you don't need to change the timeline code at all. You can just change the object being passed to `jsPsych.simulate()`.
+3. In an extreme case of the previous example, every trial on the timeline could have its own unique identifier and you could have multiple sets of simulation options to have very precise control over the data output.
+
+## Current Limitations
+
+Simulation mode is not yet as comprehensively tested as the rest of jsPsych.
+While we are confident that the simulation is accurate enough for many use cases, it's very likely that there are circumstances where the simulated behavior will be inconsistent with what is actually possible in the experiment.
+If you come across any such circumstances, please [let us know](https://github.com/jspsych/jspsych/issues)!
+
+Currently extensions are not supported in simulation mode.
+Some plugins are also not supported.
+This will be noted on their documentation page.
+
+
diff --git a/docs/overview/timeline.md b/docs/overview/timeline.md
index 2e80fca0..33b6cc22 100644
--- a/docs/overview/timeline.md
+++ b/docs/overview/timeline.md
@@ -164,7 +164,7 @@ var face_name_procedure = {
### Using in a function
Continung the example from the previous section, what if we wanted to show the name with the face, combining the two variables together?
-To do this, we can use a [dynamic parameter](dynamic-parameters) (a function) to create an HTML-string that uses both variables in a single parameter.
+To do this, we can use a [dynamic parameter](dynamic-parameters.md) (a function) to create an HTML-string that uses both variables in a single parameter.
The value of the `stimulus` parameter will be a function that returns an HTML string that contains both the image and the name.
```javascript
@@ -229,7 +229,7 @@ The `type` parameter in this object controls the type of sampling that is done.
Valid values for `type` are
* `"with-replacement"`: Sample `size` items from the timeline variables with the possibility of choosing the same item multiple time.
-* `"without-replacement"`: Sample `size` itesm from timeline variables, with each item being selected a maximum of 1 time.
+* `"without-replacement"`: Sample `size` items from timeline variables, with each item being selected a maximum of 1 time.
* `"fixed-repetitons"`: Repeat each item in the timeline variables `size` times, in a random order. Unlike using the `repetitons` parameter, this method allows for consecutive trials to use the same timeline variable set.
* `"alternate-groups"`: Sample in an alternating order based on a declared group membership. Groups are defined by the `groups` parameter. This parameter takes an array of arrays, where each inner array is a group and the items in the inner array are the indices of the timeline variables in the `timeline_variables` array that belong to that group.
* `"custom"`: Write a function that returns a custom order of the timeline variables.
diff --git a/docs/plugins/audio-button-response.md b/docs/plugins/audio-button-response.md
index f6e6bf2d..fde24a66 100644
--- a/docs/plugins/audio-button-response.md
+++ b/docs/plugins/audio-button-response.md
@@ -34,6 +34,12 @@ In addition to the [default data collected by all plugins](../overview/plugins.m
| rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the stimulus first began playing until the subject's response. |
| response | numeric | Indicates which button the subject pressed. The first button in the `choices` array is 0, the second is 1, and so on. |
+## Simulation Mode
+
+In `data-only` simulation mode, the `response_allowed_while_playing` parameter does not currently influence the simulated response time.
+This is because the audio file is not loaded in `data-only` mode and therefore the length is unknown.
+This may change in a future version as we improve the simulation modes.
+
## Examples
???+ example "Displaying question until subject gives a response"
diff --git a/docs/plugins/audio-keyboard-response.md b/docs/plugins/audio-keyboard-response.md
index 468cd62a..8d281841 100644
--- a/docs/plugins/audio-keyboard-response.md
+++ b/docs/plugins/audio-keyboard-response.md
@@ -32,6 +32,12 @@ In addition to the [default data collected by all plugins](../overview/plugins.m
| rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the stimulus first began playing until the subject made a key response. If no key was pressed before the trial ended, then the value will be `null`. |
| stimulus | string | Path to the audio file that played during the trial. |
+## Simulation Mode
+
+In `data-only` simulation mode, the `response_allowed_while_playing` parameter does not currently influence the simulated response time.
+This is because the audio file is not loaded in `data-only` mode and therefore the length is unknown.
+This may change in a future version as we improve the simulation modes.
+
## Examples
???+ example "Trial continues until subject gives a response"
diff --git a/docs/plugins/audio-slider-response.md b/docs/plugins/audio-slider-response.md
index 57349975..dc121896 100644
--- a/docs/plugins/audio-slider-response.md
+++ b/docs/plugins/audio-slider-response.md
@@ -39,6 +39,12 @@ In addition to the [default data collected by all plugins](../overview/plugins.m
| stimulus | string | The path of the audio file that was played. |
| slider_start | numeric | The starting value of the slider. |
+## Simulation Mode
+
+In `data-only` simulation mode, the `response_allowed_while_playing` parameter does not currently influence the simulated response time.
+This is because the audio file is not loaded in `data-only` mode and therefore the length is unknown.
+This may change in a future version as we improve the simulation modes.
+
## Examples
???+ example "A simple rating scale"
diff --git a/docs/plugins/browser-check.md b/docs/plugins/browser-check.md
index 49599cc1..882f0eeb 100644
--- a/docs/plugins/browser-check.md
+++ b/docs/plugins/browser-check.md
@@ -63,6 +63,16 @@ In addition to the [default data collected by all plugins](../overview/plugins.m
Note that all of these values are only recorded when the corresponding key is included in the `features` parameter for the trial.
+## Simulation Mode
+
+In [simulation mode](../overview/simulation.md) the plugin will report the actual features of the browser, with the exception of `vsync_rate`, which is always 60.
+
+In `data-only` mode, if `allow_window_resize` is true and the browser's width and height are below the maximum value then the reported width and height will be equal to `minimum_width` and `minimum_height`, as if the participant resized the browser to meet the specifications.
+
+In `visual` mode, if `allow_window_resize` is true and the browser's width and height are below the maximum value then the experiment will wait for 3 seconds before clicking the resize fail button. During this time, you can adjust the window if you would like to.
+
+As with all simulated plugins, you can override the default (actual) data with fake data using `simulation_options`. This allows you to test your exclusion criteria by simulating other configurations.
+
## Examples
???+ example "Recording all of the available features, no exclusions"
diff --git a/docs/plugins/external-html.md b/docs/plugins/external-html.md
index 8dd6af5b..40f0f294 100644
--- a/docs/plugins/external-html.md
+++ b/docs/plugins/external-html.md
@@ -24,6 +24,10 @@ In addition to the [default data collected by all plugins](../overview/plugins.m
| url | string | The URL of the page. |
| rt | numeric | The response time in milliseconds for the subject to finish the trial. |
+## Simulation Mode
+
+In `visual` simulation mode, the plugin cannot interact with any form elements on the screen other than the `cont_btn` specified in the trial parameters. If your `check_fn` requires other user interaction, for example, clicking a checkbox, then you'll need to disable simulation for the trial and complete the interaction manually.
+
## Examples
### Loading a consent form
diff --git a/docs/plugins/free-sort.md b/docs/plugins/free-sort.md
index b3ae552f..725846d2 100644
--- a/docs/plugins/free-sort.md
+++ b/docs/plugins/free-sort.md
@@ -38,6 +38,10 @@ moves | array | An array containing objects representing all of the moves the pa
final_locations | array | An array containing objects representing the final locations of all the stimuli in the sorting area. Each element in the array represents a stimulus, and has a "src", "x", and "y" value. "src" is the image path, and "x" and "y" are the object location. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions.
rt | numeric | The response time in milliseconds for the participant to finish all sorting.
+## Simulation Mode
+
+This plugin does not yet support [simulation mode](../overview/simulation.md).
+
## Examples
???+ example "Basic example"
diff --git a/docs/plugins/fullscreen.md b/docs/plugins/fullscreen.md
index 352ea3b9..07891c87 100644
--- a/docs/plugins/fullscreen.md
+++ b/docs/plugins/fullscreen.md
@@ -24,6 +24,12 @@ Name | Type | Value
-----|------|------
success | boolean | true if the browser supports fullscreen mode (i.e., is not Safari)
+## Simulation Mode
+
+Web browsers do not allow fullscreen mode to be triggered by a script to avoid malicious usage of fullscreen behavior when the user is not wanting it.
+In `visual` simulation mode, the trial will run normally and the button will get a simulated click, but the display will not change.
+If you want the display to actually enter fullscreen mode during the simulation, you should disable simulation for the fullscreen trial and manually click the button.
+
## Examples
diff --git a/docs/plugins/preload.md b/docs/plugins/preload.md
index 9b89cc9c..8ff11796 100644
--- a/docs/plugins/preload.md
+++ b/docs/plugins/preload.md
@@ -38,6 +38,9 @@ In addition to the [default data collected by all plugins](../overview/plugins.m
| failed_audio | array | One or more audio file paths that produced a loading failure before the trial ended. |
| failed_video | array | One or more video file paths that produced a loading failure before the trial ended. |
+## Simulation Mode
+
+In `visual` simulation mode, the plugin will run the trial as if the experiment was running normally. Specifying `simulation_options.data` will not work in `visual` mode.
## Examples
diff --git a/docs/plugins/resize.md b/docs/plugins/resize.md
index 1d9d61ea..57476d3e 100644
--- a/docs/plugins/resize.md
+++ b/docs/plugins/resize.md
@@ -24,6 +24,10 @@ Name | Type | Value
final_width_px | numeric | Final width of the resizable div container, in pixels.
scale_factor | numeric | Scaling factor that will be applied to the div containing jsPsych content.
+## Simulation Mode
+
+This plugin does not yet support [simulation mode](../overview/simulation.md).
+
## Examples
???+ example "Measuring a credit card and resizing the display to have 150 pixels equal an inch."
diff --git a/docs/plugins/sketchpad.md b/docs/plugins/sketchpad.md
index 429fb925..b6e8f43b 100644
--- a/docs/plugins/sketchpad.md
+++ b/docs/plugins/sketchpad.md
@@ -61,6 +61,9 @@ In addition to the [default data collected by all plugins](../overview/plugins.m
| png | base64 data URL string | If `save_image` is true, then this will contain the base64 encoded data URL for the image, in png format. |
| strokes | array of stroke objects | If `save_strokes` is true, then this will contain an array of stroke objects. Objects have an `action` property that is either `"start"`, `"move"`, or `"end"`. If `action` is `"start"` or `"move"` it will have an `x` and `y` property that report the coordinates of the action relative to the upper-left corner of the canvas. If `action` is `"start"` then the object will also have a `t` and `color` property, specifying the time of the action relative to the onset of the trial (ms) and the color of the stroke. If `action` is `"end"` then it will only have a `t` property. |
+## Simulation Mode
+
+This plugin does not yet support [simulation mode](../overview/simulation.md).
## Examples
diff --git a/docs/plugins/survey-html-form.md b/docs/plugins/survey-html-form.md
index a35f4343..44fe112c 100644
--- a/docs/plugins/survey-html-form.md
+++ b/docs/plugins/survey-html-form.md
@@ -24,6 +24,10 @@ Name | Type | Value
response | object | An object containing the response for each input. The object will have a separate key (variable) for the response to each input, with each variable being named after its corresponding input element. Each response is a string containing whatever the subject answered for this particular input. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. |
rt | numeric | The response time in milliseconds for the subject to make a response. |
+## Simulation Mode
+
+This plugin does not yet support [simulation mode](../overview/simulation.md).
+
## Examples
???+ example "Fill in the blanks"
diff --git a/docs/plugins/survey-text.md b/docs/plugins/survey-text.md
index 9abe5065..0d6639cf 100644
--- a/docs/plugins/survey-text.md
+++ b/docs/plugins/survey-text.md
@@ -8,7 +8,7 @@ In addition to the [parameters available in all plugins](../overview/plugins.md#
Parameter | Type | Default Value | Description
----------|------|---------------|------------
-questions | array | *undefined* | An array of objects, each object represents a question that appears on the screen. Each object contains a prompt, value, required, rows, and columns parameter that will be applied to the question. See examples below for further clarification. `prompt`: Type string, default value of *undefined*. The string is the prompt for the subject to respond to. Each question gets its own response field. `value`: Type string, default value of `""`. The string will create placeholder text in the text field. `required`: Boolean; if `true` then the user must enter a response to submit. `rows`: Type integer, default value of 1. The number of rows for the response text box. `columns`: Type integer, default value of 40. The number of columns for the response text box. `name`: Name of the question. Used for storing data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.
+questions | array | *undefined* | An array of objects, each object represents a question that appears on the screen. Each object contains a prompt, placeholder, required, rows, and columns parameter that will be applied to the question. See examples below for further clarification. `prompt`: Type string, default value of *undefined*. The string is the prompt for the subject to respond to. Each question gets its own response field. `placeholder`: Type string, default value of `""`. The string will create placeholder text in the text field. `required`: Boolean; if `true` then the user must enter a response to submit. `rows`: Type integer, default value of 1. The number of rows for the response text box. `columns`: Type integer, default value of 40. The number of columns for the response text box. `name`: Name of the question. Used for storing data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions.
randomize_question_order | boolean | `false` | If true, the display order of `questions` is randomly determined at the start of the trial. In the data object, `Q0` will still refer to the first question in the array, regardless of where it was presented visually.
preamble | string | empty string | HTML formatted string to display at the top of the page above all the questions.
button_label | string | 'Continue' | The text that appears on the button to finish the trial.
diff --git a/docs/plugins/video-button-response.md b/docs/plugins/video-button-response.md
index 037e6067..566274e8 100644
--- a/docs/plugins/video-button-response.md
+++ b/docs/plugins/video-button-response.md
@@ -39,6 +39,12 @@ response | numeric | Indicates which button the subject pressed. The first butto
rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the stimulus first appears on the screen until the subject's response.
stimulus | array | The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions.
+## Simulation Mode
+
+In `data-only` simulation mode, the `response_allowed_while_playing` parameter does not currently influence the simulated response time.
+This is because the audio file is not loaded in `data-only` mode and therefore the length is unknown.
+This may change in a future version as we improve the simulation modes.
+
## Example
???+ example "Responses disabled until the video is complete"
diff --git a/docs/plugins/video-keyboard-response.md b/docs/plugins/video-keyboard-response.md
index be166e55..033fc33b 100644
--- a/docs/plugins/video-keyboard-response.md
+++ b/docs/plugins/video-keyboard-response.md
@@ -35,6 +35,12 @@ In addition to the [default data collected by all plugins](../overview/plugins.m
| rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the stimulus first appears on the screen until the subject's response. |
stimulus | array | The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. |
+## Simulation Mode
+
+In `data-only` simulation mode, the `response_allowed_while_playing` parameter does not currently influence the simulated response time.
+This is because the audio file is not loaded in `data-only` mode and therefore the length is unknown.
+This may change in a future version as we improve the simulation modes.
+
## Examples
???+ example "Show a video and advance to next trial automatically"
diff --git a/docs/plugins/video-slider-response.md b/docs/plugins/video-slider-response.md
index a68edf43..39631f47 100644
--- a/docs/plugins/video-slider-response.md
+++ b/docs/plugins/video-slider-response.md
@@ -45,6 +45,12 @@ stimulus | array | The `stimulus` array. This will be encoded as a JSON string w
slider_start | numeric | The starting value of the slider.
start | numeric | The start time of the video clip.
+## Simulation Mode
+
+In `data-only` simulation mode, the `response_allowed_while_playing` parameter does not currently influence the simulated response time.
+This is because the audio file is not loaded in `data-only` mode and therefore the length is unknown.
+This may change in a future version as we improve the simulation modes.
+
## Example
???+ example "Rate enjoyment of a video clip"
diff --git a/docs/plugins/virtual-chinrest.md b/docs/plugins/virtual-chinrest.md
index 3a0d4c83..aae315ac 100644
--- a/docs/plugins/virtual-chinrest.md
+++ b/docs/plugins/virtual-chinrest.md
@@ -61,6 +61,10 @@ _Note: The deg data are **only** returned if viewing distance is estimated with
| win_height_deg | numeric | The interior height of the window in degrees. |
| view_dist_mm | numeric | Estimated distance to the screen in millimeters. |
+## Simulation Mode
+
+This plugin does not yet support [simulation mode](../overview/simulation.md).
+
## Example
???+ example "Measure distance to screen and pixel ratio; no resizing"
diff --git a/docs/plugins/webgazer-calibrate.md b/docs/plugins/webgazer-calibrate.md
index 6c0f41fd..13038472 100644
--- a/docs/plugins/webgazer-calibrate.md
+++ b/docs/plugins/webgazer-calibrate.md
@@ -25,6 +25,10 @@ Name | Type | Value
No data currently added by this plugin. Use the [webgazer-validate](jspsych-webgazer-validate) plugin to measure the precision and accuracy of calibration.
+## Simulation Mode
+
+This plugin does not yet support [simulation mode](../overview/simulation.md).
+
## Example
Because the eye tracking plugins need to be used in conjunction with each other, please see the [example on the eye tracking overview page](../overview/eye-tracking.md#example) for an integrated example.
diff --git a/docs/plugins/webgazer-init-camera.md b/docs/plugins/webgazer-init-camera.md
index 458e369b..921af4cb 100644
--- a/docs/plugins/webgazer-init-camera.md
+++ b/docs/plugins/webgazer-init-camera.md
@@ -19,6 +19,10 @@ Name | Type | Value
-----|------|------
load_time | numeric | The time it took for webgazer to initialize. This can be a long time in some situations, so this value is recorded for troubleshooting when participants are reporting difficulty.
+## Simulation Mode
+
+This plugin does not yet support [simulation mode](../overview/simulation.md).
+
## Example
Because the eye tracking plugins need to be used in conjunction with each other, please see the [example on the eye tracking overview page](../overview/eye-tracking.md#example) for an integrated example.
diff --git a/docs/plugins/webgazer-validate.md b/docs/plugins/webgazer-validate.md
index b84dc590..23692ef8 100644
--- a/docs/plugins/webgazer-validate.md
+++ b/docs/plugins/webgazer-validate.md
@@ -29,6 +29,10 @@ average_offset | array | The average `x` and `y` distance from each validation p
samples_per_sec | numeric | The average number of samples per second. Calculated by finding samples per second for each point and then averaging these estimates together.
validation_points | array | The list of validation points, in the order that they appeared.
+## Simulation Mode
+
+This plugin does not yet support [simulation mode](../overview/simulation.md).
+
## Example
Because the eye tracking plugins need to be used in conjunction with each other, please see the [example on the eye tracking overview page](../overview/eye-tracking.md#example) for an integrated example.
diff --git a/docs/reference/jspsych-pluginAPI.md b/docs/reference/jspsych-pluginAPI.md
index 2af82698..32e56fc7 100644
--- a/docs/reference/jspsych-pluginAPI.md
+++ b/docs/reference/jspsych-pluginAPI.md
@@ -1,28 +1,28 @@
# jsPsych.pluginAPI
-The pluginAPI module contains functions that are useful when developing new plugins.
+The pluginAPI module contains functions that are useful when developing plugins. All of the functions are accessible through the `pluginAPI` object. In this documentation we've divided them up based on different kinds of functionality.
----
+## Keyboard Input
-## jsPsych.pluginAPI.cancelAllKeyboardResponses
+### cancelAllKeyboardResponses
```javascript
jsPsych.pluginAPI.cancelAllKeyboardResponses()
```
-### Parameters
+#### Parameters
None.
-### Return value
+#### Return value
Returns nothing.
-### Description
+#### Description
Cancels all currently active keyboard listeners created by `jsPsych.pluginAPI.getKeyboardResponse`.
-### Example
+#### Example
```javascript
jsPsych.pluginAPI.cancelAllKeyboardResponses();
@@ -30,27 +30,27 @@ jsPsych.pluginAPI.cancelAllKeyboardResponses();
---
-## jsPsych.pluginAPI.cancelKeyboardResponse
+### cancelKeyboardResponse
```javascript
jsPsych.pluginAPI.cancelKeyboardResponse(listener_id)
```
-### Parameters
+#### Parameters
Parameter | Type | Description
----------|------|------------
listener_id | object | The listener_id object generated by the call to `jsPsych.pluginAPI.getKeyboardResponse`.
-### Return value
+#### Return value
Returns nothing.
-### Description
+#### Description
Cancels a specific keyboard listener created by `jsPsych.pluginAPI.getKeyboardResponse`.
-### Example
+#### Example
```javascript
// create a persistent keyboard listener
@@ -68,44 +68,24 @@ jsPsych.pluginAPI.cancelKeyboardResponse(listener_id);
---
-## jsPsych.pluginAPI.clearAllTimeouts
-
-```javascript
-jsPsych.pluginAPI.clearAllTimeouts()
-```
-
-### Parameters
-
-None.
-
-### Return value
-
-Returns nothing.
-
-### Description
-
-Clears any pending timeouts that were set using jsPsych.pluginAPI.setTimeout().
-
----
-
-## jsPsych.pluginAPI.compareKeys
+### compareKeys
```javascript
jsPsych.pluginAPI.compareKeys(key1, key2)
```
-### Parameters
+#### Parameters
Parameter | Type | Description
----------|------|------------
key1 | string or numeric | The representation of a key, either string or keycode
key2 | string or numeric | The representation of a key, either string or keycode
-### Return value
+#### Return value
Returns true if keycodes or strings refer to the same key, regardless of type. Returns false if the keycodes or strings do not match.
-### Description
+#### Description
Compares two keys to see if they are the same, ignoring differences in representational type, and using the appropriate case sensitivity based on the experiment's settings.
@@ -113,9 +93,9 @@ If `case_sensitive_responses` is set to `false` in `initJsPsych` (the default),
We recommend using this function to compare keys in all plugin and experiment code, rather than using something like `if (response == 'j')...`. This is because the response key returned by the `jsPsych.pluginAPI.getKeyboardResponse` function will be converted to lowercase when `case_sensitive_responses` is `false`, and it will match the exact key press representation when `case_sensitive_responses` is `true`. Using this `compareKeys` function will ensure that your key comparisons work appropriately based on the experiment's `case_sensitive_responses` setting, and that you do not need to remember to check key responses against different case versions of the comparison key (e.g. `if (response == 'ArrowLeft' || response == 'arrowleft')...`).
-### Examples
+#### Examples
-#### Basic examples
+##### Basic examples
```javascript
jsPsych.pluginAPI.compareKeys('a', 'A');
@@ -132,7 +112,7 @@ jsPsych.pluginAPI.compareKeys('space', 31);
// returns false
```
-#### Comparing a key response and key parameter value in plugins
+##### Comparing a key response and key parameter value in plugins
```javascript
// this is the callback_function passed to jsPsych.pluginAPI.getKeyboardResponse
@@ -143,7 +123,7 @@ var after_response = function(info) {
}
```
-#### Scoring a key response in experiment code
+##### Scoring a key response in experiment code
```javascript
var trial = {
@@ -164,32 +144,105 @@ var trial = {
```
---
+### getKeyboardResponse
-## jsPsych.pluginAPI.getAudioBuffer
+```javascript
+jsPsych.pluginAPI.getKeyboardResponse(parameters)
+```
+
+#### Parameters
+
+The method accepts an object of parameter values (see example below). The valid keys for this object are listed in the table below.
+
+Parameter | Type | Description
+----------|------|------------
+callback_function | function | The function to execute whenever a valid keyboard response is generated.
+valid_responses | array | An array of key codes or character strings representing valid responses. Responses not on the list will be ignored. An empty array indicates that all responses are acceptable.
+rt_method | string | Indicates which method of recording time to use. The `'performance'` method uses calls to `performance.now()`, which is the standard way of measuring timing in jsPsych. It is [supported by up-to-date versions of all the major browsers](http://caniuse.com/#search=performance). The `audio` method is used in conjuction with an `audio_context` (set as an additional parameter). This uses the clock time of the `audio_context` when audio stimuli are being played.
+audio_context | AudioContext object | The AudioContext of the audio file that is being played.
+audio_context_start_time | numeric | The scheduled time of the sound file in the AudioContext. This will be used as the start time.
+allow_held_key | boolean | If `true`, then responses will be registered from keys that are being held down. If `false`, then a held key can only register a response the first time that `getKeyboardResponse` is called for that key. For example, if a participant holds down the `A` key before the experiment starts, then the first time `getKeyboardResponse` is called, the `A` will register as a key press. However, any future calls to `getKeyboardResponse` will not register the `A` until the participant releases the key and presses it again.
+persist | boolean | If false, then the keyboard listener will only trigger the first time a valid key is pressed. If true, then it will trigger every time a valid key is pressed until it is explicitly cancelled by `jsPsych.pluginAPI.cancelKeyboardResponse` or `jsPsych.pluginAPI.cancelAllKeyboardResponses`.
+
+#### Return value
+
+Return an object that uniquely identifies the keyboard listener. This object can be passed to `jsPsych.pluginAPI.cancelKeyboardResponse` to cancel the keyboard listener.
+
+#### Description
+
+Gets a keyboard response from the subject, recording the response time from when the function is first called until a valid response is generated.
+
+The keyboard event listener will be bound to the `display_element` declared in `initJsPsych()` (or the `` element if no `display_element` is specified). This allows jsPsych experiments to be embedded in websites with other content without disrupting the functionality of other UI elements.
+
+A valid response triggers the `callback_function` specified in the parameters. A single argument is passed to the callback function. The argument contains an object with the properties `key` and `rt`. `key` contains the string representation of the response key, and `rt` contains the response time.
+
+This function uses the `.key` value of the keyboard event, which is _case sensitive_. When `case_sensitive_responses` is `false` in `initJsPsych` (the default), this function will convert both the `valid_responses` strings and the response key to lowercase before comparing them, and it will pass the lowercase version of the response key to the `callback_function`. For example, if `valid_responses` is `['a']`, then both 'A' and 'a' will be considered valid key presses, and 'a' will be returned as the response key. When `case_sensitive_responses` is `true` in `initJsPsych`, this function will not convert the case when comparing the `valid_responses` and response key, and it will not convert the case of the response key that is passed to the `callback_function`. For example, if `valid_responses` is `['a']`, then 'a' will be the only valid key press, and 'A' (i.e. 'a' with CapsLock on or Shift held down) will not be accepted. Also, if `valid_responses` includes multiple letter case options (e.g. `"ALL_KEYS"`), then you may need to check the response key against both letter cases when scoring etc., e.g. `if (response == 'ArrowLeft' || response =='arrowleft') ...`.
+
+#### Examples
+
+##### Get a single response from any key
+
+```javascript
+
+var after_response = function(info){
+ alert('You pressed key '+info.key+' after '+info.rt+'ms');
+}
+
+jsPsych.pluginAPI.getKeyboardResponse({
+ callback_function:after_response,
+ valid_responses: "ALL_KEYS",
+ rt_method: 'performance',
+ persist: false
+});
+```
+
+##### Get a responses from a key until the letter q is pressed
+
+```javascript
+
+var after_response = function(info){
+ alert('You pressed key '+info.key+' after '+info.rt+'ms');
+
+ if(jsPsych.pluginAPI.compareKeys(info.key,'q')){ /
+ jsPsych.pluginAPI.cancelKeyboardResponse(listener);
+ }
+}
+
+var listener = jsPsych.pluginAPI.getKeyboardResponse({
+ callback_function:after_response,
+ valid_responses: "ALL_KEYS",
+ rt_method: 'performance',
+ persist: true
+});
+```
+
+## Media
+
+### getAudioBuffer
```javascript
jsPsych.pluginAPI.getAudioBuffer(filepath)
```
-### Parameters
+#### Parameters
Parameter | Type | Description
----------|------|------------
filepath | string | The path to the audio file that was preloaded.
-### Return value
+#### Return value
Returns a Promise that resolves when the audio file loads. Success handler's parameter will be the audio buffer. If the experiment is running using the WebAudio API it will be an AudioBuffer object. Otherwise, it will be an HTML5 Audio object. The failure handler's parameter is the error generated by `preloadAudio`.
-### Description
+#### Description
Gets an AudioBuffer that can be played with the WebAudio API or an Audio object that can be played with HTML5 Audio.
It is strongly recommended that you preload audio files before calling this method. This method will load the files if they are not preloaded, but this may result in delays during the experiment as audio is downloaded.
-### Examples
+#### Examples
-#### HTML 5 Audio
+##### HTML 5 Audio
```javascript
jsPsych.pluginAPI.getAudioBuffer('my-sound.mp3')
@@ -201,7 +254,7 @@ jsPsych.pluginAPI.getAudioBuffer('my-sound.mp3')
})
```
-#### WebAudio API
+##### WebAudio API
```javascript
var context = jsPsych.pluginAPI.audioContext();
@@ -222,23 +275,23 @@ See the `audio-keyboard-response` plugin for an example in a fuller context.
---
-## jsPsych.pluginAPI.getAutoPreloadList
+### getAutoPreloadList
```javascript
jsPsych.pluginAPI.getAutoPreloadList(timeline)
```
-### Parameters
+#### Parameters
Parameter | Type | Description
----------|------|------------
timeline | array | An array containing the trial object(s) from which a list of media files should be automatically generated. This array can contain the entire experiment timeline, or any individual parts of a larger timeline, such as specific timeline nodes and trial objects.
-### Return value
+#### Return value
An object with properties for each media type: `images`, `audio`, and `video`. Each property contains an array of the unique files of that media type that were automatically extracted from the timeline. If no files are found in the timeline for a particular media type, then the array will be empty for that type.
-### Description
+#### Description
This method is used to automatically generate lists of unique image, audio, and video files from a timeline. It is used by the `preload` plugin to generate a list of to-be-preloaded files based on the trials passed to the `trials` parameter and/or the experiment timeline passed to `jsPsych.run` (when `auto_preload` is true). It can be used in custom plugins and experiment code to generate a list of audio/image/video files, based on a timeline.
@@ -247,7 +300,7 @@ When a file path is returned to the trial parameter from a function (including t
In these cases, the file should be preloaded manually.
See [Media Preloading](../overview/media-preloading.md) for more information.
-### Example
+#### Example
```javascript
var audio_trial = {
@@ -272,87 +325,13 @@ jsPsych.pluginAPI.getAutoPreloadList(timeline);
---
-## jsPsych.pluginAPI.getKeyboardResponse
-
-```javascript
-jsPsych.pluginAPI.getKeyboardResponse(parameters)
-```
-
-### Parameters
-
-The method accepts an object of parameter values (see example below). The valid keys for this object are listed in the table below.
-
-Parameter | Type | Description
-----------|------|------------
-callback_function | function | The function to execute whenever a valid keyboard response is generated.
-valid_responses | array | An array of key codes or character strings representing valid responses. Responses not on the list will be ignored. An empty array indicates that all responses are acceptable.
-rt_method | string | Indicates which method of recording time to use. The `'performance'` method uses calls to `performance.now()`, which is the standard way of measuring timing in jsPsych. It is [supported by up-to-date versions of all the major browsers](http://caniuse.com/#search=performance). The `audio` method is used in conjuction with an `audio_context` (set as an additional parameter). This uses the clock time of the `audio_context` when audio stimuli are being played.
-audio_context | AudioContext object | The AudioContext of the audio file that is being played.
-audio_context_start_time | numeric | The scheduled time of the sound file in the AudioContext. This will be used as the start time.
-allow_held_key | boolean | If `true`, then responses will be registered from keys that are being held down. If `false`, then a held key can only register a response the first time that `getKeyboardResponse` is called for that key. For example, if a participant holds down the `A` key before the experiment starts, then the first time `getKeyboardResponse` is called, the `A` will register as a key press. However, any future calls to `getKeyboardResponse` will not register the `A` until the participant releases the key and presses it again.
-persist | boolean | If false, then the keyboard listener will only trigger the first time a valid key is pressed. If true, then it will trigger every time a valid key is pressed until it is explicitly cancelled by `jsPsych.pluginAPI.cancelKeyboardResponse` or `jsPsych.pluginAPI.cancelAllKeyboardResponses`.
-
-### Return value
-
-Return an object that uniquely identifies the keyboard listener. This object can be passed to `jsPsych.pluginAPI.cancelKeyboardResponse` to cancel the keyboard listener.
-
-### Description
-
-Gets a keyboard response from the subject, recording the response time from when the function is first called until a valid response is generated.
-
-The keyboard event listener will be bound to the `display_element` declared in `initJsPsych()` (or the `` element if no `display_element` is specified). This allows jsPsych experiments to be embedded in websites with other content without disrupting the functionality of other UI elements.
-
-A valid response triggers the `callback_function` specified in the parameters. A single argument is passed to the callback function. The argument contains an object with the properties `key` and `rt`. `key` contains the string representation of the response key, and `rt` contains the response time.
-
-This function uses the `.key` value of the keyboard event, which is _case sensitive_. When `case_sensitive_responses` is `false` in `initJsPsych` (the default), this function will convert both the `valid_responses` strings and the response key to lowercase before comparing them, and it will pass the lowercase version of the response key to the `callback_function`. For example, if `valid_responses` is `['a']`, then both 'A' and 'a' will be considered valid key presses, and 'a' will be returned as the response key. When `case_sensitive_responses` is `true` in `initJsPsych`, this function will not convert the case when comparing the `valid_responses` and response key, and it will not convert the case of the response key that is passed to the `callback_function`. For example, if `valid_responses` is `['a']`, then 'a' will be the only valid key press, and 'A' (i.e. 'a' with CapsLock on or Shift held down) will not be accepted. Also, if `valid_responses` includes multiple letter case options (e.g. `"ALL_KEYS"`), then you may need to check the response key against both letter cases when scoring etc., e.g. `if (response == 'ArrowLeft' || response =='arrowleft') ...`.
-
-### Examples
-
-#### Get a single response from any key
-
-```javascript
-
-var after_response = function(info){
- alert('You pressed key '+info.key+' after '+info.rt+'ms');
-}
-
-jsPsych.pluginAPI.getKeyboardResponse({
- callback_function:after_response,
- valid_responses: "ALL_KEYS",
- rt_method: 'performance',
- persist: false
-});
-```
-
-#### Get a responses from a key until the letter q is pressed
-
-```javascript
-
-var after_response = function(info){
- alert('You pressed key '+info.key+' after '+info.rt+'ms');
-
- if(jsPsych.pluginAPI.compareKeys(info.key,'q')){ /
- jsPsych.pluginAPI.cancelKeyboardResponse(listener);
- }
-}
-
-var listener = jsPsych.pluginAPI.getKeyboardResponse({
- callback_function:after_response,
- valid_responses: "ALL_KEYS",
- rt_method: 'performance',
- persist: true
-});
-```
-
----
-
-## jsPsych.pluginAPI.preloadAudio
+### preloadAudio
```javascript
jsPsych.pluginAPI.preloadAudio(files, callback_complete, callback_load, callback_error)
```
-### Parameters
+#### Parameters
Parameter | Type | Description
----------|------|------------
@@ -361,11 +340,11 @@ callback_complete | function | A function to execute when all the files have bee
callback_load | function | A function to execute after a single file has been loaded. A single parameter is passed to this function which is the file source (string) that has loaded.
callback_error | function | A function to execute after a single file has produced an error. A single parameter is passed to this function which is the file source (string) that produced the error.
-### Return value
+#### Return value
Returns nothing.
-### Description
+#### Description
This function is used to preload audio files. It is used by the `preload` plugin, and could be called directly to preload audio files in custom plugins or experiment. See [Media Preloading](../overview/media-preloading.md) for more information.
@@ -373,9 +352,9 @@ It is possible to run this function without specifying a callback function. Howe
The `callback_load` and `callback_error` functions are called after each file has either loaded or produced an error, so these functions can also be used to monitor loading progress. See example below.
-### Examples
+#### Examples
-#### Basic use
+##### Basic use
```javascript
var sounds = ['file1.mp3', 'file2.mp3', 'file3.mp3'];
@@ -391,7 +370,7 @@ function startExperiment(){
}
```
-#### Show progress of loading
+##### Show progress of loading
```javascript
var sounds = ['file1.mp3', 'file2.mp3', 'file3.mp3'];
@@ -415,13 +394,13 @@ function startExperiment(){
---
-## jsPsych.pluginAPI.preloadImages
+### preloadImages
```javascript
jsPsych.pluginAPI.preloadImages(images, callback_complete, callback_load, callback_error)
```
-### Parameters
+#### Parameters
Parameter | Type | Description
----------|------|------------
@@ -430,11 +409,11 @@ callback_complete | function | A function to execute when all the images have be
callback_load | function | A function to execute after a single file has been loaded. A single parameter is passed to this function which is the file source (string) that has loaded.
callback_error | function | A function to execute after a single file has produced an error. A single parameter is passed to this function which is the file source (string) that produced the error.
-### Return value
+#### Return value
Returns nothing.
-### Description
+#### Description
This function is used to preload image files. It is used by the `preload` plugin, and could be called directly to preload image files in custom plugins or experiment code. See [Media Preloading](../overview/media-preloading.md) for more information.
@@ -442,9 +421,9 @@ It is possible to run this function without specifying a callback function. Howe
The `callback_load` and `callback_error` functions are called after each file has either loaded or produced an error, so these functions can also be used to monitor loading progress. See example below.
-### Examples
+#### Examples
-#### Basic use
+##### Basic use
```javascript
var images = ['img/file1.png', 'img/file2.png', 'img/file3.png'];
@@ -460,7 +439,7 @@ function startExperiment(){
}
```
-#### Show progress of loading
+##### Show progress of loading
```javascript
var images = ['img/file1.png', 'img/file2.png', 'img/file3.png'];
@@ -484,13 +463,13 @@ function startExperiment(){
---
-## jsPsych.pluginAPI.preloadVideo
+### preloadVideo
```javascript
jsPsych.pluginAPI.preloadVideo(video, callback_complete, callback_load, callback_error)
```
-### Parameters
+#### Parameters
Parameter | Type | Description
----------|------|------------
@@ -499,11 +478,11 @@ callback_complete | function | A function to execute when all the videos have be
callback_load | function | A function to execute after a single file has been loaded. A single parameter is passed to this function which is the file source (string) that has loaded.
callback_error | function | A function to execute after a single file has produced an error. A single parameter is passed to this function which is the file source (string) that produced the error.
-### Return value
+#### Return value
Returns nothing.
-### Description
+#### Description
This function is used to preload video files. It is used by the `preload` plugin, and could be called directly to preload video files in custom plugins or experiment code. See [Media Preloading](../overview/media-preloading.md) for more information.
@@ -511,9 +490,9 @@ It is possible to run this function without specifying a callback function. Howe
The `callback_load` and `callback_error` functions are called after each file has either loaded or produced an error, so these functions can also be used to monitor loading progress. See example below.
-### Examples
+#### Examples
-#### Basic use
+##### Basic use
```javascript
var videos = ['vid/file1.mp4', 'vid/file2.mp4', 'vid/file3.mp4'];
@@ -529,7 +508,7 @@ function startExperiment(){
}
```
-#### Show progress of loading
+##### Show progress of loading
```javascript
var videos = ['vid/file1.mp4', 'vid/file2.mp4', 'vid/file3.mp4'];
@@ -553,28 +532,272 @@ function startExperiment(){
---
-## jsPsych.pluginAPI.setTimeout
+
+## Simulation
+
+### clickTarget
+
+```javascript
+jsPsych.pluginAPI.clickTarget(target, delay)
+```
+
+#### Parameters
+
+Parameter | Type | Description
+----------|------|------------
+target | Element | The DOM element to simulate clicking on.
+delay | number | Time to wait in milliseconds. The click will be executed after the delay.
+
+#### Return value
+
+None
+
+#### Description
+
+Simulates clicking on a DOM element by dispatching three MouseEvents on the `target`: `'mousedown'`, then `'mouseup'`, then `'click'`. If `delay` is positive, then the events are scheduled to execute after the delay via `setTimeout`.
+
+#### Example
+
+```javascript
+const target = document.querySelector('.jspsych-btn');
+
+jsPsych.pluginAPI.clickTarget(target, 500);
+```
+
+---
+
+### ensureSimulationDataConsistency
+
+```javascript
+jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data)
+```
+
+#### Parameters
+
+Parameter | Type | Description
+----------|------|------------
+trial | object | Parameters for the trial, e.g., those passed to the plugin's `trial()` method.
+data | object | An object containing data for the trial.
+
+#### Return value
+
+None. The `data` object is modified in place by this method.
+
+#### Description
+
+Performs some basic consistency checks on the `data` based on the parameters specified in `trial`. For example, if `trial.choices` is `"NO_KEYS"` but `data.response` is a key string then `data.response` and `data.rt` are set to `null`.
+
+#### Example
+
+```javascript
+jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
+```
+
+---
+
+### fillTextInput
+
+```javascript
+jsPsych.pluginAPI.fillTextInput(target, text, delay)
+```
+
+#### Parameters
+
+Parameter | Type | Description
+----------|------|------------
+target | HTMLInputElement | The input element to fill in with text.
+text | string | The text to input.
+delay | number | Time to wait in milliseconds. The text will be inserted after the delay.
+
+#### Return value
+
+None
+
+#### Description
+
+Sets the value of the `target` HTMLInputElement to equal `text`.
+
+#### Example
+
+```javascript
+const target = document.querySelector('input[type="text"]');
+
+jsPsych.pluginAPI.fillTextInput(target, "hello!", 500);
+```
+
+---
+
+### getValidKey
+
+```javascript
+jsPsych.pluginAPI.getValidKey(choices)
+```
+
+#### Parameters
+
+Parameter | Type | Description
+----------|------|------------
+choices | "NO_KEYS" or "ALL_KEYS" or array of strings | Representation of the valid keys allowed for a keyboard response used by the `getKeyboardResponse` method.
+
+#### Return value
+
+A valid key given the `choices` parameter, chosen at random from the possible keys.
+
+#### Description
+
+Picks a random key given a set of options. Currently it only picks letters and numbers when `choices` is `"ALL_KEYS"`.
+
+#### Example
+
+```javascript
+const random_key = jsPsych.pluginAPI.getValidKey(trial.choices);
+```
+
+---
+
+### keyDown
+
+```javascript
+jsPsych.pluginAPI.keyDown(key)
+```
+
+#### Parameters
+
+Parameter | Type | Description
+----------|------|------------
+key | string | The `.key` property of the corresponding key on the keyboard.
+
+#### Return value
+
+None.
+
+#### Description
+
+Dispatches a `'keydown'` event for the specified `key`.
+
+#### Example
+
+```javascript
+jsPsych.pluginAPI.keyDown('a');
+```
+
+---
+
+### keyUp
+
+```javascript
+jsPsych.pluginAPI.keyUp(key)
+```
+
+#### Parameters
+
+Parameter | Type | Description
+----------|------|------------
+key | string | The `.key` property of the corresponding key on the keyboard.
+
+#### Return value
+
+None.
+
+#### Description
+
+Dispatches a `'keyup'` event for the specified `key`.
+
+#### Example
+
+```javascript
+jsPsych.pluginAPI.keyUp('a');
+```
+
+---
+
+### mergeSimulationData
+
+```javascript
+jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options)
+```
+
+#### Parameters
+
+Parameter | Type | Description
+----------|------|------------
+default_data | object | An object containing data values for the simulated trial.
+simulation_options | object | The `simulation_options` specified for the trial.
+
+#### Return value
+
+An object of data.
+
+#### Description
+
+This method merges the `default_data` with any data specified in `simulation_options.data`, giving priority to values specified in `simulation_options.data`. It returns the merged data.
+
+#### Example
+
+```javascript
+const default_data = {
+ rt: 500,
+ response: 'a'
+}
+
+const simulation_options = {
+ data: {
+ rt: 200
+ }
+}
+
+const data = jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
+
+data.rt === 200; // true
+```
+
+---
+
+
+## Timeouts
+
+### clearAllTimeouts
+
+```javascript
+jsPsych.pluginAPI.clearAllTimeouts()
+```
+
+#### Parameters
+
+None.
+
+#### Return value
+
+Returns nothing.
+
+#### Description
+
+Clears any pending timeouts that were set using jsPsych.pluginAPI.setTimeout().
+
+---
+
+### setTimeout
```javascript
jsPsych.pluginAPI.setTimeout(callback, delay)
```
-### Parameters
+#### Parameters
Parameter | Type | Description
----------|------|------------
callback | function | A function to execute after waiting for delay.
delay | integer | Time to wait in milliseconds.
-### Return value
+#### Return value
Returns the ID of the setTimeout handle.
-### Description
+#### Description
This is simply a call to the standard setTimeout function in JavaScript with the added benefit of registering the setTimeout call in a central list. This is useful for scenarios where some other event (the trial ending, aborting the experiment) should stop the execution of queued timeouts.
-### Example
+#### Example
```javascript
// print the time
@@ -585,3 +808,4 @@ jsPsych.pluginAPI.setTimeout(function(){
console.log(Date.now())
}, 1000);
```
+
diff --git a/docs/reference/jspsych-randomization.md b/docs/reference/jspsych-randomization.md
index cfbd6dd6..8a18234e 100644
--- a/docs/reference/jspsych-randomization.md
+++ b/docs/reference/jspsych-randomization.md
@@ -127,6 +127,37 @@ console.log(jsPsych.randomization.randomID(8));
---
+## jsPsych.randomization.randomInt
+
+```javascript
+jsPsych.randomization.randomInt(lower, upper)
+```
+
+### Parameters
+
+| Parameter | Type | Description |
+| --------- | ------- | --------------------------------------- |
+| lower | integer | The smallest value it is possible to generate |
+| upper | integer | The largest value it is possible to generate |
+
+### Return value
+
+An integer
+
+### Description
+
+Generates a random integer from `lower` to `upper`
+
+### Example
+
+```javascript
+console.log(jsPsych.randomization.randomInt(2,4));
+// outputs: 2 or 3 or 4.
+```
+
+---
+
+
## jsPsych.randomization.repeat
```javascript
@@ -231,6 +262,136 @@ output: shuffledArray = {
---
+## jsPsych.randomization.sampleBernoulli
+
+```javascript
+jsPsych.randomization.sampleBernoulli(p)
+```
+
+### Parameters
+
+| Parameter | Type | Description |
+| ---------- | ------- | ---------------------------------------- |
+| p | number | Probability of sampling 1 |
+
+
+### Return value
+
+Returns 0 with probability `1-p` and 1 with probability `p`.
+
+### Description
+
+Generates a random sample from a Bernoulli distribution.
+
+### Examples
+
+#### Sample a value
+
+```javascript
+if(jsPsych.randomization.sampleBernoulli(0.8)){
+ // this happens 80% of the time
+} else {
+ // this happens 20% of the time
+}
+```
+
+---
+
+## jsPsych.randomization.sampleExGaussian
+
+```javascript
+jsPsych.randomization.sampleExGaussian(mean, standard_deviation, rate, positive=false)
+```
+
+### Parameters
+
+| Parameter | Type | Description |
+| ---------- | ------- | ---------------------------------------- |
+| mean | number | Mean of the normal distribution component of the exGaussian |
+| standard_deviation | number | Standard deviation of the normal distribution component of the exGaussian |
+| rate | number | Rate of the exponential distribution component of the exGaussian |
+| positive | bool | If `true` sample will be constrained to > 0.
+
+### Return value
+
+A random sample from the distribution
+
+### Description
+
+Generates a random sample from an exponentially modified Gaussian distribution.
+
+### Examples
+
+#### Sample a value
+
+```javascript
+var rand_sample_exg = jsPsych.randomization.sampleExGaussian(500, 100, 0.01);
+```
+
+---
+
+## jsPsych.randomization.sampleExponential
+
+```javascript
+jsPsych.randomization.sampleExponential(rate)
+```
+
+### Parameters
+
+| Parameter | Type | Description |
+| ---------- | ------- | ---------------------------------------- |
+| rate | number | Rate of the exponential distribution |
+
+### Return value
+
+A random sample from the distribution
+
+### Description
+
+Generates a random sample from an exponential distribution.
+
+### Examples
+
+#### Sample a value
+
+```javascript
+var rand_sample_exg = jsPsych.randomization.sampleExponential(0.01);
+```
+
+---
+
+## jsPsych.randomization.sampleNormal
+
+```javascript
+jsPsych.randomization.sampleNormal(mean, standard_deviation)
+```
+
+### Parameters
+
+| Parameter | Type | Description |
+| ---------- | ------- | ---------------------------------------- |
+| mean | number | Mean of the normal distribution |
+| standard_deviation | number | Standard deviation of the normal distribution |
+
+
+### Return value
+
+A random sample from the distribution
+
+### Description
+
+Generates a random sample from a normal distribution.
+
+### Examples
+
+#### Sample a value
+
+```javascript
+var rand_sample_exg = jsPsych.randomization.sampleNormal(500, 250);
+```
+
+---
+
## jsPsych.randomization.sampleWithReplacement
```javascript
diff --git a/examples/jspsych-survey-multi-choice.html b/examples/jspsych-survey-multi-choice.html
index a0d219ba..0eae924c 100644
--- a/examples/jspsych-survey-multi-choice.html
+++ b/examples/jspsych-survey-multi-choice.html
@@ -24,6 +24,7 @@
{prompt: "I like vegetables", name: 'Vegetables', options: page_1_options, required:true},
{prompt: "I like fruit", name: 'Fruit', options: page_2_options, required: false}
],
+ randomize_question_order: true
};
var multi_choice_block_horizontal = {
diff --git a/examples/simulation-data-only-mode.html b/examples/simulation-data-only-mode.html
new file mode 100644
index 00000000..55975d8c
--- /dev/null
+++ b/examples/simulation-data-only-mode.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/simulation-visual-mode.html b/examples/simulation-visual-mode.html
new file mode 100644
index 00000000..f2950f4e
--- /dev/null
+++ b/examples/simulation-visual-mode.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mkdocs.yml b/mkdocs.yml
index c7a44962..6ad791f6 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -49,6 +49,7 @@ nav:
- 'Dynamic Parameters': 'overview/dynamic-parameters.md'
- 'Controlling Visual Appearance': 'overview/style.md'
- 'Data Storage, Aggregation, and Manipulation': 'overview/data.md'
+ - 'Simulation Modes': 'overview/simulation.md'
- 'Running Experiments': 'overview/running-experiments.md'
- 'Experiment Settings': 'overview/experiment-options.md'
- 'Events': 'overview/events.md'
diff --git a/package-lock.json b/package-lock.json
index efc0e613..cfcab522 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4188,6 +4188,11 @@
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
},
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
+ },
"node_modules/browserslist": {
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.2.tgz",
@@ -4898,6 +4903,24 @@
"node": ">=10"
}
},
+ "node_modules/cross-fetch": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
+ "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
+ "dev": true,
+ "dependencies": {
+ "node-fetch": "2.6.1"
+ }
+ },
+ "node_modules/cross-fetch/node_modules/node-fetch": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+ "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
+ "dev": true,
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -5228,6 +5251,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/diff-sequences": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz",
@@ -5356,6 +5387,55 @@
"is-arrayish": "^0.2.1"
}
},
+ "node_modules/es-abstract": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
+ "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/es5-ext": {
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
@@ -6081,6 +6161,39 @@
"node": ">= 0.10"
}
},
+ "node_modules/flat": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz",
+ "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==",
+ "dependencies": {
+ "is-buffer": "~2.0.3"
+ },
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat/node_modules/is-buffer": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+ "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/flush-write-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@@ -6316,6 +6429,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-value": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
@@ -6733,6 +6861,14 @@
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"dev": true
},
+ "node_modules/growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+ "engines": {
+ "node": ">=4.x"
+ }
+ },
"node_modules/gulp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
@@ -6897,6 +7033,14 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/has-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -6916,6 +7060,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -6979,6 +7137,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
"node_modules/homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -7285,6 +7451,19 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
+ "node_modules/internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
@@ -7329,6 +7508,17 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -7340,6 +7530,21 @@
"node": ">=8"
}
},
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
@@ -7357,6 +7562,17 @@
"node": ">=6"
}
},
+ "node_modules/is-callable": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
+ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-ci": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
@@ -7391,6 +7607,20 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-descriptor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
@@ -7483,6 +7713,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-negative-zero": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
+ "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -7491,6 +7732,20 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-number-object": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
+ "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
@@ -7530,6 +7785,21 @@
"@types/estree": "*"
}
},
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-regexp": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
@@ -7550,6 +7820,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
+ "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -7561,6 +7839,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-subdir": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz",
@@ -7573,6 +7865,20 @@
"node": ">=4"
}
},
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@@ -7614,6 +7920,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-weakref": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz",
+ "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==",
+ "dependencies": {
+ "call-bind": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@@ -8391,6 +8708,16 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
+ "node_modules/jest-fetch-mock": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
+ "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
+ "dev": true,
+ "dependencies": {
+ "cross-fetch": "^3.0.4",
+ "promise-polyfill": "^8.1.3"
+ }
+ },
"node_modules/jest-get-type": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz",
@@ -10609,6 +10936,368 @@
"node": ">=10"
}
},
+ "node_modules/mocha": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz",
+ "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==",
+ "dependencies": {
+ "ansi-colors": "3.2.3",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.3.0",
+ "debug": "3.2.6",
+ "diff": "3.5.0",
+ "escape-string-regexp": "1.0.5",
+ "find-up": "3.0.0",
+ "glob": "7.1.3",
+ "growl": "1.10.5",
+ "he": "1.2.0",
+ "js-yaml": "3.13.1",
+ "log-symbols": "3.0.0",
+ "minimatch": "3.0.4",
+ "mkdirp": "0.5.5",
+ "ms": "2.1.1",
+ "node-environment-flags": "1.0.6",
+ "object.assign": "4.1.0",
+ "strip-json-comments": "2.0.1",
+ "supports-color": "6.0.0",
+ "which": "1.3.1",
+ "wide-align": "1.1.3",
+ "yargs": "13.3.2",
+ "yargs-parser": "13.1.2",
+ "yargs-unparser": "1.6.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mochajs"
+ }
+ },
+ "node_modules/mocha/node_modules/ansi-colors": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
+ "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/chokidar": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
+ "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
+ "dependencies": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.2.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.1.1"
+ }
+ },
+ "node_modules/mocha/node_modules/cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dependencies": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ }
+ },
+ "node_modules/mocha/node_modules/debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/mocha/node_modules/emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+ },
+ "node_modules/mocha/node_modules/find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dependencies": {
+ "locate-path": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/fsevents": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+ "deprecated": "\"Please update to latest v2.3 or v2.2\"",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/mocha/node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/mocha/node_modules/glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mocha/node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mocha/node_modules/js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/mocha/node_modules/locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dependencies": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/log-symbols": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+ "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+ "dependencies": {
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mocha/node_modules/mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dependencies": {
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mocha/node_modules/ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ },
+ "node_modules/mocha/node_modules/object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dependencies": {
+ "define-properties": "^1.1.2",
+ "function-bind": "^1.1.1",
+ "has-symbols": "^1.0.0",
+ "object-keys": "^1.0.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mocha/node_modules/p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dependencies": {
+ "p-limit": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mocha/node_modules/readdirp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
+ "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
+ "dependencies": {
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/mocha/node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+ },
+ "node_modules/mocha/node_modules/string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dependencies": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dependencies": {
+ "ansi-regex": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/supports-color": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
+ "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/mocha/node_modules/which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
+ },
+ "node_modules/mocha/node_modules/wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dependencies": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mocha/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
+ },
+ "node_modules/mocha/node_modules/yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dependencies": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ }
+ },
+ "node_modules/mocha/node_modules/yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -10663,6 +11352,15 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
+ "node_modules/node-environment-flags": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
+ "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
+ "dependencies": {
+ "object.getownpropertydescriptors": "^2.0.3",
+ "semver": "^5.7.0"
+ }
+ },
"node_modules/node-fetch": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
@@ -10857,6 +11555,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
+ "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -10907,6 +11613,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object.getownpropertydescriptors": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz",
+ "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/object.map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
@@ -11477,6 +12199,12 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
+ "node_modules/promise-polyfill": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.1.tgz",
+ "integrity": "sha512-3p9zj0cEHbp7NVUxEYUWjQlffXqnXaZIMPkAO7HhFh8u5636xLRDHOUo2vpWSK0T2mqm6fKLXYn1KP6PAZ2gKg==",
+ "dev": true
+ },
"node_modules/prompts": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz",
@@ -11556,6 +12284,14 @@
"node": ">=8"
}
},
+ "node_modules/random-words": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/random-words/-/random-words-1.1.1.tgz",
+ "integrity": "sha512-Rdk5EoQePyt9Tz3RjeMELi2BSaCI+jDiOkBr4U+3fyBRiiW3qqEuaegGAUMOZ4yGWlQscFQGqQpdic3mAbNkrw==",
+ "dependencies": {
+ "mocha": "^7.1.1"
+ }
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -12254,6 +12990,19 @@
"node": ">=8"
}
},
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
@@ -13029,6 +13778,30 @@
"node": ">=4"
}
},
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/stringify-object": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
@@ -13091,6 +13864,14 @@
"node": ">=8"
}
},
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -13721,6 +14502,20 @@
"node": ">=4.2.0"
}
},
+ "node_modules/unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/unc-path-regex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
@@ -14123,6 +14918,21 @@
"node": ">= 8"
}
},
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/which-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
@@ -14341,6 +15151,186 @@
"node": ">=6"
}
},
+ "node_modules/yargs-unparser": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
+ "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
+ "dependencies": {
+ "flat": "^4.1.0",
+ "lodash": "^4.17.15",
+ "yargs": "^13.3.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dependencies": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+ },
+ "node_modules/yargs-unparser/node_modules/find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dependencies": {
+ "locate-path": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dependencies": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dependencies": {
+ "p-limit": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+ },
+ "node_modules/yargs-unparser/node_modules/string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dependencies": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dependencies": {
+ "ansi-regex": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
+ },
+ "node_modules/yargs-unparser/node_modules/wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dependencies": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
+ },
+ "node_modules/yargs-unparser/node_modules/yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dependencies": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ },
"node_modules/yargs/node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@@ -14585,7 +15575,8 @@
"version": "7.0.0",
"license": "MIT",
"dependencies": {
- "auto-bind": "^4.0.0"
+ "auto-bind": "^4.0.0",
+ "random-words": "^1.1.1"
},
"devDependencies": {
"@jspsych/config": "^1.0.0",
@@ -14757,7 +15748,8 @@
"license": "MIT",
"devDependencies": {
"@jspsych/config": "^1.0.0",
- "@jspsych/test-utils": "^1.0.0"
+ "@jspsych/test-utils": "^1.0.0",
+ "jest-fetch-mock": "^3.0.3"
},
"peerDependencies": {
"jspsych": ">=7.0.0"
@@ -17073,7 +18065,8 @@
"version": "file:packages/plugin-external-html",
"requires": {
"@jspsych/config": "^1.0.0",
- "@jspsych/test-utils": "^1.0.0"
+ "@jspsych/test-utils": "^1.0.0",
+ "jest-fetch-mock": "^3.0.3"
}
},
"@jspsych/plugin-free-sort": {
@@ -18607,6 +19600,11 @@
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
},
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
+ },
"browserslist": {
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.2.tgz",
@@ -19155,6 +20153,23 @@
"yaml": "^1.10.0"
}
},
+ "cross-fetch": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
+ "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
+ "dev": true,
+ "requires": {
+ "node-fetch": "2.6.1"
+ },
+ "dependencies": {
+ "node-fetch": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+ "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
+ "dev": true
+ }
+ }
+ },
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -19415,6 +20430,11 @@
"integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
"dev": true
},
+ "diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
+ },
"diff-sequences": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz",
@@ -19520,6 +20540,43 @@
"is-arrayish": "^0.2.1"
}
},
+ "es-abstract": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
+ "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
"es5-ext": {
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
@@ -20105,6 +21162,21 @@
"resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
"integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q=="
},
+ "flat": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz",
+ "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==",
+ "requires": {
+ "is-buffer": "~2.0.3"
+ },
+ "dependencies": {
+ "is-buffer": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+ "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
+ }
+ }
+ },
"flush-write-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@@ -20289,6 +21361,15 @@
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="
},
+ "get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
"get-value": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
@@ -20627,6 +21708,11 @@
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"dev": true
},
+ "growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="
+ },
"gulp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
@@ -20750,6 +21836,11 @@
"function-bind": "^1.1.1"
}
},
+ "has-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA=="
+ },
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -20760,6 +21851,14 @@
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
+ "has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -20812,6 +21911,11 @@
}
}
},
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
+ },
"homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -21058,6 +22162,16 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
+ "internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "requires": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ }
+ },
"interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
@@ -21090,6 +22204,14 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
+ "is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "requires": {
+ "has-bigints": "^1.0.1"
+ }
+ },
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -21098,6 +22220,15 @@
"binary-extensions": "^2.0.0"
}
},
+ "is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
@@ -21112,6 +22243,11 @@
"builtin-modules": "^3.0.0"
}
},
+ "is-callable": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
+ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w=="
+ },
"is-ci": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
@@ -21137,6 +22273,14 @@
"kind-of": "^6.0.0"
}
},
+ "is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
"is-descriptor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
@@ -21204,11 +22348,24 @@
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
"integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI="
},
+ "is-negative-zero": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
+ "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w=="
+ },
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
+ "is-number-object": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
+ "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
@@ -21239,6 +22396,15 @@
"@types/estree": "*"
}
},
+ "is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
"is-regexp": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
@@ -21253,11 +22419,24 @@
"is-unc-path": "^1.0.0"
}
},
+ "is-shared-array-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
+ "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA=="
+ },
"is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
},
+ "is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
"is-subdir": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz",
@@ -21267,6 +22446,14 @@
"better-path-resolve": "1.0.0"
}
},
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@@ -21296,6 +22483,14 @@
"resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz",
"integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao="
},
+ "is-weakref": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz",
+ "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==",
+ "requires": {
+ "call-bind": "^1.0.0"
+ }
+ },
"is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@@ -21857,6 +23052,16 @@
"jest-util": "^27.2.4"
}
},
+ "jest-fetch-mock": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
+ "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
+ "dev": true,
+ "requires": {
+ "cross-fetch": "^3.0.4",
+ "promise-polyfill": "^8.1.3"
+ }
+ },
"jest-get-type": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz",
@@ -22794,7 +23999,8 @@
"requires": {
"@jspsych/config": "^1.0.0",
"@jspsych/test-utils": "^1.0.0",
- "auto-bind": "^4.0.0"
+ "auto-bind": "^4.0.0",
+ "random-words": "^1.1.1"
}
},
"just-debounce": {
@@ -23523,6 +24729,285 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
+ "mocha": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz",
+ "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==",
+ "requires": {
+ "ansi-colors": "3.2.3",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.3.0",
+ "debug": "3.2.6",
+ "diff": "3.5.0",
+ "escape-string-regexp": "1.0.5",
+ "find-up": "3.0.0",
+ "glob": "7.1.3",
+ "growl": "1.10.5",
+ "he": "1.2.0",
+ "js-yaml": "3.13.1",
+ "log-symbols": "3.0.0",
+ "minimatch": "3.0.4",
+ "mkdirp": "0.5.5",
+ "ms": "2.1.1",
+ "node-environment-flags": "1.0.6",
+ "object.assign": "4.1.0",
+ "strip-json-comments": "2.0.1",
+ "supports-color": "6.0.0",
+ "which": "1.3.1",
+ "wide-align": "1.1.3",
+ "yargs": "13.3.2",
+ "yargs-parser": "13.1.2",
+ "yargs-unparser": "1.6.0"
+ },
+ "dependencies": {
+ "ansi-colors": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
+ "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw=="
+ },
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
+ },
+ "chokidar": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
+ "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
+ "requires": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.1.1",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.2.0"
+ }
+ },
+ "cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "requires": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ }
+ },
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "fsevents": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+ "optional": true
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "log-symbols": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+ "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+ "requires": {
+ "chalk": "^2.4.2"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ },
+ "object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "requires": {
+ "define-properties": "^1.1.2",
+ "function-bind": "^1.1.1",
+ "has-symbols": "^1.0.0",
+ "object-keys": "^1.0.11"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
+ },
+ "readdirp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
+ "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
+ "requires": {
+ "picomatch": "^2.0.4"
+ }
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ },
+ "supports-color": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
+ "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
+ },
+ "wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ }
+ },
+ "y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
+ },
+ "yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "requires": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -23571,6 +25056,15 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
+ "node-environment-flags": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
+ "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
+ "requires": {
+ "object.getownpropertydescriptors": "^2.0.3",
+ "semver": "^5.7.0"
+ }
+ },
"node-fetch": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
@@ -23721,6 +25215,11 @@
}
}
},
+ "object-inspect": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
+ "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg=="
+ },
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -23756,6 +25255,16 @@
"isobject": "^3.0.0"
}
},
+ "object.getownpropertydescriptors": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz",
+ "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
"object.map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
@@ -24163,6 +25672,12 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
+ "promise-polyfill": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.1.tgz",
+ "integrity": "sha512-3p9zj0cEHbp7NVUxEYUWjQlffXqnXaZIMPkAO7HhFh8u5636xLRDHOUo2vpWSK0T2mqm6fKLXYn1KP6PAZ2gKg==",
+ "dev": true
+ },
"prompts": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz",
@@ -24219,6 +25734,14 @@
"integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
"dev": true
},
+ "random-words": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/random-words/-/random-words-1.1.1.tgz",
+ "integrity": "sha512-Rdk5EoQePyt9Tz3RjeMELi2BSaCI+jDiOkBr4U+3fyBRiiW3qqEuaegGAUMOZ4yGWlQscFQGqQpdic3mAbNkrw==",
+ "requires": {
+ "mocha": "^7.1.1"
+ }
+ },
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -24765,6 +26288,16 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
"signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
@@ -25400,6 +26933,24 @@
}
}
},
+ "string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
"stringify-object": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
@@ -25444,6 +26995,11 @@
"min-indent": "^1.0.0"
}
},
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+ },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -25922,6 +27478,17 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="
},
+ "unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
"unc-path-regex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
@@ -26254,6 +27821,18 @@
"isexe": "^2.0.0"
}
},
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ },
"which-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
@@ -26521,6 +28100,152 @@
}
}
},
+ "yargs-unparser": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
+ "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
+ "requires": {
+ "flat": "^4.1.0",
+ "lodash": "^4.17.15",
+ "yargs": "^13.3.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
+ },
+ "cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "requires": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ }
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
+ },
+ "wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ }
+ },
+ "y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
+ },
+ "yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "requires": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
"yazl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
diff --git a/packages/jspsych/package.json b/packages/jspsych/package.json
index 6e64fa27..7e7418ee 100644
--- a/packages/jspsych/package.json
+++ b/packages/jspsych/package.json
@@ -39,7 +39,8 @@
},
"homepage": "https://www.jspsych.org",
"dependencies": {
- "auto-bind": "^4.0.0"
+ "auto-bind": "^4.0.0",
+ "random-words": "^1.1.1"
},
"devDependencies": {
"@jspsych/config": "^1.0.0",
diff --git a/packages/jspsych/src/JsPsych.ts b/packages/jspsych/src/JsPsych.ts
index 2c1af491..58f61376 100644
--- a/packages/jspsych/src/JsPsych.ts
+++ b/packages/jspsych/src/JsPsych.ts
@@ -72,6 +72,16 @@ export class JsPsych {
private finished: Promise;
private resolveFinishedPromise: () => void;
+ /**
+ * is the experiment running in `simulate()` mode
+ */
+ private simulation_mode: "data-only" | "visual" = null;
+
+ /**
+ * simulation options passed in via `simulate()`
+ */
+ private simulation_options;
+
// storing a single webaudio context to prevent problems with multiple inits
// of jsPsych
webaudio_context: AudioContext = null;
@@ -177,6 +187,16 @@ export class JsPsych {
await this.finished;
}
+ async simulate(
+ timeline: any[],
+ simulation_mode: "data-only" | "visual",
+ simulation_options = {}
+ ) {
+ this.simulation_mode = simulation_mode;
+ this.simulation_options = simulation_options;
+ await this.run(timeline);
+ }
+
getProgress() {
return {
total_trials: typeof this.timeline === "undefined" ? undefined : this.timeline.length(),
@@ -586,11 +606,60 @@ export class JsPsych {
}
};
- const trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
+ let trial_complete;
+ if (!this.simulation_mode) {
+ trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
+ }
+ if (this.simulation_mode) {
+ // check if the trial supports simulation
+ if (trial.type.simulate) {
+ let trial_sim_opts;
+ if (!trial.simulation_options) {
+ trial_sim_opts = this.simulation_options.default;
+ }
+ if (trial.simulation_options) {
+ if (typeof trial.simulation_options == "string") {
+ if (this.simulation_options[trial.simulation_options]) {
+ trial_sim_opts = this.simulation_options[trial.simulation_options];
+ } else if (this.simulation_options.default) {
+ console.log(
+ `No matching simulation options found for "${trial.simulation_options}". Using "default" options.`
+ );
+ trial_sim_opts = this.simulation_options.default;
+ } else {
+ console.log(
+ `No matching simulation options found for "${trial.simulation_options}" and no "default" options provided. Using the default values provided by the plugin.`
+ );
+ trial_sim_opts = {};
+ }
+ } else {
+ trial_sim_opts = trial.simulation_options;
+ }
+ }
+ trial_sim_opts = this.utils.deepCopy(trial_sim_opts);
+ trial_sim_opts = this.replaceFunctionsWithValues(trial_sim_opts, null);
+
+ if (trial_sim_opts?.simulate === false) {
+ trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
+ } else {
+ trial_complete = trial.type.simulate(
+ trial,
+ trial_sim_opts?.mode || this.simulation_mode,
+ trial_sim_opts,
+ load_callback
+ );
+ }
+ } else {
+ // trial doesn't have a simulate method, so just run as usual
+ trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
+ }
+ }
+
// see if trial_complete is a Promise by looking for .then() function
const is_promise = trial_complete && typeof trial_complete.then == "function";
- if (!is_promise) {
+ // in simulation mode we let the simulate function call the load_callback always.
+ if (!is_promise && !this.simulation_mode) {
load_callback();
}
diff --git a/packages/jspsych/src/modules/plugin-api/SimulationAPI.ts b/packages/jspsych/src/modules/plugin-api/SimulationAPI.ts
new file mode 100644
index 00000000..94288c16
--- /dev/null
+++ b/packages/jspsych/src/modules/plugin-api/SimulationAPI.ts
@@ -0,0 +1,181 @@
+export class SimulationAPI {
+ dispatchEvent(event: Event) {
+ document.body.dispatchEvent(event);
+ }
+
+ /**
+ * Dispatches a `keydown` event for the specified key
+ * @param key Character code (`.key` property) for the key to press.
+ */
+ keyDown(key: string) {
+ this.dispatchEvent(new KeyboardEvent("keydown", { key }));
+ }
+
+ /**
+ * Dispatches a `keyup` event for the specified key
+ * @param key Character code (`.key` property) for the key to press.
+ */
+ keyUp(key: string) {
+ this.dispatchEvent(new KeyboardEvent("keyup", { key }));
+ }
+
+ /**
+ * Dispatches a `keydown` and `keyup` event in sequence to simulate pressing a key.
+ * @param key Character code (`.key` property) for the key to press.
+ * @param delay Length of time to wait (ms) before executing action
+ */
+ pressKey(key: string, delay = 0) {
+ if (delay > 0) {
+ setTimeout(() => {
+ this.keyDown(key);
+ this.keyUp(key);
+ }, delay);
+ } else {
+ this.keyDown(key);
+ this.keyUp(key);
+ }
+ }
+
+ /**
+ * Dispatches `mousedown`, `mouseup`, and `click` events on the target element
+ * @param target The element to click
+ * @param delay Length of time to wait (ms) before executing action
+ */
+ clickTarget(target: Element, delay = 0) {
+ if (delay > 0) {
+ setTimeout(() => {
+ target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
+ target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ }, delay);
+ } else {
+ target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
+ target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ }
+ }
+
+ /**
+ * Sets the value of a target text input
+ * @param target A text input element to fill in
+ * @param text Text to input
+ * @param delay Length of time to wait (ms) before executing action
+ */
+ fillTextInput(target: HTMLInputElement, text: string, delay = 0) {
+ if (delay > 0) {
+ setTimeout(() => {
+ target.value = text;
+ }, delay);
+ } else {
+ target.value = text;
+ }
+ }
+
+ /**
+ * Picks a valid key from `choices`, taking into account jsPsych-specific
+ * identifiers like "NO_KEYS" and "ALL_KEYS".
+ * @param choices Which keys are valid.
+ * @returns A key selected at random from the valid keys.
+ */
+ getValidKey(choices: "NO_KEYS" | "ALL_KEYS" | Array | Array>) {
+ const possible_keys = [
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f",
+ "g",
+ "h",
+ "i",
+ "j",
+ "k",
+ "l",
+ "m",
+ "n",
+ "o",
+ "p",
+ "q",
+ "r",
+ "s",
+ "t",
+ "u",
+ "v",
+ "w",
+ "x",
+ "y",
+ "z",
+ "0",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ " ",
+ ];
+
+ let key;
+ if (choices == "NO_KEYS") {
+ key = null;
+ } else if (choices == "ALL_KEYS") {
+ key = possible_keys[Math.floor(Math.random() * possible_keys.length)];
+ } else {
+ const flat_choices = choices.flat();
+ key = flat_choices[Math.floor(Math.random() * flat_choices.length)];
+ }
+
+ return key;
+ }
+
+ mergeSimulationData(default_data, simulation_options) {
+ // override any data with data from simulation object
+ return {
+ ...default_data,
+ ...simulation_options?.data,
+ };
+ }
+
+ ensureSimulationDataConsistency(trial, data) {
+ // All RTs must be rounded
+ if (data.rt) {
+ data.rt = Math.round(data.rt);
+ }
+
+ // If a trial_duration and rt exist, make sure that the RT is not longer than the trial.
+ if (trial.trial_duration && data.rt && data.rt > trial.trial_duration) {
+ data.rt = null;
+ if (data.response) {
+ data.response = null;
+ }
+ if (data.correct) {
+ data.correct = false;
+ }
+ }
+
+ // If trial.choices is NO_KEYS make sure that response and RT are null
+ if (trial.choices && trial.choices == "NO_KEYS") {
+ if (data.rt) {
+ data.rt = null;
+ }
+ if (data.response) {
+ data.response = null;
+ }
+ }
+
+ // If response is not allowed before stimulus display complete, ensure RT
+ // is longer than display time.
+ if (trial.allow_response_before_complete) {
+ if (trial.sequence_reps && trial.frame_time) {
+ const min_time = trial.sequence_reps * trial.frame_time * trial.stimuli.length;
+ if (data.rt < min_time) {
+ data.rt = null;
+ data.response = null;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/jspsych/src/modules/plugin-api/index.ts b/packages/jspsych/src/modules/plugin-api/index.ts
index 511e54f4..5da34514 100644
--- a/packages/jspsych/src/modules/plugin-api/index.ts
+++ b/packages/jspsych/src/modules/plugin-api/index.ts
@@ -4,6 +4,7 @@ import { JsPsych } from "../../JsPsych";
import { HardwareAPI } from "./HardwareAPI";
import { KeyboardListenerAPI } from "./KeyboardListenerAPI";
import { MediaAPI } from "./MediaAPI";
+import { SimulationAPI } from "./SimulationAPI";
import { TimeoutAPI } from "./TimeoutAPI";
export function createJointPluginAPIObject(jsPsych: JsPsych) {
@@ -19,8 +20,9 @@ export function createJointPluginAPIObject(jsPsych: JsPsych) {
new TimeoutAPI(),
new MediaAPI(settings.use_webaudio, jsPsych.webaudio_context),
new HardwareAPI(),
+ new SimulationAPI(),
].map((object) => autoBind(object))
- ) as KeyboardListenerAPI & TimeoutAPI & MediaAPI & HardwareAPI;
+ ) as KeyboardListenerAPI & TimeoutAPI & MediaAPI & HardwareAPI & SimulationAPI;
}
export type PluginAPI = ReturnType;
diff --git a/packages/jspsych/src/modules/randomization.ts b/packages/jspsych/src/modules/randomization.ts
index 300be13d..c19c1604 100644
--- a/packages/jspsych/src/modules/randomization.ts
+++ b/packages/jspsych/src/modules/randomization.ts
@@ -1,3 +1,5 @@
+import rw from "random-words";
+
export function repeat(array, repetitions, unpack = false) {
const arr_isArray = Array.isArray(array);
const rep_isArray = Array.isArray(repetitions);
@@ -173,7 +175,7 @@ export function sampleWithoutReplacement(arr, size) {
return shuffle(arr).slice(0, size);
}
-export function sampleWithReplacement(arr, size, weights) {
+export function sampleWithReplacement(arr, size, weights?) {
if (!Array.isArray(arr)) {
console.error("First argument to sampleWithReplacement() must be an array");
}
@@ -240,6 +242,75 @@ export function randomID(length = 32) {
return result;
}
+/**
+ * Generate a random integer from `lower` to `upper`, inclusive of both end points.
+ * @param lower The lowest value it is possible to generate
+ * @param upper The highest value it is possible to generate
+ * @returns A random integer
+ */
+export function randomInt(lower: number, upper: number) {
+ if (upper < lower) {
+ throw new Error("Upper boundary must be less than or equal to lower boundary");
+ }
+ return lower + Math.floor(Math.random() * (upper - lower + 1));
+}
+
+/**
+ * Generates a random sample from a Bernoulli distribution.
+ * @param p The probability of sampling 1.
+ * @returns 0, with probability 1-p, or 1, with probability p.
+ */
+export function sampleBernoulli(p: number) {
+ return Math.random() <= p ? 1 : 0;
+}
+
+export function sampleNormal(mean: number, standard_deviation: number) {
+ return randn_bm() * standard_deviation + mean;
+}
+
+export function sampleExponential(rate: number) {
+ return -Math.log(Math.random()) / rate;
+}
+
+export function sampleExGaussian(
+ mean: number,
+ standard_deviation: number,
+ rate: number,
+ positive = false
+) {
+ let s = sampleNormal(mean, standard_deviation) + sampleExponential(rate);
+ if (positive) {
+ while (s <= 0) {
+ s = sampleNormal(mean, standard_deviation) + sampleExponential(rate);
+ }
+ }
+ return s;
+}
+
+/**
+ * Generate one or more random words.
+ *
+ * This is a wrapper function for the {@link https://www.npmjs.com/package/random-words `random-words` npm package}.
+ *
+ * @param opts An object with optional properties `min`, `max`, `exactly`,
+ * `join`, `maxLength`, `wordsPerString`, `separator`, and `formatter`.
+ *
+ * @returns An array of words or a single string, depending on parameter choices.
+ */
+export function randomWords(opts) {
+ return rw(opts);
+}
+
+// Box-Muller transformation for a random sample from normal distribution with mean = 0, std = 1
+// https://stackoverflow.com/a/36481059/3726673
+function randn_bm() {
+ var u = 0,
+ v = 0;
+ while (u === 0) u = Math.random(); //Converting [0,1) to (0,1)
+ while (v === 0) v = Math.random();
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
+}
+
function unpackArray(array) {
const out = {};
diff --git a/packages/jspsych/tests/core/simulation-mode.test.ts b/packages/jspsych/tests/core/simulation-mode.test.ts
new file mode 100644
index 00000000..ea0a5b42
--- /dev/null
+++ b/packages/jspsych/tests/core/simulation-mode.test.ts
@@ -0,0 +1,371 @@
+import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
+import { clickTarget, pressKey, simulateTimeline } from "@jspsych/test-utils";
+
+import { JsPsych, JsPsychPlugin, ParameterType, TrialType, initJsPsych } from "../../src";
+
+jest.useFakeTimers();
+
+describe("data simulation mode", () => {
+ test("jsPsych.simulate() runs as drop-in replacement for jsPsych.run()", async () => {
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ },
+ ];
+
+ const { expectFinished, getData } = await simulateTimeline(timeline);
+
+ await expectFinished();
+
+ expect(getData().values().length).toBe(1);
+ });
+
+ test("Can set simulation_options at the trial level", async () => {
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: {
+ data: {
+ rt: 100,
+ },
+ },
+ },
+ ];
+
+ const { expectFinished, getData } = await simulateTimeline(timeline);
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBe(100);
+ });
+
+ test("Simulation options can be functions that eval at runtime", async () => {
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: {
+ data: {
+ rt: () => {
+ return 100;
+ },
+ },
+ },
+ },
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: {
+ data: {
+ rt: () => {
+ return 200;
+ },
+ },
+ },
+ },
+ ];
+
+ const { expectFinished, getData } = await simulateTimeline(timeline);
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBe(100);
+ expect(getData().values()[1].rt).toBe(200);
+ });
+
+ test("Simulation options can be set using default key, only applies if no trial opts set", async () => {
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ },
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: {
+ data: {
+ rt: 200,
+ },
+ },
+ },
+ ];
+
+ const simulation_options = {
+ default: {
+ data: {
+ rt: 100,
+ },
+ },
+ };
+
+ const { expectFinished, getData } = await simulateTimeline(
+ timeline,
+ "data-only",
+ simulation_options
+ );
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBe(100);
+ expect(getData().values()[1].rt).toBe(200);
+ });
+
+ test("Simulation options can be set using string lookup", async () => {
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ },
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: "short_trial",
+ },
+ ];
+
+ const simulation_options = {
+ default: {
+ data: {
+ rt: 100,
+ },
+ },
+ short_trial: {
+ data: {
+ rt: 10,
+ },
+ },
+ };
+
+ const { expectFinished, getData } = await simulateTimeline(
+ timeline,
+ "data-only",
+ simulation_options
+ );
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBe(100);
+ expect(getData().values()[1].rt).toBe(10);
+ });
+
+ test("Simulation options can be set with timeline variables", async () => {
+ const jsPsych = initJsPsych();
+
+ const timeline = [
+ {
+ timeline: [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: jsPsych.timelineVariable("sim_opt"),
+ },
+ ],
+ timeline_variables: [{ sim_opt: "short_trial" }, { sim_opt: "long_trial" }],
+ },
+ ];
+
+ const simulation_options = {
+ long_trial: {
+ data: {
+ rt: 100,
+ },
+ },
+ short_trial: {
+ data: {
+ rt: 10,
+ },
+ },
+ };
+
+ const { expectFinished, getData } = await simulateTimeline(
+ timeline,
+ "data-only",
+ simulation_options,
+ jsPsych
+ );
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBe(10);
+ expect(getData().values()[1].rt).toBe(100);
+ });
+
+ test("Simulation options can be a function that evals at run time", async () => {
+ const jsPsych = initJsPsych();
+
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: () => {
+ return {
+ data: {
+ rt: 100,
+ },
+ };
+ },
+ },
+ ];
+
+ const { expectFinished, getData } = await simulateTimeline(timeline);
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBe(100);
+ });
+
+ test("If a plugin doesn't support simulation, it runs as usual", async () => {
+ class FakePlugin {
+ static info = {
+ name: "fake-plugin",
+ parameters: {
+ foo: {
+ type: ParameterType.BOOL,
+ default: true,
+ },
+ },
+ };
+
+ constructor(private jsPsych: JsPsych) {}
+
+ trial(display_element, trial) {
+ display_element.innerHTML = "";
+ display_element.querySelector("#end").addEventListener("click", () => {
+ this.jsPsych.finishTrial({ foo: trial.foo });
+ });
+ }
+ }
+
+ const jsPsych = initJsPsych();
+
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: {
+ data: {
+ rt: 100,
+ },
+ },
+ },
+ {
+ type: FakePlugin,
+ },
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: {
+ data: {
+ rt: 200,
+ },
+ },
+ },
+ ];
+
+ const { expectFinished, expectRunning, getData, getHTML, displayElement } =
+ await simulateTimeline(timeline);
+
+ await expectRunning();
+
+ expect(getHTML()).toContain("button");
+
+ clickTarget(displayElement.querySelector("#end"));
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBe(100);
+ expect(getData().values()[1].foo).toBe(true);
+ expect(getData().values()[2].rt).toBe(200);
+ });
+
+ test("endExperiment() works in simulation mode", async () => {
+ const jsPsych = initJsPsych();
+
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ on_finish: () => {
+ jsPsych.endExperiment("done");
+ },
+ },
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "bar",
+ },
+ ];
+
+ const { expectFinished, getData, getHTML } = await simulateTimeline(
+ timeline,
+ "data-only",
+ {},
+ jsPsych
+ );
+
+ await expectFinished();
+
+ expect(getHTML()).toMatch("done");
+ expect(getData().count()).toBe(1);
+ });
+
+ test("Setting mode in simulation_options will control which mode is used", async () => {
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "bar",
+ simulation_options: {
+ mode: "data-only",
+ },
+ },
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: {
+ mode: "visual",
+ },
+ },
+ ];
+
+ const { expectRunning, expectFinished, getHTML } = await simulateTimeline(timeline);
+
+ await expectRunning();
+
+ expect(getHTML()).toContain("foo");
+
+ jest.runAllTimers();
+
+ await expectFinished();
+ });
+
+ test("Trial can be run normally by specifying simulate:false in simulation options", async () => {
+ const timeline = [
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "bar",
+ },
+ {
+ type: htmlKeyboardResponse,
+ stimulus: "foo",
+ simulation_options: {
+ simulate: false,
+ },
+ },
+ ];
+
+ const { expectRunning, expectFinished, getHTML } = await simulateTimeline(timeline);
+
+ await expectRunning();
+
+ expect(getHTML()).toContain("foo");
+
+ jest.runAllTimers();
+
+ await expectRunning();
+
+ pressKey("a");
+
+ await expectFinished();
+ });
+});
diff --git a/packages/jspsych/tests/randomization/randomziation.test.ts b/packages/jspsych/tests/randomization/randomziation.test.ts
index 3009c7fa..53cc6a58 100644
--- a/packages/jspsych/tests/randomization/randomziation.test.ts
+++ b/packages/jspsych/tests/randomization/randomziation.test.ts
@@ -1,6 +1,7 @@
import {
factorial,
randomID,
+ randomInt,
repeat,
shuffle,
shuffleAlternateGroups,
@@ -134,3 +135,33 @@ describe("shuffleNoRepeats", function () {
expect(repeats).toBe(0);
});
});
+
+describe("randomInt", () => {
+ test("generates random int between positive boundaries", () => {
+ var samples = [];
+ for (var i = 0; i < 1000; i++) {
+ samples.push(randomInt(3, 10));
+ }
+ expect(
+ samples.every((x) => {
+ return x >= 3 && x <= 10;
+ })
+ ).toBe(true);
+ });
+ test("generates random int between negative boundaries", () => {
+ var samples = [];
+ for (var i = 0; i < 1000; i++) {
+ samples.push(randomInt(-5, -1));
+ }
+ expect(
+ samples.every((x) => {
+ return x >= -5 && x <= -1;
+ })
+ ).toBe(true);
+ });
+ test("setting upper < lower throws an error", () => {
+ expect(() => {
+ randomInt(1, 0);
+ }).toThrowError();
+ });
+});
diff --git a/packages/plugin-animation/src/index.spec.ts b/packages/plugin-animation/src/index.spec.ts
index 72c75b82..6a8b47b5 100644
--- a/packages/plugin-animation/src/index.spec.ts
+++ b/packages/plugin-animation/src/index.spec.ts
@@ -1,4 +1,4 @@
-import { startTimeline } from "@jspsych/test-utils";
+import { pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils";
import animation from ".";
@@ -22,3 +22,58 @@ describe("animation plugin", () => {
await expectFinished();
});
});
+
+describe("animation simulation", () => {
+ test("data mode works", async () => {
+ const timeline = [
+ {
+ type: animation,
+ stimuli: ["1.png", "2.png", "3.png", "4.png"],
+ sequence_reps: 3,
+ render_on_canvas: false,
+ },
+ ];
+
+ const { expectFinished, getData } = await simulateTimeline(timeline);
+
+ await expectFinished();
+
+ const data = getData().values()[0];
+
+ expect(data.animation_sequence.length).toBe(12);
+ expect(data.response).not.toBeUndefined();
+ });
+
+ test("visual mode works", async () => {
+ const timeline = [
+ {
+ type: animation,
+ stimuli: ["1.png", "2.png", "3.png", "4.png"],
+ sequence_reps: 3,
+ frame_time: 50,
+ frame_isi: 50,
+ render_on_canvas: false,
+ },
+ ];
+
+ const { expectFinished, expectRunning, getHTML, getData, displayElement } =
+ await simulateTimeline(timeline, "visual");
+
+ await expectRunning();
+
+ expect(getHTML()).toContain("1.png");
+ jest.advanceTimersByTime(50);
+ expect(displayElement.querySelector("img").style.visibility).toBe("hidden");
+ jest.advanceTimersByTime(50);
+ expect(getHTML()).toContain("2.png");
+
+ jest.runAllTimers();
+
+ await expectFinished();
+
+ const data = getData().values()[0];
+
+ expect(data.animation_sequence.length).toBe(24);
+ expect(data.response).not.toBeUndefined();
+ });
+});
diff --git a/packages/plugin-animation/src/index.ts b/packages/plugin-animation/src/index.ts
index e3aa0d79..9f37a38b 100644
--- a/packages/plugin-animation/src/index.ts
+++ b/packages/plugin-animation/src/index.ts
@@ -123,10 +123,7 @@ class AnimationPlugin implements JsPsychPlugin {
}
}, interval_time);
- // show the first frame immediately
- show_next_frame();
-
- function show_next_frame() {
+ const show_next_frame = () => {
if (trial.render_on_canvas) {
display_element.querySelector("#jspsych-animation-image").style.visibility =
"visible";
@@ -166,7 +163,7 @@ class AnimationPlugin implements JsPsychPlugin {
});
}, trial.frame_time);
}
- }
+ };
var after_response = (info) => {
responses.push({
@@ -190,6 +187,93 @@ class AnimationPlugin implements JsPsychPlugin {
persist: true,
allow_held_key: false,
});
+
+ // show the first frame immediately
+ show_next_frame();
+ }
+
+ simulate(
+ trial: TrialType,
+ simulation_mode,
+ simulation_options: any,
+ load_callback: () => void
+ ) {
+ if (simulation_mode == "data-only") {
+ load_callback();
+ this.simulate_data_only(trial, simulation_options);
+ }
+ if (simulation_mode == "visual") {
+ this.simulate_visual(trial, simulation_options, load_callback);
+ }
+ }
+
+ private create_simulation_data(trial: TrialType, simulation_options) {
+ const fake_animation_sequence = [];
+ const fake_responses = [];
+ let t = 0;
+ const check_if_fake_response_generated: () => boolean = () => {
+ return this.jsPsych.randomization.sampleWithReplacement([true, false], 1, [1, 10])[0];
+ };
+ for (let i = 0; i < trial.sequence_reps; i++) {
+ for (const frame of trial.stimuli) {
+ fake_animation_sequence.push({
+ stimulus: frame,
+ time: t,
+ });
+ if (check_if_fake_response_generated()) {
+ fake_responses.push({
+ key_press: this.jsPsych.pluginAPI.getValidKey(trial.choices),
+ rt: t + this.jsPsych.randomization.randomInt(0, trial.frame_time - 1),
+ current_stim: frame,
+ });
+ }
+ t += trial.frame_time;
+ if (trial.frame_isi > 0) {
+ fake_animation_sequence.push({
+ stimulus: "blank",
+ time: t,
+ });
+ if (check_if_fake_response_generated()) {
+ fake_responses.push({
+ key_press: this.jsPsych.pluginAPI.getValidKey(trial.choices),
+ rt: t + this.jsPsych.randomization.randomInt(0, trial.frame_isi - 1),
+ current_stim: "blank",
+ });
+ }
+ t += trial.frame_isi;
+ }
+ }
+ }
+
+ const default_data = {
+ animation_sequence: fake_animation_sequence,
+ response: fake_responses,
+ };
+
+ const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
+
+ this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
+
+ return data;
+ }
+
+ private simulate_data_only(trial: TrialType, simulation_options) {
+ const data = this.create_simulation_data(trial, simulation_options);
+
+ this.jsPsych.finishTrial(data);
+ }
+
+ private simulate_visual(trial: TrialType, simulation_options, load_callback: () => void) {
+ const data = this.create_simulation_data(trial, simulation_options);
+
+ const display_element = this.jsPsych.getDisplayElement();
+
+ this.trial(display_element, trial);
+ load_callback();
+
+ for (const response of data.response) {
+ this.jsPsych.pluginAPI.pressKey(response.key_press, response.rt);
+ }
}
}
diff --git a/packages/plugin-audio-button-response/package.json b/packages/plugin-audio-button-response/package.json
index 11198ba7..734aa58a 100644
--- a/packages/plugin-audio-button-response/package.json
+++ b/packages/plugin-audio-button-response/package.json
@@ -16,7 +16,7 @@
],
"source": "src/index.ts",
"scripts": {
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "npm test -- --watch",
"tsc": "tsc",
"build": "rollup --config",
diff --git a/packages/plugin-audio-button-response/src/index.spec.ts b/packages/plugin-audio-button-response/src/index.spec.ts
index 4405e2f3..4a252f82 100644
--- a/packages/plugin-audio-button-response/src/index.spec.ts
+++ b/packages/plugin-audio-button-response/src/index.spec.ts
@@ -1,4 +1,4 @@
-import { clickTarget, startTimeline } from "@jspsych/test-utils";
+import { clickTarget, simulateTimeline, startTimeline } from "@jspsych/test-utils";
import { initJsPsych } from "jspsych";
import audioButtonResponse from ".";
@@ -15,7 +15,7 @@ describe.skip("audio-button-response", () => {
prompt: "foo",
choices: ["choice1"],
on_load: () => {
- expect(getHTML()).toContain("ffgfgoo");
+ expect(getHTML()).toContain("foo");
clickTarget(displayElement.querySelector("button"));
},
@@ -33,3 +33,54 @@ describe.skip("audio-button-response", () => {
await finished;
});
});
+
+describe("audio-button-response simulation", () => {
+ test("data mode works", async () => {
+ const timeline = [
+ {
+ type: audioButtonResponse,
+ stimulus: "foo.mp3",
+ choices: ["click"],
+ },
+ ];
+
+ const { expectFinished, getData } = await simulateTimeline(timeline);
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBeGreaterThan(0);
+ expect(getData().values()[0].response).toBe(0);
+ });
+
+ // can't run this until we mock Audio elements.
+ test.skip("visual mode works", async () => {
+ const jsPsych = initJsPsych({ use_webaudio: false });
+
+ const timeline = [
+ {
+ type: audioButtonResponse,
+ stimulus: "foo.mp3",
+ prompt: "foo",
+ choices: ["click"],
+ },
+ ];
+
+ const { expectFinished, expectRunning, getHTML, getData } = await simulateTimeline(
+ timeline,
+ "visual",
+ {},
+ jsPsych
+ );
+
+ await expectRunning();
+
+ expect(getHTML()).toContain("foo");
+
+ jest.runAllTimers();
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBeGreaterThan(0);
+ expect(getData().values()[0].response).toBe(0);
+ });
+});
diff --git a/packages/plugin-audio-button-response/src/index.ts b/packages/plugin-audio-button-response/src/index.ts
index eb054782..94206316 100644
--- a/packages/plugin-audio-button-response/src/index.ts
+++ b/packages/plugin-audio-button-response/src/index.ts
@@ -83,6 +83,7 @@ type Info = typeof info;
*/
class AudioButtonResponsePlugin implements JsPsychPlugin {
static info = info;
+ private audio;
constructor(private jsPsych: JsPsych) {}
@@ -92,7 +93,6 @@ class AudioButtonResponsePlugin implements JsPsychPlugin {
// setup stimulus
var context = this.jsPsych.pluginAPI.audioContext();
- var audio;
// store response
var response = {
@@ -108,12 +108,12 @@ class AudioButtonResponsePlugin implements JsPsychPlugin {
.getAudioBuffer(trial.stimulus)
.then((buffer) => {
if (context !== null) {
- audio = context.createBufferSource();
- audio.buffer = buffer;
- audio.connect(context.destination);
+ this.audio = context.createBufferSource();
+ this.audio.buffer = buffer;
+ this.audio.connect(context.destination);
} else {
- audio = buffer;
- audio.currentTime = 0;
+ this.audio = buffer;
+ this.audio.currentTime = 0;
}
setupTrial();
})
@@ -127,12 +127,12 @@ class AudioButtonResponsePlugin implements JsPsychPlugin {
const setupTrial = () => {
// set up end event if trial needs it
if (trial.trial_ends_after_audio) {
- audio.addEventListener("ended", end_trial);
+ this.audio.addEventListener("ended", end_trial);
}
// enable buttons after audio ends if necessary
if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) {
- audio.addEventListener("ended", enable_buttons);
+ this.audio.addEventListener("ended", enable_buttons);
}
//display buttons
@@ -188,9 +188,9 @@ class AudioButtonResponsePlugin implements JsPsychPlugin {
// start audio
if (context !== null) {
startTime = context.currentTime;
- audio.start(startTime);
+ this.audio.start(startTime);
} else {
- audio.play();
+ this.audio.play();
}
// end trial if time limit is set
@@ -231,13 +231,13 @@ class AudioButtonResponsePlugin implements JsPsychPlugin {
// stop the audio file if it is playing
// remove end event listeners if they exist
if (context !== null) {
- audio.stop();
+ this.audio.stop();
} else {
- audio.pause();
+ this.audio.pause();
}
- audio.removeEventListener("ended", end_trial);
- audio.removeEventListener("ended", enable_buttons);
+ this.audio.removeEventListener("ended", end_trial);
+ this.audio.removeEventListener("ended", enable_buttons);
// gather the data to store for the trial
var trial_data = {
@@ -286,6 +286,65 @@ class AudioButtonResponsePlugin implements JsPsychPlugin {
trial_complete = resolve;
});
}
+
+ simulate(
+ trial: TrialType,
+ simulation_mode,
+ simulation_options: any,
+ load_callback: () => void
+ ) {
+ if (simulation_mode == "data-only") {
+ load_callback();
+ this.simulate_data_only(trial, simulation_options);
+ }
+ if (simulation_mode == "visual") {
+ this.simulate_visual(trial, simulation_options, load_callback);
+ }
+ }
+
+ private create_simulation_data(trial: TrialType, simulation_options) {
+ const default_data = {
+ stimulus: trial.stimulus,
+ rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
+ response: this.jsPsych.randomization.randomInt(0, trial.choices.length - 1),
+ };
+
+ const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
+
+ this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
+
+ return data;
+ }
+
+ private simulate_data_only(trial: TrialType, simulation_options) {
+ const data = this.create_simulation_data(trial, simulation_options);
+
+ this.jsPsych.finishTrial(data);
+ }
+
+ private simulate_visual(trial: TrialType, simulation_options, load_callback: () => void) {
+ const data = this.create_simulation_data(trial, simulation_options);
+
+ const display_element = this.jsPsych.getDisplayElement();
+
+ const respond = () => {
+ if (data.rt !== null) {
+ this.jsPsych.pluginAPI.clickTarget(
+ display_element.querySelector(`div[data-choice="${data.response}"] button`),
+ data.rt
+ );
+ }
+ };
+
+ this.trial(display_element, trial, () => {
+ load_callback();
+ if (!trial.response_allowed_while_playing) {
+ this.audio.addEventListener("ended", respond);
+ } else {
+ respond();
+ }
+ });
+ }
}
export default AudioButtonResponsePlugin;
diff --git a/packages/plugin-audio-keyboard-response/package.json b/packages/plugin-audio-keyboard-response/package.json
index fbc34244..4ab21528 100644
--- a/packages/plugin-audio-keyboard-response/package.json
+++ b/packages/plugin-audio-keyboard-response/package.json
@@ -16,7 +16,7 @@
],
"source": "src/index.ts",
"scripts": {
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "npm test -- --watch",
"tsc": "tsc",
"build": "rollup --config",
diff --git a/packages/plugin-audio-keyboard-response/src/index.spec.ts b/packages/plugin-audio-keyboard-response/src/index.spec.ts
new file mode 100644
index 00000000..cd5e0737
--- /dev/null
+++ b/packages/plugin-audio-keyboard-response/src/index.spec.ts
@@ -0,0 +1,55 @@
+import { pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils";
+import { initJsPsych } from "jspsych";
+
+import audioKeyboardResponse from ".";
+
+jest.useFakeTimers();
+
+describe("audio-keyboard-response simulation", () => {
+ test("data mode works", async () => {
+ const timeline = [
+ {
+ type: audioKeyboardResponse,
+ stimulus: "foo.mp3",
+ },
+ ];
+
+ const { expectFinished, getData } = await simulateTimeline(timeline);
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBeGreaterThan(0);
+ expect(typeof getData().values()[0].response).toBe("string");
+ });
+
+ // can't run this until we mock Audio elements.
+ test.skip("visual mode works", async () => {
+ const jsPsych = initJsPsych({ use_webaudio: false });
+
+ const timeline = [
+ {
+ type: audioKeyboardResponse,
+ stimulus: "foo.mp3",
+ prompt: "foo",
+ },
+ ];
+
+ const { expectFinished, expectRunning, getHTML, getData } = await simulateTimeline(
+ timeline,
+ "visual",
+ {},
+ jsPsych
+ );
+
+ await expectRunning();
+
+ expect(getHTML()).toContain("foo");
+
+ jest.runAllTimers();
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBeGreaterThan(0);
+ expect(typeof getData().values()[0].response).toBe("string");
+ });
+});
diff --git a/packages/plugin-audio-keyboard-response/src/index.ts b/packages/plugin-audio-keyboard-response/src/index.ts
index aa2deafe..aaae336b 100644
--- a/packages/plugin-audio-keyboard-response/src/index.ts
+++ b/packages/plugin-audio-keyboard-response/src/index.ts
@@ -60,6 +60,7 @@ type Info = typeof info;
*/
class AudioKeyboardResponsePlugin implements JsPsychPlugin {
static info = info;
+ private audio;
constructor(private jsPsych: JsPsych) {}
@@ -69,7 +70,6 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin {
// setup stimulus
var context = this.jsPsych.pluginAPI.audioContext();
- var audio;
// store response
var response = {
@@ -85,12 +85,12 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin {
.getAudioBuffer(trial.stimulus)
.then((buffer) => {
if (context !== null) {
- audio = context.createBufferSource();
- audio.buffer = buffer;
- audio.connect(context.destination);
+ this.audio = context.createBufferSource();
+ this.audio.buffer = buffer;
+ this.audio.connect(context.destination);
} else {
- audio = buffer;
- audio.currentTime = 0;
+ this.audio = buffer;
+ this.audio.currentTime = 0;
}
setupTrial();
})
@@ -104,7 +104,7 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin {
const setupTrial = () => {
// set up end event if trial needs it
if (trial.trial_ends_after_audio) {
- audio.addEventListener("ended", end_trial);
+ this.audio.addEventListener("ended", end_trial);
}
// show prompt if there is one
@@ -115,16 +115,16 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin {
// start audio
if (context !== null) {
startTime = context.currentTime;
- audio.start(startTime);
+ this.audio.start(startTime);
} else {
- audio.play();
+ this.audio.play();
}
// start keyboard listener when trial starts or sound ends
if (trial.response_allowed_while_playing) {
setup_keyboard_listener();
} else if (!trial.trial_ends_after_audio) {
- audio.addEventListener("ended", setup_keyboard_listener);
+ this.audio.addEventListener("ended", setup_keyboard_listener);
}
// end trial if time limit is set
@@ -145,13 +145,13 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin {
// stop the audio file if it is playing
// remove end event listeners if they exist
if (context !== null) {
- audio.stop();
+ this.audio.stop();
} else {
- audio.pause();
+ this.audio.pause();
}
- audio.removeEventListener("ended", end_trial);
- audio.removeEventListener("ended", setup_keyboard_listener);
+ this.audio.removeEventListener("ended", end_trial);
+ this.audio.removeEventListener("ended", setup_keyboard_listener);
// kill keyboard listeners
this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
@@ -211,6 +211,62 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin {
trial_complete = resolve;
});
}
+
+ simulate(
+ trial: TrialType,
+ simulation_mode,
+ simulation_options: any,
+ load_callback: () => void
+ ) {
+ if (simulation_mode == "data-only") {
+ load_callback();
+ this.simulate_data_only(trial, simulation_options);
+ }
+ if (simulation_mode == "visual") {
+ this.simulate_visual(trial, simulation_options, load_callback);
+ }
+ }
+
+ private simulate_data_only(trial: TrialType, simulation_options) {
+ const data = this.create_simulation_data(trial, simulation_options);
+
+ this.jsPsych.finishTrial(data);
+ }
+
+ private simulate_visual(trial: TrialType, simulation_options, load_callback: () => void) {
+ const data = this.create_simulation_data(trial, simulation_options);
+
+ const display_element = this.jsPsych.getDisplayElement();
+
+ const respond = () => {
+ if (data.rt !== null) {
+ this.jsPsych.pluginAPI.pressKey(data.response, data.rt);
+ }
+ };
+
+ this.trial(display_element, trial, () => {
+ load_callback();
+ if (!trial.response_allowed_while_playing) {
+ this.audio.addEventListener("ended", respond);
+ } else {
+ respond();
+ }
+ });
+ }
+
+ private create_simulation_data(trial: TrialType, simulation_options) {
+ const default_data = {
+ stimulus: trial.stimulus,
+ rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
+ response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
+ };
+
+ const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
+
+ this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
+
+ return data;
+ }
}
export default AudioKeyboardResponsePlugin;
diff --git a/packages/plugin-audio-slider-response/src/index.spec.ts b/packages/plugin-audio-slider-response/src/index.spec.ts
new file mode 100644
index 00000000..180214d9
--- /dev/null
+++ b/packages/plugin-audio-slider-response/src/index.spec.ts
@@ -0,0 +1,57 @@
+import { pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils";
+import { initJsPsych } from "jspsych";
+
+import audioSliderResponse from ".";
+
+jest.useFakeTimers();
+
+describe("audio-slider-response simulation", () => {
+ test("data mode works", async () => {
+ const timeline = [
+ {
+ type: audioSliderResponse,
+ stimulus: "foo.mp3",
+ },
+ ];
+
+ const { expectFinished, getData } = await simulateTimeline(timeline);
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBeGreaterThan(0);
+ expect(getData().values()[0].response).toBeGreaterThanOrEqual(0);
+ expect(getData().values()[0].response).toBeLessThanOrEqual(100);
+ });
+
+ // can't run this until we mock Audio elements.
+ test.skip("visual mode works", async () => {
+ const jsPsych = initJsPsych({ use_webaudio: false });
+
+ const timeline = [
+ {
+ type: audioSliderResponse,
+ stimulus: "foo.mp3",
+ prompt: "foo",
+ },
+ ];
+
+ const { expectFinished, expectRunning, getHTML, getData } = await simulateTimeline(
+ timeline,
+ "visual",
+ {},
+ jsPsych
+ );
+
+ await expectRunning();
+
+ expect(getHTML()).toContain("foo");
+
+ jest.runAllTimers();
+
+ await expectFinished();
+
+ expect(getData().values()[0].rt).toBeGreaterThan(0);
+ expect(getData().values()[0].response).toBeGreaterThanOrEqual(0);
+ expect(getData().values()[0].response).toBeLessThanOrEqual(100);
+ });
+});
diff --git a/packages/plugin-audio-slider-response/src/index.ts b/packages/plugin-audio-slider-response/src/index.ts
index 67b756da..a38200aa 100644
--- a/packages/plugin-audio-slider-response/src/index.ts
+++ b/packages/plugin-audio-slider-response/src/index.ts
@@ -104,6 +104,7 @@ type Info = typeof info;
*/
class AudioSliderResponsePlugin implements JsPsychPlugin {
static info = info;
+ private audio;
constructor(private jsPsych: JsPsych) {}
@@ -116,7 +117,6 @@ class AudioSliderResponsePlugin implements JsPsychPlugin {
// setup stimulus
var context = this.jsPsych.pluginAPI.audioContext();
- var audio;
// record webaudio context start time
var startTime;
@@ -129,12 +129,12 @@ class AudioSliderResponsePlugin implements JsPsychPlugin {
.getAudioBuffer(trial.stimulus)
.then((buffer) => {
if (context !== null) {
- audio = context.createBufferSource();
- audio.buffer = buffer;
- audio.connect(context.destination);
+ this.audio = context.createBufferSource();
+ this.audio.buffer = buffer;
+ this.audio.connect(context.destination);
} else {
- audio = buffer;
- audio.currentTime = 0;
+ this.audio = buffer;
+ this.audio.currentTime = 0;
}
setupTrial();
})
@@ -148,12 +148,12 @@ class AudioSliderResponsePlugin implements JsPsychPlugin {
const setupTrial = () => {
// set up end event if trial needs it
if (trial.trial_ends_after_audio) {
- audio.addEventListener("ended", end_trial);
+ this.audio.addEventListener("ended", end_trial);
}
// enable slider after audio ends if necessary
if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) {
- audio.addEventListener("ended", enable_slider);
+ this.audio.addEventListener("ended", enable_slider);
}
var html = '
';
var elements = trial.text.split("%");
- var solutions = [];
+ const solutions = this.getSolutions(trial.text);
+ let solution_counter = 0;
for (var i = 0; i < elements.length; i++) {
if (i % 2 === 0) {
html += elements[i];
} else {
- solutions.push(elements[i].trim());
- html += '';
+ html += ``;
+ solution_counter++;
}
}
html += "