diff --git a/.changeset/afraid-badgers-lie.md b/.changeset/afraid-badgers-lie.md new file mode 100644 index 0000000000..0039503371 --- /dev/null +++ b/.changeset/afraid-badgers-lie.md @@ -0,0 +1,5 @@ +--- +"jspsych": minor +--- + +Added `jsPsych.abortTimelineByName()`. This allows for aborting a specific active timeline by its `name` property. The `name` can be set in the description of the timline. diff --git a/.changeset/bright-apples-hope.md b/.changeset/bright-apples-hope.md new file mode 100644 index 0000000000..9af2958eb6 --- /dev/null +++ b/.changeset/bright-apples-hope.md @@ -0,0 +1,5 @@ +--- +"jspsych": patch +--- + +`getKeyboardResponse` now returns the `key` in the original case (e.g., "Enter" instead of "enter") for easier matching to standard key event documentation. diff --git a/.changeset/button-layouts.md b/.changeset/button-layouts.md new file mode 100644 index 0000000000..0ba3717c1c --- /dev/null +++ b/.changeset/button-layouts.md @@ -0,0 +1,8 @@ +--- +"@jspsych/plugin-html-button-response": major +"jspsych": patch +--- + +Button plugins now support either `display: grid` or `display: flex` on the container element that hold the buttons. If the layout is `grid`, the number of rows and/or columns can be specified. The `margin_horizontal` and `margin_vertical` parameters have been removed from the button plugins. If you need control over the button CSS, you can add inline style to the button element using the `button_html` parameter. + +jspsych.css has new layout classes to support this feature. diff --git a/.changeset/button-response-plugins.md b/.changeset/button-response-plugins.md new file mode 100644 index 0000000000..4588392f34 --- /dev/null +++ b/.changeset/button-response-plugins.md @@ -0,0 +1,31 @@ +--- +"@jspsych/plugin-audio-button-response": major +"@jspsych/plugin-canvas-button-response": major +"@jspsych/plugin-html-button-response": major +"@jspsych/plugin-image-button-response": major +"@jspsych/plugin-video-button-response": major +--- + +- Make `button_html` a function parameter which, given a choice's text and its index, returns the HTML string of the choice's button. If you were previously passing a string to `button_html`, like ``, you can now pass the function + ```js + function (choice) { + return '"; + } + ``` + Similarly, if you were using the array syntax, like + ```js + ['', '', ''] + ``` + an easy way to migrate your trial definition is to pass a function which accesses your array and replaces the `%choice%` placeholder: + ```js + function (choice, choice_index) { + return ['', '', ''][choice_index].replace("%choice%", choice); + } + ``` + From there on, you can further simplify your function. For instance, if the intention of the above example is to have alternating button classes, the `button_html` function might be rewritten as + ```js + function (choice, choice_index) { + return '"; + } + ``` +- Simplify the button DOM structure and styling: Buttons are no longer wrapped in individual container `div`s for spacing and `data-choice` attributes. Instead, each button is assigned its `data-choice` attribute and all buttons are direct children of the button group container `div`. The container `div`, in turn, utilizes a flexbox layout to position the buttons. diff --git a/.changeset/chilled-papayas-admire.md b/.changeset/chilled-papayas-admire.md new file mode 100644 index 0000000000..830e20d443 --- /dev/null +++ b/.changeset/chilled-papayas-admire.md @@ -0,0 +1,58 @@ +--- +"jspsych": minor +"@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-free-sort": minor +"@jspsych/plugin-fullscreen": minor +"@jspsych/plugin-html-audio-response": minor +"@jspsych/plugin-html-button-response": minor +"@jspsych/plugin-html-keyboard-response": minor +"@jspsych/plugin-html-slider-response": minor +"@jspsych/plugin-html-video-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-initialize-camera": minor +"@jspsych/plugin-initialize-microphone": minor +"@jspsych/plugin-instructions": minor +"@jspsych/plugin-maxdiff": minor +"@jspsych/plugin-mirror-camera": minor +"@jspsych/plugin-preload": minor +"@jspsych/plugin-reconstruction": minor +"@jspsych/plugin-resize": 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-sketchpad": minor +"@jspsych/plugin-survey": minor +"@jspsych/plugin-survey-html-form": 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-virtual-chinrest": minor +"@jspsych/plugin-visual-search-circle": minor +"@jspsych/plugin-webgazer-calibrate": minor +"@jspsych/plugin-webgazer-init-camera": minor +"@jspsych/plugin-webgazer-validate": minor +--- + +Updated all plugins to implement new pluginInfo standard that contains version, data generated and new documentation style to match migration of docs to be integrated with the code and packages themselves" diff --git a/.changeset/chilly-pans-sin.md b/.changeset/chilly-pans-sin.md new file mode 100644 index 0000000000..f6173d2ad9 --- /dev/null +++ b/.changeset/chilly-pans-sin.md @@ -0,0 +1,5 @@ +--- +"@jspsych/config": major +--- + +Activate TypeScript's `isolatedModules` flag in the root `tsconfig.json` file. If you are facing any TypeScript errors due to `isolatedModules`, please update your code according to the error messages. diff --git a/.changeset/core-rewrite.md b/.changeset/core-rewrite.md new file mode 100644 index 0000000000..1e5503dc97 --- /dev/null +++ b/.changeset/core-rewrite.md @@ -0,0 +1,39 @@ +--- +"jspsych": major +--- + +Rewrite jsPsych's core logic. The following breaking changes have been made: + +**Timeline Events** + +- `conditional_function` is no longer executed on every iteration of a looping timeline, but only once before running the first trial of the timeline. If you rely on the old behavior, move your `conditional_function` into a nested timeline instead. +- `on_timeline_start` and `on_timeline_finish` are no longer invoked in every repetition of a timeline, but only at the beginning or at the end of the timeline, respectively. If you rely on the old behavior, move the `on_timeline_start` and `on_timeline_finish` callbacks into a nested timeline. + +**Timeline Variables** + +- The functionality of `jsPsych.timelineVariable()` has been explicitly split into two functions, `jsPsych.timelineVariable()` and `jsPsych.evaluateTimelineVariable()`. Use `jsPsych.timelineVariable()` to create a timeline variable placeholder and `jsPsych.evaluateTimelineVariable()` to retrieve a given timeline variable's current value. +- `jsPsych.evaluateTimelineVariable()` now throws an error if a variable is not found. +- `jsPsych.getAllTimelineVariables()` has been replaced by a trial-level `save_timeline_variables` parameter that can be used to include all or some timeline variables in a trial's result data. + +**Parameter Handling** + +- JsPsych will now throw an error when a non-array value is used for a trial parameter marked as `array: true` in the plugin's info object. +- Parameter functions and timeline variables are no longer automatically evaluated recursively throughout the whole trial object, but only for the parameters that a plugin specifies in its `info` object. Parameter functions and timeline variables in nested objects are only evaluated if the nested object's parameters are explicitly specified using the `nested` property in the parameter description. + +**Progress Bar** + +- `jsPsych.setProgressBar(x)` has been replaced by `jsPsych.progressBar.progress = x` +- `jsPsych.getProgressBarCompleted()` has been replaced by `jsPsych.progressBar.progress` +- The automatic progress bar updates after every trial now, including trials in nested timelines. + +**Data Handling** + +- Timeline nodes no longer have IDs. As a consequence, the `internal_node_id` trial result property and `jsPsych.data.getDataByTimelineNode()` have been removed. +- Unlike previously, the `save_trial_parameters` parameter can only be used to remove parameters that are specified in the plugin's info object. Other result properties will be left untouched. + +**Miscellaneous Changes** + +- `jsPsych.endExperiment()` and `jsPsych.endCurrentTimeline()` have been renamed to `jsPsych.abortExperiment()` and `jsPsych.abortCurrentTimeline()`, respectively. +- JsPsych now internally relies on the JavaScript event loop. This means automated tests have to `await` utility functions like `pressKey()` to process the event loop. +- The `jspsych` package no longer exports `universalPluginParameters` and the `UniversalPluginParameters` type. +- Interaction listeners are now removed when the experiment ends. diff --git a/.changeset/esbuild.md b/.changeset/esbuild.md new file mode 100644 index 0000000000..591ffff498 --- /dev/null +++ b/.changeset/esbuild.md @@ -0,0 +1,5 @@ +--- +"@jspsych/config": major +--- + +Migrate the build chain from TypeScript, Babel, and Terser to [esbuild](https://esbuild.github.io/). Babel and Terser are no longer included as dependencies and the Babel configuration at `@jspsych/config/babel` has been removed. The minified browser builds are only transpiled down to [ES2015](https://caniuse.com/es6) now. diff --git a/.changeset/flat-tables-repair.md b/.changeset/flat-tables-repair.md new file mode 100644 index 0000000000..1e912fbbf7 --- /dev/null +++ b/.changeset/flat-tables-repair.md @@ -0,0 +1,7 @@ +--- +"jspsych": major +--- + +Removed the `exclusions` option from `initJsPsych()`. The recommended replacement for this functionality is the browser-check plugin. + +Removed the `hardwareAPI` module from the pluginAPI. This was no longer being updated and the features were out of date. diff --git a/.changeset/forty-weeks-walk.md b/.changeset/forty-weeks-walk.md new file mode 100644 index 0000000000..b0133a1e01 --- /dev/null +++ b/.changeset/forty-weeks-walk.md @@ -0,0 +1,5 @@ +--- +"jspsych": major +--- + +Changed the behavior of `DataColumn.mean()` to exclude `null` and `undefined` values from the calculation, as suggested in #2905 diff --git a/.changeset/fresh-doors-watch.md b/.changeset/fresh-doors-watch.md new file mode 100644 index 0000000000..865938e424 --- /dev/null +++ b/.changeset/fresh-doors-watch.md @@ -0,0 +1,8 @@ +--- +"jspsych": major +"@jspsych/plugin-audio-button-response": minor +"@jspsych/plugin-audio-keyboard-response": minor +"@jspsych/plugin-audio-slider-response": minor +--- + +Changed plugins to use AudioPlayer class; added tests using AudioPlayer mock; plugins now use AudioPlayerInterface. diff --git a/.changeset/lucky-glasses-crash.md b/.changeset/lucky-glasses-crash.md new file mode 100644 index 0000000000..54522df1e0 --- /dev/null +++ b/.changeset/lucky-glasses-crash.md @@ -0,0 +1,6 @@ +--- +"jspsych": minor +--- + +Added `record_data` as a parameter available for any trial. Setting `record_data: false` will prevent data from being stored in the jsPsych data object for that trial. + diff --git a/.changeset/old-moons-lay.md b/.changeset/old-moons-lay.md new file mode 100644 index 0000000000..5b3ffc70a9 --- /dev/null +++ b/.changeset/old-moons-lay.md @@ -0,0 +1,5 @@ +--- +"jspsych": minor +--- + +Allow trial `on_finish` methods to be asynchronous, i.e. return a `Promise`. Prior to this, promises returned by `on_finish` were not awaited before proceeding with the next trial. diff --git a/.changeset/pretty-lions-float.md b/.changeset/pretty-lions-float.md new file mode 100644 index 0000000000..be0b864dde --- /dev/null +++ b/.changeset/pretty-lions-float.md @@ -0,0 +1,5 @@ +--- +"jspsych": patch +--- + +Fix typo in randomInt error message diff --git a/.changeset/proud-stingrays-wonder.md b/.changeset/proud-stingrays-wonder.md new file mode 100644 index 0000000000..8c4173342b --- /dev/null +++ b/.changeset/proud-stingrays-wonder.md @@ -0,0 +1,5 @@ +--- +"jspsych": major +--- + +Removed `max-width: 95%` CSS rule on the `.jspsych-content` `
`. This rule existed to address an old IE bug with flex layouts. diff --git a/.changeset/rich-cups-roll.md b/.changeset/rich-cups-roll.md new file mode 100644 index 0000000000..ec6cb3ff27 --- /dev/null +++ b/.changeset/rich-cups-roll.md @@ -0,0 +1,7 @@ +--- +"@jspsych/plugin-canvas-button-response": patch +"@jspsych/plugin-canvas-keyboard-response": patch +"@jspsych/plugin-canvas-slider-response": patch +--- + +Change canvas display to `block` to fix issues when canvas is full screen. diff --git a/.changeset/rotten-mails-collect.md b/.changeset/rotten-mails-collect.md new file mode 100644 index 0000000000..3bb6c199f9 --- /dev/null +++ b/.changeset/rotten-mails-collect.md @@ -0,0 +1,58 @@ +--- +"jspsych": major +"@jspsych/plugin-animation": major +"@jspsych/plugin-audio-button-response": major +"@jspsych/plugin-audio-keyboard-response": major +"@jspsych/plugin-audio-slider-response": major +"@jspsych/plugin-browser-check": major +"@jspsych/plugin-call-function": major +"@jspsych/plugin-canvas-button-response": major +"@jspsych/plugin-canvas-keyboard-response": major +"@jspsych/plugin-canvas-slider-response": major +"@jspsych/plugin-categorize-animation": major +"@jspsych/plugin-categorize-html": major +"@jspsych/plugin-categorize-image": major +"@jspsych/plugin-cloze": major +"@jspsych/plugin-external-html": major +"@jspsych/plugin-free-sort": major +"@jspsych/plugin-fullscreen": major +"@jspsych/plugin-html-audio-response": major +"@jspsych/plugin-html-button-response": major +"@jspsych/plugin-html-keyboard-response": major +"@jspsych/plugin-html-slider-response": major +"@jspsych/plugin-html-video-response": major +"@jspsych/plugin-iat-html": major +"@jspsych/plugin-iat-image": major +"@jspsych/plugin-image-button-response": major +"@jspsych/plugin-image-keyboard-response": major +"@jspsych/plugin-image-slider-response": major +"@jspsych/plugin-initialize-camera": major +"@jspsych/plugin-initialize-microphone": major +"@jspsych/plugin-instructions": major +"@jspsych/plugin-maxdiff": major +"@jspsych/plugin-mirror-camera": major +"@jspsych/plugin-preload": major +"@jspsych/plugin-reconstruction": major +"@jspsych/plugin-resize": major +"@jspsych/plugin-same-different-html": major +"@jspsych/plugin-same-different-image": major +"@jspsych/plugin-serial-reaction-time": major +"@jspsych/plugin-serial-reaction-time-mouse": major +"@jspsych/plugin-sketchpad": major +"@jspsych/plugin-survey": major +"@jspsych/plugin-survey-html-form": major +"@jspsych/plugin-survey-likert": major +"@jspsych/plugin-survey-multi-choice": major +"@jspsych/plugin-survey-multi-select": major +"@jspsych/plugin-survey-text": major +"@jspsych/plugin-video-button-response": major +"@jspsych/plugin-video-keyboard-response": major +"@jspsych/plugin-video-slider-response": major +"@jspsych/plugin-virtual-chinrest": major +"@jspsych/plugin-visual-search-circle": major +"@jspsych/plugin-webgazer-calibrate": major +"@jspsych/plugin-webgazer-init-camera": major +"@jspsych/plugin-webgazer-validate": major +--- + +`finishTrial()` now clears the display and any timeouts set with `pluginApi.setTimeout()` diff --git a/.changeset/sour-ants-push.md b/.changeset/sour-ants-push.md new file mode 100644 index 0000000000..eac5747e46 --- /dev/null +++ b/.changeset/sour-ants-push.md @@ -0,0 +1,5 @@ +--- +"@jspsych/plugin-instructions": minor +--- + +Add callback function when navigating through pages diff --git a/.changeset/stupid-baboons-wait.md b/.changeset/stupid-baboons-wait.md new file mode 100644 index 0000000000..a2477adb8f --- /dev/null +++ b/.changeset/stupid-baboons-wait.md @@ -0,0 +1,5 @@ +--- +"jspsych": minor +--- + +Allow message_progress_bar to be a function diff --git a/.changeset/thick-berries-arrive.md b/.changeset/thick-berries-arrive.md new file mode 100644 index 0000000000..ff7a5f24fe --- /dev/null +++ b/.changeset/thick-berries-arrive.md @@ -0,0 +1,5 @@ +--- +"@jspsych/test-utils": minor +--- + +clickTarget method now respects disabled tag on form elements. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cccc9a4684..c0b1bfdd77 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,10 +32,11 @@ jobs: restore-keys: | ${{ runner.os }}-node-${{ matrix.node }}-turbo- + - name: Check types + run: npm run tsc + - name: Build packages run: npm run build - name: Run tests - run: npm run test -- --ci --coverage --maxWorkers=2 - env: - NODE_OPTIONS: "--max-old-space-size=4096" # Increase heap size for jest + run: npm run test -- --ci --coverage --maxWorkers=2 --reporters=default --reporters=github-actions diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd195fc0bd..baff319d74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,10 +35,11 @@ jobs: restore-keys: | ${{ runner.os }}-node-20-turbo- + - name: Check types + run: npm run tsc + - name: Run tests run: npm run test -- --ci --maxWorkers=2 - env: - NODE_OPTIONS: "--max-old-space-size=4096" # Increase heap size for jest - name: Create Release Pull Request or Publish Packages id: changesets diff --git a/contributors.md b/contributors.md index f43ac2e539..a377a52864 100644 --- a/contributors.md +++ b/contributors.md @@ -61,4 +61,6 @@ The following people have contributed to the development of jsPsych by writing c * Rob Wilkinson - https://github.com/RobAWilkinson * Andy Woods - https://github.com/andytwoods * Reto Wyss - https://github.com/retowyss -* Haotian Tu - https://github.com/thtTNT \ No newline at end of file +* Shaobin Jiang - https://github.com/Shaobin-Jiang +* Haotian Tu - https://github.com/thtTNT + diff --git a/docs/demos/jspsych-audio-button-response-demo-2.html b/docs/demos/jspsych-audio-button-response-demo-2.html index d8fcb3c380..4a82816bad 100644 --- a/docs/demos/jspsych-audio-button-response-demo-2.html +++ b/docs/demos/jspsych-audio-button-response-demo-2.html @@ -30,7 +30,7 @@ stimulus: 'sound/roar.mp3', choices: images, prompt: "

Which animal made the sound?

", - button_html: '' + button_html: (choice)=>`` }; timeline.push(trial); diff --git a/docs/demos/jspsych-audio-button-response-demo-3.html b/docs/demos/jspsych-audio-button-response-demo-3.html new file mode 100644 index 0000000000..e4f95ac8a8 --- /dev/null +++ b/docs/demos/jspsych-audio-button-response-demo-3.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/docs/demos/jspsych-canvas-button-response-demo3.html b/docs/demos/jspsych-canvas-button-response-demo3.html index c309044566..c7cd92d7f0 100644 --- a/docs/demos/jspsych-canvas-button-response-demo3.html +++ b/docs/demos/jspsych-canvas-button-response-demo3.html @@ -22,13 +22,13 @@ } // To use the canvas stimulus function with timeline variables, - // the jsPsych.timelineVariable() function can be used inside your stimulus function. + // the jsPsych.evaluateTimelineVariable() function can be used inside your stimulus function. // In addition, this code demonstrates how to check whether participants' answers were correct or not. const circle_procedure = { timeline: [{ type: jsPsychCanvasButtonResponse, stimulus: function(c) { - filledCirc(c, jsPsych.timelineVariable('radius'), jsPsych.timelineVariable('color')); + filledCirc(c, jsPsych.evaluateTimelineVariable('radius'), jsPsych.evaluateTimelineVariable('color')); }, canvas_size: [300, 300], choices: ['Red', 'Green', 'Blue'], diff --git a/docs/demos/jspsych-html-audio-response-demo3.html b/docs/demos/jspsych-html-audio-response-demo3.html index 9e80904f6e..8010c7c916 100644 --- a/docs/demos/jspsych-html-audio-response-demo3.html +++ b/docs/demos/jspsych-html-audio-response-demo3.html @@ -61,7 +61,7 @@ }, prompt: '

Click the object the matches the spoken name.

', choices: ['img/9.gif','img/10.gif','img/11.gif','img/12.gif'], - button_html: '' + button_html: (choice) => `` } var trial_loop = { diff --git a/docs/demos/jspsych-html-button-response-demo2.html b/docs/demos/jspsych-html-button-response-demo2.html new file mode 100644 index 0000000000..66af48b512 --- /dev/null +++ b/docs/demos/jspsych-html-button-response-demo2.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/docs/demos/sound/telephone.mp3 b/docs/demos/sound/telephone.mp3 new file mode 100644 index 0000000000..5fc6954cfe Binary files /dev/null and b/docs/demos/sound/telephone.mp3 differ diff --git a/docs/developers/configuration.md b/docs/developers/configuration.md index 73cc9ea97b..27f39a4db7 100644 --- a/docs/developers/configuration.md +++ b/docs/developers/configuration.md @@ -24,26 +24,27 @@ or ```sh git clone https://github.com/jspsych/jspsych-contrib.git && cd jspsych-contrib ``` + in a terminal. Then run `npm install`. This will create a `node_modules` directory and install all the dependencies into it that are required to build and test jsPsych. !!! info - The jsPsych (-contrib) repositories depend on the `canvas` package which comes with pre-built binaries. - On systems for which no pre-built binaries are available, `npm install` will try to build the binaries from scratch, sometimes failing with an error message mentioning the `canvas` package. - If you are facing such installation issues, please follow the [installation instructions](https://github.com/Automattic/node-canvas/wiki#installation-guides) of the `canvas` package and run `npm install` again afterwards. +The jsPsych (-contrib) repositories depend on the `canvas` package which comes with pre-built binaries. +On systems for which no pre-built binaries are available, `npm install` will try to build the binaries from scratch, sometimes failing with an error message mentioning the `canvas` package. +If you are facing such installation issues, please follow the [installation instructions](https://github.com/Automattic/node-canvas/wiki#installation-guides) of the `canvas` package and run `npm install` again afterwards. !!! info - If you are running `npm install` in the core jsPsych repository, this will also execute the build chain for all packages in the jsPsych repository. - This step may take a few minutes. - If you would like to use that time efficiently, consider reading the following two sections to know what's happening. +If you are running `npm install` in the core jsPsych repository, this will also execute the build chain for all packages in the jsPsych repository. +This step may take a few minutes. +If you would like to use that time efficiently, consider reading the following two sections to know what's happening. ## Repository structure A Node.js package is a directory that contains a `package.json` file describing it. Most importantly, a `package.json` file lists other packages that the package depends on. -The jsPsych and jspsych-contrib repositories use NPM *workspaces*. +The jsPsych and jspsych-contrib repositories use NPM _workspaces_. That means, running `npm install` in the repository root will install the dependencies for all packages in the `packages` directory. The core jsPsych library and every jsPsych plugin or extension is laid out as an individual package. These packages are published to the [NPM registry](https://www.npmjs.com/) where they can be downloaded by NPM or any CDN (such as [unpkg](https://unpkg.com/)). @@ -53,40 +54,39 @@ These packages are published to the [NPM registry](https://www.npmjs.com/) where JsPsych comes with a build chain (specified in the `@jspsych/config` package) that can be executed by running `npm run build` in a package's directory. The build chain will read the package (starting at its `src/index.ts` file) and create the following build artifacts in the package's `dist` directory: -* **`index.js`** +- **`index.js`** This file contains everything from `index.ts`, but as plain JavaScript and bundled in a single file (i.e. without `import`ing files from the same package). It is used by bundlers like [webpack](https://webpack.js.org/). -* **`index.cjs`** - Like `index.js`, but using the old CommonJS standard to support backwards-compatible tools like the [Jest](https://jestjs.io/) testing framework. +- **`index.cjs`** + Like `index.js`, but using the old CommonJS standard to support tools like the [Jest](https://jestjs.io/) testing framework. -* **`index.browser.js`** +- **`index.browser.js`** This file, like `index.js`, contains the entire package as plain JavaScript, but this time wrapped in a function so that it can be included directly by browsers using the ` - + diff --git a/examples/end-experiment.html b/examples/abort-experiment.html similarity index 90% rename from examples/end-experiment.html rename to examples/abort-experiment.html index df01ae3d17..9927a7a941 100644 --- a/examples/end-experiment.html +++ b/examples/abort-experiment.html @@ -4,7 +4,7 @@ - + diff --git a/examples/jspsych-audio-button-response.html b/examples/jspsych-audio-button-response.html index 8f99a5146d..ab49292486 100644 --- a/examples/jspsych-audio-button-response.html +++ b/examples/jspsych-audio-button-response.html @@ -43,7 +43,7 @@ stimulus: 'sound/speech_red.mp3', choices: ['#00ff00', '#0000ff', '#ff0000'], response_allowed_while_playing: false, - button_html: '
', + button_html: (choice) => `
`, prompt: "

Which color was said?

" }); diff --git a/examples/jspsych-canvas-button-response.html b/examples/jspsych-canvas-button-response.html index 08c109898e..9473de260d 100644 --- a/examples/jspsych-canvas-button-response.html +++ b/examples/jspsych-canvas-button-response.html @@ -3,7 +3,7 @@ - + - \ No newline at end of file + diff --git a/examples/jspsych-html-button-response.html b/examples/jspsych-html-button-response.html index e681fb5cb0..53ba572cf1 100644 --- a/examples/jspsych-html-button-response.html +++ b/examples/jspsych-html-button-response.html @@ -20,7 +20,25 @@ type: jsPsychHtmlButtonResponse, stimulus: '

GREEN

', choices: ['Green', 'Blue', 'Red'], - prompt: "

What color is this word?

" + button_layout: "flex", + prompt: "

What color is this word? (flex layout)

" + }); + + timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: '

GREEN

', + choices: ['Green', 'Blue', 'Red'], + button_layout: "grid", + prompt: "

What color is this word? (grid layout)

" + }); + + timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: '

GREEN

', + choices: ['Green', 'Blue', 'Red'], + button_layout: "grid", + grid_rows: 2, + prompt: "

What color is this word? (grid layout, two rows)

" }); timeline.push({ diff --git a/examples/jspsych-video-button-response.html b/examples/jspsych-video-button-response.html index 335eb0ec37..2439c1cdfb 100644 --- a/examples/jspsych-video-button-response.html +++ b/examples/jspsych-video-button-response.html @@ -5,7 +5,7 @@ - + diff --git a/mkdocs.yml b/mkdocs.yml index 8ec869e77a..e5495598c7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -141,6 +141,7 @@ nav: - Support: - 'Getting Help': 'support/support.md' - 'Migrating from 6.x to 7.x': 'support/migration-v7.md' + - 'Migrating from 7.x to 8.x': 'support/migration-v8.md' - About: - 'About jsPsych': 'about/about.md' - 'License': 'about/license.md' diff --git a/package-lock.json b/package-lock.json index 772b572513..288e9744d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,15 @@ "packages/*" ], "devDependencies": { - "@changesets/changelog-github": "^0.4.4", - "@changesets/cli": "^2.22.0", - "alias-hq": "github:bjoluc/alias-hq#tsconfig-parsing-quickfix", - "husky": "^8.0.1", + "@changesets/changelog-github": "^0.4.7", + "@changesets/cli": "^2.25.2", + "@jspsych/config": "^1.3.2", + "husky": "^8.0.2", "import-sort-style-module": "^6.0.0", - "jest": "*", - "lint-staged": "^12.4.1", - "prettier": "^2.6.2", + "lint-staged": "^13.0.3", + "prettier": "^2.7.1", "prettier-plugin-import-sort": "^0.0.7", - "turbo": "^1.2.9" + "turbo": "^1.6.3" }, "engines": { "node": ">=18.0.0", @@ -25,116 +24,52 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, - "node_modules/@babel/cli": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.0.tgz", - "integrity": "sha512-17E1oSkGk2IwNILM4jtfAvgjt+ohmpfBky8aLerUfYZhiPNg7ca+CRCxZn8QDxwNhV/upsc2VHBCqGFIR+iBfA==", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "commander": "^4.0.1", - "convert-source-map": "^2.0.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.2.0", - "make-dir": "^2.1.0", - "slash": "^2.0.0" - }, - "bin": { - "babel": "bin/babel.js", - "babel-external-helpers": "bin/babel-external-helpers.js" - }, - "engines": { - "node": ">=6.9.0" - }, - "optionalDependencies": { - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", - "chokidar": "^3.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/cli/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/@babel/cli/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/cli/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@babel/cli/node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -149,6 +84,11 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -158,49 +98,64 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", - "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dependencies": { - "@babel/types": "^7.23.3", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "peer": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -208,6 +163,14 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -216,19 +179,24 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", "semver": "^6.3.1" }, "engines": { @@ -247,11 +215,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "peer": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-annotate-as-pure": "^7.24.7", "regexpu-core": "^5.3.1", "semver": "^6.3.1" }, @@ -266,14 +235,16 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "peer": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "peer": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -286,68 +257,73 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", "dependencies": { - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -357,32 +333,33 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "peer": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -392,13 +369,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -408,105 +385,109 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "peer": true, "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -514,12 +495,29 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -529,13 +527,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -545,12 +544,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", - "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "peer": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -612,6 +612,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "peer": true, "engines": { "node": ">=6.9.0" }, @@ -656,6 +657,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -670,6 +672,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -681,6 +684,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, @@ -689,11 +693,11 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.23.3.tgz", - "integrity": "sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz", + "integrity": "sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -703,11 +707,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -717,11 +722,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -753,11 +759,11 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -836,6 +842,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -861,11 +868,11 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", - "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -878,6 +885,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -890,11 +898,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -904,13 +913,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.3.tgz", - "integrity": "sha512-59GsVNavGxAXCDDbakWSMJhajASb4kBCqDjqJsv+p5nKdbz7istmZ3HrX3L2LuiI80+zsOADCvooqQH3qGCucQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -921,13 +931,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "peer": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -937,11 +948,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -951,11 +963,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.3.tgz", - "integrity": "sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -965,12 +978,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "peer": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -980,12 +994,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.3.tgz", - "integrity": "sha512-PENDVxdr7ZxKPyi5Ffc0LjXdnJyrJxyqF5T5YjlVg4a0VFfQHW0r8iAtRiDXkfHlu1wwcvdtnndGYIeJLSuRMQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "peer": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -996,18 +1011,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz", - "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-split-export-declaration": "^7.22.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", "globals": "^11.1.0" }, "engines": { @@ -1018,12 +1033,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1033,11 +1049,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1047,12 +1064,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1062,11 +1080,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1076,11 +1095,12 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.3.tgz", - "integrity": "sha512-vTG+cTGxPFou12Rj7ll+eD5yWeNl5/8xvQvF08y5Gv3v4mZQoyFf8/n9zg4q5vvCWt5jmgymfzMAldO7orBn7A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1091,12 +1111,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "peer": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1106,11 +1127,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.3.tgz", - "integrity": "sha512-yCLhW34wpJWRdTxxWtFZASJisihrfyMOTOQexhVzA78jlU+dH7Dw+zQgcPepQ5F3C6bAIiblZZ+qBggJdHiBAg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -1121,12 +1143,12 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.23.3.tgz", - "integrity": "sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz", + "integrity": "sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-flow": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-flow": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1136,11 +1158,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", - "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1150,13 +1174,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "peer": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1166,11 +1191,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.3.tgz", - "integrity": "sha512-H9Ej2OiISIZowZHaBwF0tsJOih1PftXJtE8EWqlEIwpc7LMTGq0rPOrywKLQ4nefzx8/HMR0D3JGXoMHYvhi0A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -1181,11 +1207,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1195,11 +1222,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.3.tgz", - "integrity": "sha512-+pD5ZbxofyOygEp+zZAfujY2ShNCXRpDRIPOiBmTO693hhyOEteZgl876Xs9SAHPQpcV0vz8LvA/T+w8AzyX8A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -1210,11 +1238,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1224,12 +1253,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "peer": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1239,13 +1269,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1255,14 +1285,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", - "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "peer": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1272,12 +1303,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "peer": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1287,12 +1319,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1302,11 +1335,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1316,11 +1350,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.3.tgz", - "integrity": "sha512-xzg24Lnld4DYIdysyf07zJ1P+iIfJpxtVFOzX4g+bsJ3Ng5Le7rXx9KwqKzuyaUeRnt+I1EICwQITqc0E2PmpA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -1331,11 +1366,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.3.tgz", - "integrity": "sha512-s9GO7fIBi/BLsZ0v3Rftr6Oe4t0ctJ8h4CCXfPoEJwmvAPMyNrfkOOJzm6b9PX9YXcCJWWQd/sBF/N26eBiMVw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -1346,15 +1382,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.3.tgz", - "integrity": "sha512-VxHt0ANkDmu8TANdE9Kc0rndo/ccsmfe2Cx2y5sI4hu3AukHQ5wAu4cM7j3ba8B9548ijVyclBU+nuDQftZsog==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "peer": true, "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" + "@babel/plugin-transform-parameters": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1364,12 +1400,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1379,11 +1416,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.3.tgz", - "integrity": "sha512-LxYSb0iLjUamfm7f1D7GpiS4j0UAC8AOiehnsGAP8BEsIX8EOi3qV6bbctw8M7ZvLtcoZfZX5Z7rN9PlWk0m5A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -1394,12 +1432,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.3.tgz", - "integrity": "sha512-zvL8vIfIUgMccIAK1lxjvNv572JHFJIKb4MWBz5OGdBQA0fB0Xluix5rmOby48exiJc987neOmP/m9Fnpkz3Tg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -1410,11 +1449,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1424,12 +1464,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "peer": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1439,13 +1480,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.3.tgz", - "integrity": "sha512-a5m2oLNFyje2e/rGKjVfAELTVI5mbA0FeZpBnkOWWV7eSmKQ+T/XW0Vf+29ScLzSxX+rnsarvU0oie/4m6hkxA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -1456,11 +1498,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1470,11 +1513,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "regenerator-transform": "^0.15.2" }, "engines": { @@ -1485,11 +1529,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1499,11 +1544,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1513,12 +1559,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1528,11 +1575,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1542,11 +1590,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1556,11 +1605,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1570,14 +1620,14 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.3.tgz", - "integrity": "sha512-ogV0yWnq38CFwH20l2Afz0dfKuZBx9o/Y2Rmh5vuSS0YD1hswgEgTfyTzuSrT2q9btmHRSqYoSfwFUVaC1M1Jw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", + "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.23.3" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1587,11 +1637,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1601,12 +1652,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1616,12 +1668,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1631,12 +1684,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1646,25 +1700,27 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.3.tgz", - "integrity": "sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==", - "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", + "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -1676,58 +1732,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.3", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.3", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.3", - "@babel/plugin-transform-classes": "^7.23.3", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.3", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.3", - "@babel/plugin-transform-for-of": "^7.23.3", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.3", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.3", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.3", - "@babel/plugin-transform-numeric-separator": "^7.23.3", - "@babel/plugin-transform-object-rest-spread": "^7.23.3", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.3", - "@babel/plugin-transform-optional-chaining": "^7.23.3", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.3", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1742,18 +1798,19 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "peer": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/preset-flow": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.23.3.tgz", - "integrity": "sha512-7yn6hl8RIv+KNk6iIrGZ+D06VhVY35wLVf23Cz/mMu1zOr7u4MMP4j0nZ9tLf8+4ZFpnib8cFYgB/oYg9hfswA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.24.7.tgz", + "integrity": "sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-flow-strip-types": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-flow-strip-types": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1766,6 +1823,7 @@ "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", @@ -1776,15 +1834,15 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", - "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-typescript": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1794,14 +1852,14 @@ } }, "node_modules/@babel/register": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz", - "integrity": "sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.24.6.tgz", + "integrity": "sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==", "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", "make-dir": "^2.1.0", - "pirates": "^4.0.5", + "pirates": "^4.0.6", "source-map-support": "^0.5.16" }, "engines": { @@ -1823,14 +1881,6 @@ "node": ">=6" } }, - "node_modules/@babel/register/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/@babel/register/node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -1843,46 +1893,47 @@ "node_modules/@babel/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "peer": true }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", + "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", "dependencies": { - "regenerator-runtime": "^0.14.0" + "regenerator-runtime": "^0.13.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", - "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.3", - "@babel/types": "^7.23.3", - "debug": "^4.1.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1890,12 +1941,12 @@ } }, "node_modules/@babel/types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", - "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1908,16 +1959,16 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, "node_modules/@changesets/apply-release-plan": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-6.1.4.tgz", - "integrity": "sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-6.1.2.tgz", + "integrity": "sha512-H8TV9E/WtJsDfoDVbrDGPXmkZFSv7W2KLqp4xX4MKZXshb0hsQZUNowUa8pnus9qb/5OZrFFRVsUsDCVHNW/AQ==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", - "@changesets/config": "^2.3.1", + "@babel/runtime": "^7.10.4", + "@changesets/config": "^2.2.0", "@changesets/get-version-range-type": "^0.3.2", - "@changesets/git": "^2.0.0", - "@changesets/types": "^5.2.1", + "@changesets/git": "^1.5.0", + "@changesets/types": "^5.2.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", @@ -1925,66 +1976,66 @@ "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", - "semver": "^7.5.3" + "semver": "^5.4.1" } }, "node_modules/@changesets/assemble-release-plan": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-5.2.4.tgz", - "integrity": "sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-5.2.2.tgz", + "integrity": "sha512-B1qxErQd85AeZgZFZw2bDKyOfdXHhG+X5S+W3Da2yCem8l/pRy4G/S7iOpEcMwg6lH8q2ZhgbZZwZ817D+aLuQ==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", + "@babel/runtime": "^7.10.4", "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", - "@changesets/types": "^5.2.1", + "@changesets/get-dependents-graph": "^1.3.4", + "@changesets/types": "^5.2.0", "@manypkg/get-packages": "^1.1.3", - "semver": "^7.5.3" + "semver": "^5.4.1" } }, "node_modules/@changesets/changelog-git": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.1.14.tgz", - "integrity": "sha512-+vRfnKtXVWsDDxGctOfzJsPhaCdXRYoe+KyWYoq5X/GqoISREiat0l3L8B0a453B2B4dfHGcZaGyowHbp9BSaA==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.1.13.tgz", + "integrity": "sha512-zvJ50Q+EUALzeawAxax6nF2WIcSsC5PwbuLeWkckS8ulWnuPYx8Fn/Sjd3rF46OzeKA8t30loYYV6TIzp4DIdg==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1" + "@changesets/types": "^5.2.0" } }, "node_modules/@changesets/changelog-github": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.4.8.tgz", - "integrity": "sha512-jR1DHibkMAb5v/8ym77E4AMNWZKB5NPzw5a5Wtqm1JepAuIF+hrKp2u04NKM14oBZhHglkCfrla9uq8ORnK/dw==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.4.7.tgz", + "integrity": "sha512-UUG5sKwShs5ha1GFnayUpZNcDGWoY7F5XxhOEHS62sDPOtoHQZsG3j1nC5RxZ3M1URHA321cwVZHeXgu99Y3ew==", "dev": true, "dependencies": { - "@changesets/get-github-info": "^0.5.2", - "@changesets/types": "^5.2.1", + "@changesets/get-github-info": "^0.5.1", + "@changesets/types": "^5.2.0", "dotenv": "^8.1.0" } }, "node_modules/@changesets/cli": { - "version": "2.26.2", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.2.tgz", - "integrity": "sha512-dnWrJTmRR8bCHikJHl9b9HW3gXACCehz4OasrXpMp7sx97ECuBGGNjJhjPhdZNCvMy9mn4BWdplI323IbqsRig==", + "version": "2.25.2", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.25.2.tgz", + "integrity": "sha512-ACScBJXI3kRyMd2R8n8SzfttDHi4tmKSwVwXBazJOylQItSRSF4cGmej2E4FVf/eNfGy6THkL9GzAahU9ErZrA==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", - "@changesets/apply-release-plan": "^6.1.4", - "@changesets/assemble-release-plan": "^5.2.4", - "@changesets/changelog-git": "^0.1.14", - "@changesets/config": "^2.3.1", + "@babel/runtime": "^7.10.4", + "@changesets/apply-release-plan": "^6.1.2", + "@changesets/assemble-release-plan": "^5.2.2", + "@changesets/changelog-git": "^0.1.13", + "@changesets/config": "^2.2.0", "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", - "@changesets/get-release-plan": "^3.0.17", - "@changesets/git": "^2.0.0", + "@changesets/get-dependents-graph": "^1.3.4", + "@changesets/get-release-plan": "^3.0.15", + "@changesets/git": "^1.5.0", "@changesets/logger": "^0.0.5", - "@changesets/pre": "^1.0.14", - "@changesets/read": "^0.5.9", - "@changesets/types": "^5.2.1", - "@changesets/write": "^0.2.3", + "@changesets/pre": "^1.0.13", + "@changesets/read": "^0.5.8", + "@changesets/types": "^5.2.0", + "@changesets/write": "^0.2.2", "@manypkg/get-packages": "^1.1.3", "@types/is-ci": "^3.0.0", - "@types/semver": "^7.5.0", + "@types/semver": "^6.0.0", "ansi-colors": "^4.1.3", "chalk": "^2.1.0", "enquirer": "^2.3.0", @@ -1997,7 +2048,7 @@ "p-limit": "^2.2.0", "preferred-pm": "^3.0.0", "resolve-from": "^5.0.0", - "semver": "^7.5.3", + "semver": "^5.4.1", "spawndamnit": "^2.0.0", "term-size": "^2.1.0", "tty-table": "^4.1.5" @@ -2007,15 +2058,15 @@ } }, "node_modules/@changesets/config": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@changesets/config/-/config-2.3.1.tgz", - "integrity": "sha512-PQXaJl82CfIXddUOppj4zWu+987GCw2M+eQcOepxN5s+kvnsZOwjEJO3DH9eVy+OP6Pg/KFEWdsECFEYTtbg6w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-2.2.0.tgz", + "integrity": "sha512-GGaokp3nm5FEDk/Fv2PCRcQCOxGKKPRZ7prcMqxEr7VSsG75MnChQE8plaW1k6V8L2bJE+jZWiRm19LbnproOw==", "dev": true, "dependencies": { "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", + "@changesets/get-dependents-graph": "^1.3.4", "@changesets/logger": "^0.0.5", - "@changesets/types": "^5.2.1", + "@changesets/types": "^5.2.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.2" @@ -2031,22 +2082,22 @@ } }, "node_modules/@changesets/get-dependents-graph": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.3.6.tgz", - "integrity": "sha512-Q/sLgBANmkvUm09GgRsAvEtY3p1/5OCzgBE5vX3vgb5CvW0j7CEljocx5oPXeQSNph6FXulJlXV3Re/v3K3P3Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.3.4.tgz", + "integrity": "sha512-+C4AOrrFY146ydrgKOo5vTZfj7vetNu1tWshOID+UjPUU9afYGDXI8yLnAeib1ffeBXV3TuGVcyphKpJ3cKe+A==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1", + "@changesets/types": "^5.2.0", "@manypkg/get-packages": "^1.1.3", "chalk": "^2.1.0", "fs-extra": "^7.0.1", - "semver": "^7.5.3" + "semver": "^5.4.1" } }, "node_modules/@changesets/get-github-info": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.2.tgz", - "integrity": "sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.1.tgz", + "integrity": "sha512-w2yl3AuG+hFuEEmT6j1zDlg7GQLM/J2UxTmk0uJBMdRqHni4zXGe/vUlPfLom5KfX3cRfHc0hzGvloDPjWFNZw==", "dev": true, "dependencies": { "dataloader": "^1.4.0", @@ -2054,17 +2105,17 @@ } }, "node_modules/@changesets/get-release-plan": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-3.0.17.tgz", - "integrity": "sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-3.0.15.tgz", + "integrity": "sha512-W1tFwxE178/en+zSj/Nqbc3mvz88mcdqUMJhRzN1jDYqN3QI4ifVaRF9mcWUU+KI0gyYEtYR65tour690PqTcA==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", - "@changesets/assemble-release-plan": "^5.2.4", - "@changesets/config": "^2.3.1", - "@changesets/pre": "^1.0.14", - "@changesets/read": "^0.5.9", - "@changesets/types": "^5.2.1", + "@babel/runtime": "^7.10.4", + "@changesets/assemble-release-plan": "^5.2.2", + "@changesets/config": "^2.2.0", + "@changesets/pre": "^1.0.13", + "@changesets/read": "^0.5.8", + "@changesets/types": "^5.2.0", "@manypkg/get-packages": "^1.1.3" } }, @@ -2075,17 +2126,16 @@ "dev": true }, "node_modules/@changesets/git": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@changesets/git/-/git-2.0.0.tgz", - "integrity": "sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-1.5.0.tgz", + "integrity": "sha512-Xo8AT2G7rQJSwV87c8PwMm6BAc98BnufRMsML7m7Iw8Or18WFvFmxqG5aOL5PBvhgq9KrKvaeIBNIymracSuHg==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", + "@babel/runtime": "^7.10.4", "@changesets/errors": "^0.1.4", - "@changesets/types": "^5.2.1", + "@changesets/types": "^5.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", - "micromatch": "^4.0.2", "spawndamnit": "^2.0.0" } }, @@ -2099,58 +2149,58 @@ } }, "node_modules/@changesets/parse": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.16.tgz", - "integrity": "sha512-127JKNd167ayAuBjUggZBkmDS5fIKsthnr9jr6bdnuUljroiERW7FBTDNnNVyJ4l69PzR57pk6mXQdtJyBCJKg==", + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.15.tgz", + "integrity": "sha512-3eDVqVuBtp63i+BxEWHPFj2P1s3syk0PTrk2d94W9JD30iG+OER0Y6n65TeLlY8T2yB9Fvj6Ev5Gg0+cKe/ZUA==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1", + "@changesets/types": "^5.2.0", "js-yaml": "^3.13.1" } }, "node_modules/@changesets/pre": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.14.tgz", - "integrity": "sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.13.tgz", + "integrity": "sha512-jrZc766+kGZHDukjKhpBXhBJjVQMied4Fu076y9guY1D3H622NOw8AQaLV3oQsDtKBTrT2AUFjt9Z2Y9Qx+GfA==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", + "@babel/runtime": "^7.10.4", "@changesets/errors": "^0.1.4", - "@changesets/types": "^5.2.1", + "@changesets/types": "^5.2.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "node_modules/@changesets/read": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.5.9.tgz", - "integrity": "sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==", + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.5.8.tgz", + "integrity": "sha512-eYaNfxemgX7f7ELC58e7yqQICW5FB7V+bd1lKt7g57mxUrTveYME+JPaBPpYx02nP53XI6CQp6YxnR9NfmFPKw==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", - "@changesets/git": "^2.0.0", + "@babel/runtime": "^7.10.4", + "@changesets/git": "^1.5.0", "@changesets/logger": "^0.0.5", - "@changesets/parse": "^0.3.16", - "@changesets/types": "^5.2.1", + "@changesets/parse": "^0.3.15", + "@changesets/types": "^5.2.0", "chalk": "^2.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0" } }, "node_modules/@changesets/types": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.1.tgz", - "integrity": "sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.0.tgz", + "integrity": "sha512-km/66KOqJC+eicZXsm2oq8A8bVTSpkZJ60iPV/Nl5Z5c7p9kk8xxh6XGRTlnludHldxOOfudhnDN2qPxtHmXzA==", "dev": true }, "node_modules/@changesets/write": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.2.3.tgz", - "integrity": "sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.2.2.tgz", + "integrity": "sha512-kCYNHyF3xaId1Q/QE+DF3UTrHTyg3Cj/f++T8S8/EkC+jh1uK2LFnM9h+EzV+fsmnZDrs7r0J4LLpeI/VWC5Hg==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", - "@changesets/types": "^5.2.1", + "@babel/runtime": "^7.10.4", + "@changesets/types": "^5.2.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", "prettier": "^2.7.1" @@ -2165,6 +2215,36 @@ "node": ">=10.0.0" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.14.tgz", + "integrity": "sha512-+Rb20XXxRGisNu2WmNKk+scpanb7nL5yhuI1KR9wQFiC43ddPj/V1fmNyzlFC9bKiG4mYzxW7egtoHVcynr+OA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.14.tgz", + "integrity": "sha512-eQi9rosGNVQFJyJWV0HCA5WZae/qWIQME7s8/j8DMvnylfBv62Pbu+zJ2eUDqNf2O4u3WB+OEXyfkpBoe194sg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@fontsource/open-sans": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.3.tgz", @@ -2581,6 +2661,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/source-map/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/@jest/test-result": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", @@ -2679,6 +2767,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/@jest/transform/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2779,30 +2872,29 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -2811,20 +2903,35 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3123,9 +3230,9 @@ } }, "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", @@ -3163,11 +3270,16 @@ "semver": "bin/semver.js" } }, - "node_modules/@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "optional": true + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -3204,31 +3316,6 @@ "node": ">= 8" } }, - "node_modules/@rollup/plugin-babel": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", - "integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@rollup/pluginutils": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - }, - "rollup": { - "optional": true - } - } - }, "node_modules/@rollup/plugin-commonjs": { "version": "25.0.7", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", @@ -3262,9 +3349,9 @@ } }, "node_modules/@rollup/plugin-commonjs/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3279,34 +3366,23 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/@rollup/plugin-commonjs/node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "@jridgewell/sourcemap-codec": "^1.4.15" } }, - "node_modules/@rollup/plugin-json": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.1.tgz", - "integrity": "sha512-RgVfl5hWMkxN1h/uZj8FVESvPuBJ/uf6ly6GTj0GONnkfoBN5KC0MSz+PN2OLDgYXMhtG0mWpTrkiOjoxAIevw==", + "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "dependencies": { - "@rollup/pluginutils": "^5.0.1" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">=10" } }, "node_modules/@rollup/plugin-node-resolve": { @@ -3333,61 +3409,20 @@ } } }, - "node_modules/@rollup/plugin-replace": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz", - "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==", + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", - "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -3545,9 +3580,9 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dependencies": { "type-detect": "4.0.8" } @@ -3578,9 +3613,9 @@ } }, "node_modules/@types/babel__core": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz", - "integrity": "sha512-mLnSC22IC4vcWiuObSRjrLd9XcBTGf59vUSoq2jkQDJ/QQ8PMI9rSuzE+aEV8karUMbskw07bKYoUJCKTUaygg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -3590,9 +3625,9 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.7", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", - "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dependencies": { "@babel/types": "^7.0.0" } @@ -3607,23 +3642,23 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", - "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/dom-mediacapture-record": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.19.tgz", - "integrity": "sha512-Cz/85z3YTuUPnXrOp5MvSZZSgDkWTWvj1HgE7MWc5C8d/w/soJBXjnAoYDl4P5gmenDNNZkhXzNylGqxS1FzOw==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.13.tgz", + "integrity": "sha512-awMC2n4iRwJ6VWpQDA7dPqJhImeXHsVeEoQKWkdGB2cau+h/6erGoVre5Dk8p/DL1LIiLUcK6MnC3mibiKRRFg==", "dev": true }, "node_modules/@types/eslint": { - "version": "8.44.7", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", - "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", + "version": "8.4.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", + "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -3631,9 +3666,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", "dev": true, "dependencies": { "@types/eslint": "*", @@ -3641,23 +3676,31 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" }, "node_modules/@types/expect": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==" }, + "node_modules/@types/glob": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.0.0.tgz", + "integrity": "sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/glob-stream": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@types/glob-stream/-/glob-stream-8.0.2.tgz", - "integrity": "sha512-kyuRfGE+yiSJWzSO3t74rXxdZNdYfLcllO0IUha4eX1fl40pm9L02Q/TEc3mykTLjoWz4STBNwYnUWdFu3I0DA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/glob-stream/-/glob-stream-6.1.1.tgz", + "integrity": "sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==", "dependencies": { - "@types/node": "*", - "@types/picomatch": "*", - "@types/streamx": "*" + "@types/glob": "*", + "@types/node": "*" } }, "node_modules/@types/graceful-fs": { @@ -3680,31 +3723,31 @@ } }, "node_modules/@types/is-ci": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.4.tgz", - "integrity": "sha512-AkCYCmwlXeuH89DagDCzvCAyltI2v9lh3U3DqSg/GrBYoReAaWwxfXCqMx9UV5MajLZ4ZFwZzV4cABGIxk2XRw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.0.tgz", + "integrity": "sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==", "dev": true, "dependencies": { "ci-info": "^3.1.0" } }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" }, "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", "dependencies": { "@types/istanbul-lib-report": "*" } @@ -3729,15 +3772,20 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" + }, "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, "node_modules/@types/node": { @@ -3746,20 +3794,15 @@ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, - "node_modules/@types/picomatch": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.3.tgz", - "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==" - }, "node_modules/@types/resize-observer-browser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.10.tgz", - "integrity": "sha512-pLLF6KJzPPKqJI8rJSTwsesjGKLLHlBSFDEPadYwILI4l7fwEL0juPlFT+exg7DHWxnEMG31t2yyZHDtqJrblA==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz", + "integrity": "sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg==", "dev": true }, "node_modules/@types/resolve": { @@ -3768,33 +3811,25 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" }, "node_modules/@types/semver": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", - "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.3.tgz", + "integrity": "sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==", "dev": true }, "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" - }, - "node_modules/@types/streamx": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@types/streamx/-/streamx-2.9.4.tgz", - "integrity": "sha512-0M4RKl0MJnST4TtMTg/gcoeQMniMyZW4x+FpI78X2/ksdxC99P9tUgk8K56McWXMrptZ3/A+c39lSIEGrXJ3Rw==", - "dependencies": { - "@types/node": "*" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" }, "node_modules/@types/undertaker": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@types/undertaker/-/undertaker-1.2.11.tgz", - "integrity": "sha512-j1Z0V2ByRHr8ZK7eOeGq0LGkkdthNFW0uAZGY22iRkNQNL9/vAV0yFPr1QN3FM/peY5bxs9P+1f0PYJTQVa5iA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@types/undertaker/-/undertaker-1.2.8.tgz", + "integrity": "sha512-gW3PRqCHYpo45XFQHJBhch7L6hytPsIe0QeLujlnFsjHPnXLhJcPdN6a9368d7aIQgH2I/dUTPFBlGeSNA3qOg==", "dependencies": { "@types/node": "*", "@types/undertaker-registry": "*", @@ -3802,23 +3837,23 @@ } }, "node_modules/@types/undertaker-registry": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/undertaker-registry/-/undertaker-registry-1.0.4.tgz", - "integrity": "sha512-tW77pHh2TU4uebWXWeEM5laiw8BuJ7pyJYDh6xenOs75nhny2kVgwYbegJ4BoLMYsIrXaBpKYaPdYO3/udG+hg==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ==" }, "node_modules/@types/vinyl": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.10.tgz", - "integrity": "sha512-DqN5BjCrmjAtZ1apqzcq2vk2PSW0m1nFfjIafBFkAyddmHxuw3ZAK3omLiSdpuu81+8h07i6U4DtaE38Xsf2xQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", + "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", "dependencies": { "@types/expect": "^1.20.4", "@types/node": "*" } }, "node_modules/@types/vinyl-fs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/vinyl-fs/-/vinyl-fs-3.0.5.tgz", - "integrity": "sha512-ckYz9giHgV6U10RFuf9WsDQ3X86EFougapxHmmoxLK7e6ICQqO8CE+4V/3lBN148V5N1pb4nQMmMjyScleVsig==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@types/vinyl-fs/-/vinyl-fs-2.4.12.tgz", + "integrity": "sha512-LgBpYIWuuGsihnlF+OOWWz4ovwCYlT03gd3DuLwex50cYZLmX3yrW+sFF9ndtmh7zcZpS6Ri47PrIu+fV+sbXw==", "dependencies": { "@types/glob-stream": "*", "@types/node": "*", @@ -3826,17 +3861,17 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.31", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz", - "integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==", + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", @@ -4043,9 +4078,9 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "bin": { "acorn": "bin/acorn" }, @@ -4072,9 +4107,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "engines": { "node": ">=0.4.0" } @@ -4104,9 +4139,9 @@ } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", @@ -4149,10 +4184,9 @@ } }, "node_modules/alias-hq": { - "version": "6.2.2", - "resolved": "git+ssh://git@github.com/bjoluc/alias-hq.git#6725a77da2569b9c19b463a9af43d4013469143c", - "dev": true, - "license": "MIT", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/alias-hq/-/alias-hq-6.2.3.tgz", + "integrity": "sha512-yaz3BvKpWphfcMBf3fkf2vE1Ln1ELB+m1mUsp1SkXZD1Q+cKpU0B3Agsws9mFO6YrGqUqOBAVbyUELhYaxAsZA==", "dependencies": { "colors": "^1.4.0", "get-tsconfig": "^4.7.0", @@ -4231,9 +4265,9 @@ } }, "node_modules/ansis": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-1.5.6.tgz", - "integrity": "sha512-vKn0k5V0oiaOClcUMNDFb7DvI3+asAozhg1wI8bLkYxV0lhfPtIuwjUa1wG9/YaUY/KwO9U2B7m0FMqzn/jXUQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-1.5.5.tgz", + "integrity": "sha512-DNctovTacxs/NfZpGo6bIGWgLd2oZsDO7RJbiYX6Ttj40LPZM1XKv9WtesH13ieOEm1GajjD+Vik2n9YnSTPdA==", "dev": true, "engines": { "node": ">=12.13" @@ -4249,9 +4283,9 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -4294,9 +4328,9 @@ } }, "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4360,19 +4394,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", @@ -4467,14 +4488,14 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -4484,27 +4505,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -4557,15 +4557,9 @@ } }, "node_modules/async-each": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", - "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" }, "node_modules/async-settle": { "version": "1.0.0", @@ -4605,17 +4599,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", @@ -4625,41 +4612,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/babel-helper-evaluate-path": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-helper-evaluate-path/-/babel-helper-evaluate-path-0.5.0.tgz", - "integrity": "sha512-mUh0UhS607bGh5wUMAQfOpt2JX2ThXMtppHRdRU1kL7ZLRWIXxoV2UIV1r2cAeeNeU1M5SB5/RSUgUxrK8yOkA==" - }, - "node_modules/babel-helper-flip-expressions": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-helper-flip-expressions/-/babel-helper-flip-expressions-0.4.3.tgz", - "integrity": "sha512-rSrkRW4YQ2ETCWww9gbsWk4N0x1BOtln349Tk0dlCS90oT68WMLyGR7WvaMp3eAnsVrCqdUtC19lo1avyGPejA==" - }, - "node_modules/babel-helper-is-nodes-equiv": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/babel-helper-is-nodes-equiv/-/babel-helper-is-nodes-equiv-0.0.1.tgz", - "integrity": "sha512-ri/nsMFVRqXn7IyT5qW4/hIAGQxuYUFHa3qsxmPtbk6spZQcYlyDogfVpNm2XYOslH/ULS4VEJGUqQX5u7ACQw==" - }, - "node_modules/babel-helper-is-void-0": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-helper-is-void-0/-/babel-helper-is-void-0-0.4.3.tgz", - "integrity": "sha512-07rBV0xPRM3TM5NVJEOQEkECX3qnHDjaIbFvWYPv+T1ajpUiVLiqTfC+MmiZxY5KOL/Ec08vJdJD9kZiP9UkUg==" - }, - "node_modules/babel-helper-mark-eval-scopes": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-helper-mark-eval-scopes/-/babel-helper-mark-eval-scopes-0.4.3.tgz", - "integrity": "sha512-+d/mXPP33bhgHkdVOiPkmYoeXJ+rXRWi7OdhwpyseIqOS8CmzHQXHUp/+/Qr8baXsT0kjGpMHHofHs6C3cskdA==" - }, - "node_modules/babel-helper-remove-or-void": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-helper-remove-or-void/-/babel-helper-remove-or-void-0.4.3.tgz", - "integrity": "sha512-eYNceYtcGKpifHDir62gHJadVXdg9fAhuZEXiRQnJJ4Yi4oUTpqpNY//1pM4nVyjjDMPYaC2xSf0I+9IqVzwdA==" - }, - "node_modules/babel-helper-to-multiple-sequence-expressions": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz", - "integrity": "sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA==" - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4796,96 +4748,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/babel-plugin-minify-builtins": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.5.0.tgz", - "integrity": "sha512-wpqbN7Ov5hsNwGdzuzvFcjgRlzbIeVv1gMIlICbPj0xkexnfoIDe7q+AZHMkQmAE/F9R5jkrB6TLfTegImlXag==" - }, - "node_modules/babel-plugin-minify-constant-folding": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-constant-folding/-/babel-plugin-minify-constant-folding-0.5.0.tgz", - "integrity": "sha512-Vj97CTn/lE9hR1D+jKUeHfNy+m1baNiJ1wJvoGyOBUx7F7kJqDZxr9nCHjO/Ad+irbR3HzR6jABpSSA29QsrXQ==", - "dependencies": { - "babel-helper-evaluate-path": "^0.5.0" - } - }, - "node_modules/babel-plugin-minify-dead-code-elimination": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.5.2.tgz", - "integrity": "sha512-krq9Lwi0QIzyAlcNBXTL4usqUvevB4BzktdEsb8srcXC1AaYqRJiAQw6vdKdJSaXbz6snBvziGr6ch/aoRCfpA==", - "dependencies": { - "babel-helper-evaluate-path": "^0.5.0", - "babel-helper-mark-eval-scopes": "^0.4.3", - "babel-helper-remove-or-void": "^0.4.3", - "lodash": "^4.17.11" - } - }, - "node_modules/babel-plugin-minify-flip-comparisons": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-flip-comparisons/-/babel-plugin-minify-flip-comparisons-0.4.3.tgz", - "integrity": "sha512-8hNwgLVeJzpeLVOVArag2DfTkbKodzOHU7+gAZ8mGBFGPQHK6uXVpg3jh5I/F6gfi5Q5usWU2OKcstn1YbAV7A==", - "dependencies": { - "babel-helper-is-void-0": "^0.4.3" - } - }, - "node_modules/babel-plugin-minify-guarded-expressions": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-guarded-expressions/-/babel-plugin-minify-guarded-expressions-0.4.4.tgz", - "integrity": "sha512-RMv0tM72YuPPfLT9QLr3ix9nwUIq+sHT6z8Iu3sLbqldzC1Dls8DPCywzUIzkTx9Zh1hWX4q/m9BPoPed9GOfA==", - "dependencies": { - "babel-helper-evaluate-path": "^0.5.0", - "babel-helper-flip-expressions": "^0.4.3" - } - }, - "node_modules/babel-plugin-minify-infinity": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-infinity/-/babel-plugin-minify-infinity-0.4.3.tgz", - "integrity": "sha512-X0ictxCk8y+NvIf+bZ1HJPbVZKMlPku3lgYxPmIp62Dp8wdtbMLSekczty3MzvUOlrk5xzWYpBpQprXUjDRyMA==" - }, - "node_modules/babel-plugin-minify-mangle-names": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-mangle-names/-/babel-plugin-minify-mangle-names-0.5.1.tgz", - "integrity": "sha512-8KMichAOae2FHlipjNDTo2wz97MdEb2Q0jrn4NIRXzHH7SJ3c5TaNNBkeTHbk9WUsMnqpNUx949ugM9NFWewzw==", - "dependencies": { - "babel-helper-mark-eval-scopes": "^0.4.3" - } - }, - "node_modules/babel-plugin-minify-numeric-literals": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-numeric-literals/-/babel-plugin-minify-numeric-literals-0.4.3.tgz", - "integrity": "sha512-5D54hvs9YVuCknfWywq0eaYDt7qYxlNwCqW9Ipm/kYeS9gYhJd0Rr/Pm2WhHKJ8DC6aIlDdqSBODSthabLSX3A==" - }, - "node_modules/babel-plugin-minify-replace": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-replace/-/babel-plugin-minify-replace-0.5.0.tgz", - "integrity": "sha512-aXZiaqWDNUbyNNNpWs/8NyST+oU7QTpK7J9zFEFSA0eOmtUNMU3fczlTTTlnCxHmq/jYNFEmkkSG3DDBtW3Y4Q==" - }, - "node_modules/babel-plugin-minify-simplify": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-simplify/-/babel-plugin-minify-simplify-0.5.1.tgz", - "integrity": "sha512-OSYDSnoCxP2cYDMk9gxNAed6uJDiDz65zgL6h8d3tm8qXIagWGMLWhqysT6DY3Vs7Fgq7YUDcjOomhVUb+xX6A==", - "dependencies": { - "babel-helper-evaluate-path": "^0.5.0", - "babel-helper-flip-expressions": "^0.4.3", - "babel-helper-is-nodes-equiv": "^0.0.1", - "babel-helper-to-multiple-sequence-expressions": "^0.5.0" - } - }, - "node_modules/babel-plugin-minify-type-constructors": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-type-constructors/-/babel-plugin-minify-type-constructors-0.4.3.tgz", - "integrity": "sha512-4ADB0irJ/6BeXWHubjCJmrPbzhxDgjphBMjIjxCc25n4NGJ00NsYqwYt+F/OvE9RXx8KaSW7cJvp+iZX436tnQ==", - "dependencies": { - "babel-helper-is-void-0": "^0.4.3" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "peer": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.6.2", "semver": "^6.3.1" }, "peerDependencies": { @@ -4896,94 +4766,36 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "peer": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "peer": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", - "core-js-compat": "^3.33.1" + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "peer": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.6.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-transform-inline-consecutive-adds": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz", - "integrity": "sha512-8D104wbzzI5RlxeVPYeQb9QsUyepiH1rAO5hpPpQ6NPRgQLpIVwkS/Nbx944pm4K8Z+rx7CgjPsFACz/VCBN0Q==" - }, - "node_modules/babel-plugin-transform-member-expression-literals": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.9.4.tgz", - "integrity": "sha512-Xq9/Rarpj+bjOZSl1nBbZYETsNEDDJSrb6Plb1sS3/36FukWFLLRysgecva5KZECjUJTrJoQqjJgtWToaflk5Q==" - }, - "node_modules/babel-plugin-transform-merge-sibling-variables": { - "version": "6.9.5", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.9.5.tgz", - "integrity": "sha512-xj/KrWi6/uP+DrD844h66Qh2cZN++iugEIgH8QcIxhmZZPNP6VpOE9b4gP2FFW39xDAY43kCmYMM6U0QNKN8fw==" - }, - "node_modules/babel-plugin-transform-minify-booleans": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-minify-booleans/-/babel-plugin-transform-minify-booleans-6.9.4.tgz", - "integrity": "sha512-9pW9ePng6DZpzGPalcrULuhSCcauGAbn8AeU3bE34HcDkGm8Ldt0ysjGkyb64f0K3T5ilV4mriayOVv5fg0ASA==" - }, - "node_modules/babel-plugin-transform-property-literals": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.9.4.tgz", - "integrity": "sha512-Pf8JHTjTPxecqVyL6KSwD/hxGpoTZjiEgV7nCx0KFQsJYM0nuuoCajbg09KRmZWeZbJ5NGTySABYv8b/hY1eEA==", - "dependencies": { - "esutils": "^2.0.2" - } - }, - "node_modules/babel-plugin-transform-regexp-constructors": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.4.3.tgz", - "integrity": "sha512-JjymDyEyRNhAoNFp09y/xGwYVYzT2nWTGrBrWaL6eCg2m+B24qH2jR0AA8V8GzKJTgC8NW6joJmc6nabvWBD/g==" - }, - "node_modules/babel-plugin-transform-remove-console": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz", - "integrity": "sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg==" - }, - "node_modules/babel-plugin-transform-remove-debugger": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.9.4.tgz", - "integrity": "sha512-Kd+eTBYlXfwoFzisburVwrngsrz4xh9I0ppoJnU/qlLysxVBRgI4Pj+dk3X8F5tDiehp3hhP8oarRMT9v2Z3lw==" - }, - "node_modules/babel-plugin-transform-remove-undefined": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-undefined/-/babel-plugin-transform-remove-undefined-0.5.0.tgz", - "integrity": "sha512-+M7fJYFaEE/M9CXa0/IRkDbiV3wRELzA1kKQFCJ4ifhrzLKn/9VCCgj9OFmYWwBd8IB48YdgPkHYtbYq+4vtHQ==", - "dependencies": { - "babel-helper-evaluate-path": "^0.5.0" - } - }, - "node_modules/babel-plugin-transform-simplify-comparison-operators": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.9.4.tgz", - "integrity": "sha512-GLInxhGAQWJ9YIdjwF6dAFlmh4U+kN8pL6Big7nkDzHoZcaDQOtBm28atEhQJq6m9GpAovbiGEShKqXv4BSp0A==" - }, - "node_modules/babel-plugin-transform-undefined-to-void": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz", - "integrity": "sha512-D2UbwxawEY1xVc9svYAUZQM2xarwSNXue2qDIx6CeV2EuMGaes/0su78zlIDIAgE7BvnMw4UpmSo9fDy+znghg==" - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -5021,36 +4833,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/babel-preset-minify": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-preset-minify/-/babel-preset-minify-0.5.2.tgz", - "integrity": "sha512-v4GL+kk0TfovbRIKZnC3HPbu2cAGmPAby7BsOmuPdMJfHV+4FVdsGXTH/OOGQRKYdjemBuL1+MsE6mobobhe9w==", - "dependencies": { - "babel-plugin-minify-builtins": "^0.5.0", - "babel-plugin-minify-constant-folding": "^0.5.0", - "babel-plugin-minify-dead-code-elimination": "^0.5.2", - "babel-plugin-minify-flip-comparisons": "^0.4.3", - "babel-plugin-minify-guarded-expressions": "^0.4.4", - "babel-plugin-minify-infinity": "^0.4.3", - "babel-plugin-minify-mangle-names": "^0.5.1", - "babel-plugin-minify-numeric-literals": "^0.4.3", - "babel-plugin-minify-replace": "^0.5.0", - "babel-plugin-minify-simplify": "^0.5.1", - "babel-plugin-minify-type-constructors": "^0.4.3", - "babel-plugin-transform-inline-consecutive-adds": "^0.4.3", - "babel-plugin-transform-member-expression-literals": "^6.9.4", - "babel-plugin-transform-merge-sibling-variables": "^6.9.5", - "babel-plugin-transform-minify-booleans": "^6.9.4", - "babel-plugin-transform-property-literals": "^6.9.4", - "babel-plugin-transform-regexp-constructors": "^0.4.3", - "babel-plugin-transform-remove-console": "^6.9.4", - "babel-plugin-transform-remove-debugger": "^6.9.4", - "babel-plugin-transform-remove-undefined": "^0.5.0", - "babel-plugin-transform-simplify-comparison-operators": "^6.9.4", - "babel-plugin-transform-undefined-to-void": "^6.9.4", - "lodash": "^4.17.11" - } - }, "node_modules/bach": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", @@ -5075,6 +4857,12 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "optional": true + }, "node_modules/base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", @@ -5176,29 +4964,29 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/breakword": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.6.tgz", - "integrity": "sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.5.tgz", + "integrity": "sha512-ex5W9DoOQ/LUEU3PMdLs9ua/CYZl1678NUkKOdUSi8Aw5F1idieaiRURCBFJCwVcrD1J8Iy3vfWSloaMwO2qFg==", "dev": true, "dependencies": { "wcwidth": "^1.0.1" } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -5214,9 +5002,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -5289,13 +5077,12 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5313,15 +5100,6 @@ "node": ">=4" } }, - "node_modules/caller-callsite/node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/caller-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", @@ -5335,11 +5113,12 @@ } }, "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "dev": true, "engines": { - "node": ">=6" + "node": ">=4" } }, "node_modules/camelcase": { @@ -5368,9 +5147,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "version": "1.0.30001629", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", + "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==", "funding": [ { "type": "opencollective", @@ -5470,23 +5249,17 @@ } }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", + "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", "engines": { "node": ">=8" } }, "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" }, "node_modules/class-utils": { "version": "0.3.6", @@ -5513,102 +5286,105 @@ "node": ">=0.10.0" } }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "node_modules/class-utils/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "kind-of": "^3.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "node_modules/class-utils/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", "dependencies": { - "restore-cursor": "^3.1.0" + "kind-of": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" + "is-buffer": "^1.1.5" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "node_modules/class-utils/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" }, "engines": { - "node": ">=12" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-width": { @@ -5620,16 +5396,68 @@ } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dependencies": { + "number-is-nan": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">=0.10.0" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/clone": { @@ -5661,6 +5489,17 @@ "node": ">=6" } }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/clone-stats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", @@ -5745,9 +5584,9 @@ } }, "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", "dev": true }, "node_modules/colors": { @@ -5770,9 +5609,9 @@ } }, "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", + "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", "dev": true, "engines": { "node": "^12.20.0 || >=14" @@ -5813,9 +5652,9 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/copy-descriptor": { "version": "0.1.1", @@ -5834,20 +5673,13 @@ "is-plain-object": "^5.0.0" } }, - "node_modules/copy-props/node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/core-js-compat": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.2.tgz", - "integrity": "sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw==", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "peer": true, "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.23.0" }, "funding": { "type": "opencollective", @@ -5874,19 +5706,6 @@ "node": ">=4" } }, - "node_modules/cosmiconfig/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -5972,12 +5791,12 @@ } }, "node_modules/cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", "dev": true, "dependencies": { - "node-fetch": "^2.6.12" + "node-fetch": "2.6.7" } }, "node_modules/cross-spawn": { @@ -5994,15 +5813,15 @@ } }, "node_modules/css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz", + "integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", + "postcss": "^8.4.18", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-local-by-default": "^4.0.0", "postcss-modules-scope": "^3.0.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", @@ -6019,6 +5838,18 @@ "webpack": "^5.0.0" } }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -6191,9 +6022,9 @@ } }, "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", + "integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==" }, "node_modules/decode-uri-component": { "version": "0.2.2", @@ -6215,9 +6046,9 @@ } }, "node_modules/dedent": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", - "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -6227,10 +6058,15 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "engines": { "node": ">=0.10.0" } @@ -6283,25 +6119,11 @@ "node": ">=0.8" } }, - "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", "dependencies": { - "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -6360,19 +6182,20 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "engines": { "node": ">=8" } }, "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/diff-sequences": { @@ -6435,6 +6258,17 @@ "object.defaults": "^1.1.0" } }, + "node_modules/each-props/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6453,9 +6287,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.580", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.580.tgz", - "integrity": "sha512-T5q3pjQon853xxxHUq3ZP68ZpvJHuSMY2+BZaW3QzjS4HvNuvsMmZ/+lU+nCrftre1jFZ+OSlExynXWBihnXzw==" + "version": "1.4.795", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.795.tgz", + "integrity": "sha512-hHo4lK/8wb4NUa+NJYSFyJ0xedNHiR6ylilDtb8NUW9d4dmBFmGiecYEKCEbti1wTNzbKXLfl4hPWEkAFbHYlw==" }, "node_modules/emittery": { "version": "0.13.1", @@ -6469,9 +6303,10 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/emojis-list": { "version": "3.0.0", @@ -6504,22 +6339,21 @@ } }, "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" + "ansi-colors": "^4.1.1" }, "engines": { "node": ">=8.6" } }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", "engines": { "node": ">=0.12" }, @@ -6528,9 +6362,9 @@ } }, "node_modules/envinfo": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", - "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -6548,50 +6382,35 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "has": "^1.0.3", "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "internal-slot": "^1.0.3", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "object-inspect": "^1.12.2", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", + "regexp.prototype.flags": "^1.4.3", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6601,32 +6420,17 @@ } }, "node_modules/es-module-lexer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.0.tgz", - "integrity": "sha512-lcCr3v3OLezdfFyx9r5NRYHOUTQNnFEQ9E87Mx8Kc+iqyJNkO7MJoB4GQRTlIMw9kLLTwGw0OAkm4BQQud/d9g==", - "dev": true - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==" }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "has": "^1.0.3" } }, "node_modules/es-to-primitive": { @@ -6647,13 +6451,14 @@ } }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -6690,43 +6495,380 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, + "node_modules/esbuild": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.14.tgz", + "integrity": "sha512-pJN8j42fvWLFWwSMG4luuupl2Me7mxciUOsMegKvwCmhEbJ2covUdFnihxm0FMIBV+cbwbtMoHgMCCI+pj1btQ==", + "hasInstallScript": true, "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=6.0" + "node": ">=12" }, "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/eslint-scope": { + "@esbuild/android-arm": "0.15.14", + "@esbuild/linux-loong64": "0.15.14", + "esbuild-android-64": "0.15.14", + "esbuild-android-arm64": "0.15.14", + "esbuild-darwin-64": "0.15.14", + "esbuild-darwin-arm64": "0.15.14", + "esbuild-freebsd-64": "0.15.14", + "esbuild-freebsd-arm64": "0.15.14", + "esbuild-linux-32": "0.15.14", + "esbuild-linux-64": "0.15.14", + "esbuild-linux-arm": "0.15.14", + "esbuild-linux-arm64": "0.15.14", + "esbuild-linux-mips64le": "0.15.14", + "esbuild-linux-ppc64le": "0.15.14", + "esbuild-linux-riscv64": "0.15.14", + "esbuild-linux-s390x": "0.15.14", + "esbuild-netbsd-64": "0.15.14", + "esbuild-openbsd-64": "0.15.14", + "esbuild-sunos-64": "0.15.14", + "esbuild-windows-32": "0.15.14", + "esbuild-windows-64": "0.15.14", + "esbuild-windows-arm64": "0.15.14" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.14.tgz", + "integrity": "sha512-HuilVIb4rk9abT4U6bcFdU35UHOzcWVGLSjEmC58OVr96q5UiRqzDtWjPlCMugjhgUGKEs8Zf4ueIvYbOStbIg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.14.tgz", + "integrity": "sha512-/QnxRVxsR2Vtf3XottAHj7hENAMW2wCs6S+OZcAbc/8nlhbAL/bCQRCVD78VtI5mdwqWkVi3wMqM94kScQCgqg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.14.tgz", + "integrity": "sha512-ToNuf1uifu8hhwWvoZJGCdLIX/1zpo8cOGnT0XAhDQXiKOKYaotVNx7pOVB1f+wHoWwTLInrOmh3EmA7Fd+8Vg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.14.tgz", + "integrity": "sha512-KgGP+y77GszfYJgceO0Wi/PiRtYo5y2Xo9rhBUpxTPaBgWDJ14gqYN0+NMbu+qC2fykxXaipHxN4Scaj9tUS1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.14.tgz", + "integrity": "sha512-xr0E2n5lyWw3uFSwwUXHc0EcaBDtsal/iIfLioflHdhAe10KSctV978Te7YsfnsMKzcoGeS366+tqbCXdqDHQA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.14.tgz", + "integrity": "sha512-8XH96sOQ4b1LhMlO10eEWOjEngmZ2oyw3pW4o8kvBcpF6pULr56eeYVP5radtgw54g3T8nKHDHYEI5AItvskZg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.14.tgz", + "integrity": "sha512-6ssnvwaTAi8AzKN8By2V0nS+WF5jTP7SfuK6sStGnDP7MCJo/4zHgM9oE1eQTS2jPmo3D673rckuCzRlig+HMA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.14.tgz", + "integrity": "sha512-ONySx3U0wAJOJuxGUlXBWxVKFVpWv88JEv0NZ6NlHknmDd1yCbf4AEdClSgLrqKQDXYywmw4gYDvdLsS6z0hcw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.14.tgz", + "integrity": "sha512-D2LImAIV3QzL7lHURyCHBkycVFbKwkDb1XEUWan+2fb4qfW7qAeUtul7ZIcIwFKZgPcl+6gKZmvLgPSj26RQ2Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.14.tgz", + "integrity": "sha512-kle2Ov6a1e5AjlHlMQl1e+c4myGTeggrRzArQFmWp6O6JoqqB9hT+B28EW4tjFWgV/NxUq46pWYpgaWXsXRPAg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.14.tgz", + "integrity": "sha512-FVdMYIzOLXUq+OE7XYKesuEAqZhmAIV6qOoYahvUp93oXy0MOVTP370ECbPfGXXUdlvc0TNgkJa3YhEwyZ6MRA==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.14.tgz", + "integrity": "sha512-2NzH+iuzMDA+jjtPjuIz/OhRDf8tzbQ1tRZJI//aT25o1HKc0reMMXxKIYq/8nSHXiJSnYV4ODzTiv45s+h73w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.14.tgz", + "integrity": "sha512-VqxvutZNlQxmUNS7Ac+aczttLEoHBJ9e3OYGqnULrfipRvG97qLrAv9EUY9iSrRKBqeEbSvS9bSfstZqwz0T4Q==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.14.tgz", + "integrity": "sha512-+KVHEUshX5n6VP6Vp/AKv9fZIl5kr2ph8EUFmQUJnDpHwcfTSn2AQgYYm0HTBR2Mr4d0Wlr0FxF/Cs5pbFgiOw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.14.tgz", + "integrity": "sha512-6D/dr17piEgevIm1xJfZP2SjB9Z+g8ERhNnBdlZPBWZl+KSPUKLGF13AbvC+nzGh8IxOH2TyTIdRMvKMP0nEzQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.14.tgz", + "integrity": "sha512-rREQBIlMibBetgr2E9Lywt2Qxv2ZdpmYahR4IUlAQ1Efv/A5gYdO0/VIN3iowDbCNTLxp0bb57Vf0LFcffD6kA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.14.tgz", + "integrity": "sha512-DNVjSp/BY4IfwtdUAvWGIDaIjJXY5KI4uD82+15v6k/w7px9dnaDaJJ2R6Mu+KCgr5oklmFc0KjBjh311Gxl9Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.14.tgz", + "integrity": "sha512-pHBWrcA+/oLgvViuG9FO3kNPO635gkoVrRQwe6ZY1S0jdET07xe2toUvQoJQ8KT3/OkxqUasIty5hpuKFLD+eg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.14.tgz", + "integrity": "sha512-CszIGQVk/P8FOS5UgAH4hKc9zOaFo69fe+k1rqgBHx3CSK3Opyk5lwYriIamaWOVjBt7IwEP6NALz+tkVWdFog==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.14.tgz", + "integrity": "sha512-KW9W4psdZceaS9A7Jsgl4WialOznSURvqX/oHZk3gOP7KbjtHLSsnmSvNdzagGJfxbAe30UVGXRe8q8nDsOSQw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", @@ -6748,6 +6890,25 @@ "node": ">=4.0" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -6793,6 +6954,15 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -6803,22 +6973,23 @@ } }, "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" @@ -6879,16 +7050,61 @@ "node": ">=0.10.0" } }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, "node_modules/expand-brackets/node_modules/is-extendable": { @@ -6899,6 +7115,14 @@ "node": ">=0.10.0" } }, + "node_modules/expand-brackets/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expand-brackets/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -7053,9 +7277,9 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7074,9 +7298,9 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", - "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -7088,9 +7312,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -7125,9 +7349,9 @@ "optional": true }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -7213,14 +7437,6 @@ "node": ">=6" } }, - "node_modules/find-cache-dir/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/find-line-column": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/find-line-column/-/find-line-column-0.5.2.tgz", @@ -7405,6 +7621,17 @@ "node": ">= 0.10" } }, + "node_modules/fined/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/flagged-respawn": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", @@ -7413,19 +7640,10 @@ "node": ">= 0.10" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, "node_modules/flow-parser": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.221.0.tgz", - "integrity": "sha512-i+GzdLcKYy5bxhx1N+FIcR1bTqssuVWTJcuytMhwqLAxifz46g4BSNicWXGrtzT0HibJUBIzZOYA3FveJucTPg==", + "version": "0.237.2", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.237.2.tgz", + "integrity": "sha512-mvI/kdfr3l1waaPbThPA8dJa77nHXrfZIun+SWvFwSwDjmeByU7mGJGRmv1+7guU6ccyLV8e1lqZA1lD4iMGnQ==", "engines": { "node": ">=0.4.0" } @@ -7439,15 +7657,6 @@ "readable-stream": "^2.3.6" } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -7516,22 +7725,6 @@ "node": ">= 8" } }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -7561,20 +7754,15 @@ "node": ">=0.4" } }, - "node_modules/fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "optional": true, "os": [ @@ -7585,23 +7773,20 @@ } }, "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -7624,18 +7809,44 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=10" + "node": ">=8" } }, "node_modules/gensync": { @@ -7647,22 +7858,18 @@ } }, "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.*" - } + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7704,9 +7911,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -7925,7 +8132,7 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", "hasInstallScript": true, "optional": true, "os": [ @@ -8076,331 +8283,124 @@ "node": ">=0.10.0" } }, - "node_modules/global-prefix/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/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", - "dependencies": { - "sparkles": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "node_modules/gulp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", - "dependencies": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" - }, - "bin": { - "gulp": "bin/gulp.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", - "dependencies": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" - }, - "bin": { - "gulp": "bin/gulp.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-cli/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dependencies": { - "ansi-wrap": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/gulp-cli/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, - "node_modules/gulp-cli/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" + "isexe": "^2.0.0" }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/gulp-cli/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gulp-cli/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "node_modules/glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", "dependencies": { - "ansi-regex": "^2.0.0" + "sparkles": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/gulp-cli/node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/gulp-cli/node_modules/y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" - }, - "node_modules/gulp-cli/node_modules/yargs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", - "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", + "node_modules/gulp-cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", "dependencies": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.1" + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/gulp-cli/node_modules/yargs-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", - "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", + "node_modules/gulp-cli/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dependencies": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/gulp-file": { @@ -8462,6 +8462,11 @@ "node": ">=10" } }, + "node_modules/gulp-replace/node_modules/@types/node": { + "version": "14.18.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.33.tgz", + "integrity": "sha512-qelS/Ra6sacc4loe/3MSjXNL1dNQ/GjxNHVzuChwMfmk7HuycRLVQN2qNY3XahK+fZc5E2szqQSKUyAF0E+2bg==" + }, "node_modules/gulp-zip": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gulp-zip/-/gulp-zip-6.0.0.tgz", @@ -8541,6 +8546,17 @@ "node": ">=6" } }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -8559,22 +8575,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" + "get-intrinsic": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8669,17 +8674,6 @@ "node": ">=0.10.0" } }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -8752,17 +8746,18 @@ "dev": true }, "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, "engines": { - "node": ">=10.17.0" + "node": ">=12.20.0" } }, "node_modules/husky": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", - "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz", + "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==", "dev": true, "bin": { "husky": "lib/bin.js" @@ -8798,18 +8793,18 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/immutable": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", - "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", "dev": true }, "node_modules/import-fresh": { @@ -8914,6 +8909,19 @@ "typescript": "^3.2.4" } }, + "node_modules/import-sort-parser-typescript/node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/import-sort-style": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/import-sort-style/-/import-sort-style-6.0.0.tgz", @@ -8926,15 +8934,6 @@ "integrity": "sha512-Oxd256EVt6TAgawhIDuKnNHWumzHMHFWhVncBBvlHVnx69B4GP/Gu4Xo+gjxtqSEKEvam5ajUkNvnsXLDMDjKg==", "dev": true }, - "node_modules/import-sort/node_modules/detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9039,6 +9038,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/inquirer/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -9047,6 +9051,38 @@ "node": ">=8" } }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/inquirer/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9058,14 +9094,19 @@ "node": ">=8" } }, + "node_modules/inquirer/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", "side-channel": "^1.0.4" }, "engines": { @@ -9101,28 +9142,14 @@ } }, "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dependencies": { - "hasown": "^2.0.0" + "kind-of": "^6.0.0" }, "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, "node_modules/is-arrayish": { @@ -9213,25 +9240,25 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dependencies": { - "hasown": "^2.0.0" + "has": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dependencies": { - "hasown": "^2.0.0" + "kind-of": "^6.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, "node_modules/is-date-object": { @@ -9250,15 +9277,16 @@ } }, "node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, "node_modules/is-directory": { @@ -9295,6 +9323,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9392,12 +9431,9 @@ } }, "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "engines": { "node": ">=0.10.0" } @@ -9455,11 +9491,12 @@ } }, "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9507,21 +9544,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -9604,13 +9626,13 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", - "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" }, @@ -9618,6 +9640,17 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -9664,9 +9697,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -9728,6 +9761,80 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-changed-files/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9742,6 +9849,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jest-changed-files/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, "node_modules/jest-circus": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", @@ -9911,6 +10026,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jest-cli/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -9927,6 +10055,19 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/jest-cli/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/jest-cli/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/jest-cli/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -9935,6 +10076,27 @@ "node": ">=8" } }, + "node_modules/jest-cli/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-cli/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9946,6 +10108,39 @@ "node": ">=8" } }, + "node_modules/jest-cli/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/jest-config": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", @@ -10043,6 +10238,23 @@ "node": ">=8" } }, + "node_modules/jest-config/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-config/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10143,6 +10355,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-docblock/node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "engines": { + "node": ">=8" + } + }, "node_modules/jest-each": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", @@ -10899,6 +11119,17 @@ "node": ">=8" } }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-snapshot/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11199,6 +11430,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11444,17 +11683,17 @@ } }, "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.2.tgz", + "integrity": "sha512-AHWa+QO/cgRg4N+DsmHg1Y7xnz+8KU3EflM0LVDTdmrYOc1WWTSkOjtpUveQH+1Bqd5rtcVnb/DuxV/UjDO4rA==", "dependencies": { "abab": "^2.0.6", - "acorn": "^8.8.1", + "acorn": "^8.8.0", "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", + "decimal.js": "^10.4.1", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", @@ -11467,12 +11706,12 @@ "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "w3c-xmlserializer": "^3.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", - "ws": "^8.11.0", + "ws": "^8.9.0", "xml-name-validator": "^4.0.0" }, "engines": { @@ -11554,6 +11793,11 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -11589,9 +11833,9 @@ } }, "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", "dev": true, "engines": { "node": ">= 8" @@ -11655,6 +11899,18 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -11673,6 +11929,17 @@ "node": ">= 0.8" } }, + "node_modules/liftoff/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lilconfig": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", @@ -11688,46 +11955,33 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/lint-staged": { - "version": "12.5.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.5.0.tgz", - "integrity": "sha512-BKLUjWDsKquV/JuIcoQW4MSAI3ggwEImF1+sB4zaKvyVx1wBk3FsG7UK9bpnmBTN1pm7EH2BBcMwINJzCRv12g==", + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz", + "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==", "dev": true, "dependencies": { "cli-truncate": "^3.1.0", - "colorette": "^2.0.16", + "colorette": "^2.0.17", "commander": "^9.3.0", "debug": "^4.3.4", - "execa": "^5.1.1", + "execa": "^6.1.0", "lilconfig": "2.0.5", "listr2": "^4.0.5", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-inspect": "^1.12.2", - "pidtree": "^0.5.0", + "pidtree": "^0.6.0", "string-argv": "^0.3.1", - "supports-color": "^9.2.2", - "yaml": "^1.10.2" + "yaml": "^2.1.1" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "dev": true, - "engines": { - "node": ">=12" + "node": "^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://opencollective.com/lint-staged" } }, "node_modules/listr2": { @@ -11806,6 +12060,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/listr2/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -11815,15 +12075,6 @@ "node": ">=8" } }, - "node_modules/listr2/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/listr2/node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -11838,49 +12089,51 @@ "node": ">=8" } }, - "node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "node_modules/listr2/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, "dependencies": { - "error-ex": "^1.2.0" + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/load-json-file/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/load-json-file/node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dependencies": { - "is-utf8": "^0.2.0" - }, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/load-yaml-file": { @@ -11949,7 +12202,8 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "peer": true }, "node_modules/lodash.startcase": { "version": "4.4.0", @@ -12008,6 +12262,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -12034,6 +12294,20 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -12048,20 +12322,12 @@ "node": ">=8" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", + "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "sourcemap-codec": "^1.4.8" }, "engines": { "node": ">=12" @@ -12081,6 +12347,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -12383,11 +12660,15 @@ } }, "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/mimic-response": { @@ -12411,9 +12692,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", - "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.0.tgz", + "integrity": "sha512-auqtVo8KhTScMsba7MbijqZTfibbXiBNlPAQbsVt7enQfcDYLdgG57eGxMqwVU3mfeWANY4F1wUg+rMF+ycZgw==", "dev": true, "dependencies": { "schema-utils": "^4.0.0" @@ -12455,9 +12736,12 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", + "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { "node": ">=8" } @@ -12474,22 +12758,6 @@ "node": ">= 8" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -12503,9 +12771,9 @@ } }, "node_modules/mixme": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.9.tgz", - "integrity": "sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.4.tgz", + "integrity": "sha512-3KYa4m4Vlqx98GPdOHghxSdNtTvcP8E0kkaJ5Dlh+h2DRzF7zpuVVcA8B0QpKd11YJeP9QQ7ASkKzOeu195Wzw==", "dev": true, "engines": { "node": ">= 8.0.0" @@ -12523,9 +12791,9 @@ } }, "node_modules/module-alias": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", - "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", + "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" }, "node_modules/ms": { "version": "2.1.2", @@ -12556,9 +12824,9 @@ } }, "node_modules/nan": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -12632,9 +12900,9 @@ } }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -12656,9 +12924,9 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nopt": { "version": "5.0.0", @@ -12685,14 +12953,6 @@ "validate-npm-package-license": "^3.0.1" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -12753,34 +13013,6 @@ "node": ">=4.8" } }, - "node_modules/npm-run-all/node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/npm-run-all/node_modules/path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -12790,18 +13022,6 @@ "node": ">=4" } }, - "node_modules/npm-run-all/node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/npm-run-all/node_modules/pidtree": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", @@ -12814,38 +13034,6 @@ "node": ">=0.10" } }, - "node_modules/npm-run-all/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/npm-run-all/node_modules/shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -12867,15 +13055,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-all/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/npm-run-all/node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -12889,14 +13068,30 @@ } }, "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, "dependencies": { - "path-key": "^3.0.0" + "path-key": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/npmlog": { @@ -12919,9 +13114,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" }, "node_modules/object-assign": { "version": "4.1.1", @@ -12936,35 +13131,66 @@ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", "dependencies": { - "is-descriptor": "^0.1.0" + "kind-of": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" } }, "node_modules/object-copy/node_modules/kind-of": { @@ -12979,9 +13205,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13081,14 +13307,15 @@ } }, "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-fn": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -13109,6 +13336,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -13225,20 +13468,16 @@ } }, "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "json-parse-better-errors": "^1.0.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, "node_modules/parse-node-version": { @@ -13258,9 +13497,9 @@ } }, "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.1.tgz", + "integrity": "sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==", "dependencies": { "entities": "^4.4.0" }, @@ -13339,9 +13578,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -13355,9 +13594,9 @@ } }, "node_modules/pidtree": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.5.0.tgz", - "integrity": "sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, "bin": { "pidtree": "bin/pidtree.js" @@ -13421,9 +13660,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, "funding": [ { @@ -13440,9 +13679,9 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -13461,9 +13700,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -13508,9 +13747,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -13527,9 +13766,9 @@ "dev": true }, "node_modules/preferred-pm": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.1.2.tgz", - "integrity": "sha512-nk7dKrcW8hfCZ4H6klWcdRknBOXWzNQByJ0oJyX97BOupsYD+FzLS4hflgEu/uPUEHZCuRfMxzCBsuWd7OzT8Q==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", + "integrity": "sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==", "dev": true, "dependencies": { "find-up": "^5.0.0", @@ -13602,10 +13841,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -13670,9 +13917,9 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/promise-polyfill": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", - "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", "dev": true }, "node_modules/prompts": { @@ -13718,17 +13965,17 @@ } }, "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "engines": { "node": ">=6" } }, "node_modules/pure-rand": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", - "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "funding": [ { "type": "individual", @@ -13780,17 +14027,15 @@ } }, "node_modules/random-words": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/random-words/-/random-words-1.3.0.tgz", - "integrity": "sha512-brwCGe+DN9DqZrAQVNj1Tct1Lody6GrYL/7uei5wfjeQdacFyFd2h/51LNlOoBMzIKMS9xohuL4+wlF/z1g/xg==", - "dependencies": { - "seedrandom": "^3.0.5" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/random-words/-/random-words-1.2.0.tgz", + "integrity": "sha512-YP2bXrT19pxtBh22DK9CLcWsmBjUBAGzw3JWJycTNbXm1+0aS6PrKuAJ9aLT0GGaPlPp9LExfJIMVkzhrDZE6g==" }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -13801,18 +14046,17 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/read-pkg-up": { @@ -13832,16 +14076,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "node_modules/read-pkg-up/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, "engines": { "node": ">=8" } }, - "node_modules/read-pkg/node_modules/type-fest": { + "node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", @@ -13850,6 +14118,36 @@ "node": ">=8" } }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/read-yaml-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", @@ -13875,9 +14173,9 @@ } }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -13940,12 +14238,14 @@ "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "peer": true }, "node_modules/regenerate-unicode-properties": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "peer": true, "dependencies": { "regenerate": "^1.4.2" }, @@ -13954,14 +14254,15 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.8.4" } @@ -13979,14 +14280,14 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -13999,6 +14300,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "peer": true, "dependencies": { "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", @@ -14015,6 +14317,7 @@ "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "peer": true, "dependencies": { "jsesc": "~0.5.0" }, @@ -14026,6 +14329,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "peer": true, "bin": { "jsesc": "bin/jsesc" } @@ -14158,11 +14462,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -14249,6 +14553,28 @@ "node": ">=8" } }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -14288,128 +14614,71 @@ } }, "node_modules/rollup": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.3.0.tgz", - "integrity": "sha512-scIi1NrKLDIYSPK66jjECtII7vIgdAMFmFo8h6qm++I6nN9qDSV35Ku6erzGVqYjx+lj+j5wkusRMr++8SyDZg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.3.0.tgz", + "integrity": "sha512-wqOV/vUJCYEbWsXvwCkgGWvgaEnsbn4jxBQWKpN816CqsmCimDmCNJI83c6if7QVD4v/zlyRzxN7U2yDT5rfoA==", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=18.0.0", + "node": ">=14.18.0", "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.3.0", - "@rollup/rollup-android-arm64": "4.3.0", - "@rollup/rollup-darwin-arm64": "4.3.0", - "@rollup/rollup-darwin-x64": "4.3.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.3.0", - "@rollup/rollup-linux-arm64-gnu": "4.3.0", - "@rollup/rollup-linux-arm64-musl": "4.3.0", - "@rollup/rollup-linux-x64-gnu": "4.3.0", - "@rollup/rollup-linux-x64-musl": "4.3.0", - "@rollup/rollup-win32-arm64-msvc": "4.3.0", - "@rollup/rollup-win32-ia32-msvc": "4.3.0", - "@rollup/rollup-win32-x64-msvc": "4.3.0", "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-typescript2": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz", - "integrity": "sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==", - "dependencies": { - "@rollup/pluginutils": "^4.1.2", - "find-cache-dir": "^3.3.2", - "fs-extra": "^10.0.0", - "semver": "^7.5.4", - "tslib": "^2.6.2" - }, - "peerDependencies": { - "rollup": ">=1.26.3", - "typescript": ">=2.4.0" - } - }, - "node_modules/rollup-plugin-typescript2/node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/rollup-plugin-typescript2/node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "node_modules/rollup-plugin-dts": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-5.0.0.tgz", + "integrity": "sha512-OO8ayCvuJCKaQSShyVTARxGurVVk4ulzbuvz+0zFd1f93vlnWFU5pBMT7HFeS6uj7MvvZLx4kUAarGATSU1+Ng==", "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" + "magic-string": "^0.26.7" }, "engines": { - "node": ">=8" + "node": ">=v14" }, "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/rollup-plugin-typescript2/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/rollup-plugin-typescript2/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" + "url": "https://github.com/sponsors/Swatinem" }, "optionalDependencies": { - "graceful-fs": "^4.1.6" + "@babel/code-frame": "^7.18.6" + }, + "peerDependencies": { + "rollup": "^3.0.0", + "typescript": "^4.1" } }, - "node_modules/rollup-plugin-typescript2/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/rollup-plugin-esbuild": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-5.0.0.tgz", + "integrity": "sha512-1cRIOHAPh8WQgdQQyyvFdeOdxuiyk+zB5zJ5+YOwrZP4cJ0MT3Fs48pQxrZeyZHcn+klFherytILVfE4aYrneg==", "dependencies": { - "semver": "^6.0.0" + "@rollup/pluginutils": "^5.0.1", + "debug": "^4.3.4", + "es-module-lexer": "^1.0.5", + "joycon": "^3.1.1", + "jsonc-parser": "^3.2.0" }, "engines": { - "node": ">=8" + "node": ">=14.18.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rollup-plugin-typescript2/node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "peerDependencies": { + "esbuild": ">=0.10.1", + "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0" } }, - "node_modules/rollup-plugin-typescript2/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/rollup-plugin-node-externals": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-externals/-/rollup-plugin-node-externals-5.0.2.tgz", + "integrity": "sha512-UGAPdPjD0PPk4hNcHLnqwqsfNc/u0vaAjWnjkyS6j2jIMB4LLi1pW3TE01eaytJKZactNik2t8AQC33esS9GKw==", "engines": { - "node": ">= 10.0.0" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.60.0 || ^3.0.0" } }, "node_modules/run-async": { @@ -14440,49 +14709,18 @@ } ], "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "queue-microtask": "^1.2.2" + } }, - "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "tslib": "^2.1.0" } }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -14516,9 +14754,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", + "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -14529,7 +14767,7 @@ "sass": "sass.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" } }, "node_modules/sass-loader": { @@ -14582,15 +14820,15 @@ } }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", + "ajv": "^8.8.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "ajv-keywords": "^5.0.0" }, "engines": { "node": ">= 12.13.0" @@ -14606,17 +14844,11 @@ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "semver": "bin/semver" } }, "node_modules/semver-greatest-satisfied-range": { @@ -14630,26 +14862,11 @@ "node": ">= 0.10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/serialize-javascript": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, "dependencies": { "randombytes": "^2.1.0" } @@ -14659,34 +14876,6 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, - "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -14720,6 +14909,17 @@ "node": ">=0.10.0" } }, + "node_modules/set-value/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -14751,9 +14951,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", + "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14912,16 +15112,54 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/smartwrap/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/smartwrap/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==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/smartwrap/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/smartwrap/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==", "dev": true }, + "node_modules/smartwrap/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/smartwrap/node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", "dev": true }, "node_modules/smartwrap/node_modules/wrap-ansi": { @@ -14966,11 +15204,6 @@ "node": ">=8" } }, - "node_modules/smob": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", - "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==" - }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -15065,16 +15298,61 @@ "node": ">=0.10.0" } }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, "node_modules/snapdragon/node_modules/is-extendable": { @@ -15085,6 +15363,14 @@ "node": ">=0.10.0" } }, + "node_modules/snapdragon/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/snapdragon/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -15107,9 +15393,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -15143,6 +15429,11 @@ "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", "deprecated": "See https://github.com/lydell/source-map-url#deprecated" }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, "node_modules/sparkles": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", @@ -15222,9 +15513,9 @@ "dev": true }, "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -15245,9 +15536,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==" + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" }, "node_modules/split-string": { "version": "3.1.0", @@ -15315,16 +15606,69 @@ "node": ">=0.10.0" } }, + "node_modules/static-extend/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" } }, "node_modules/stream-exhaust": { @@ -15347,12 +15691,16 @@ } }, "node_modules/streamx": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.3.tgz", - "integrity": "sha512-8UmzFRA08VahBuaw6UxQAX+NAmMtPVkPDWUtLhyHRaU2uxiw3+keTuSJRJfAfpqo7M3TSAhYtdRzYqG/j02hzA==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -15364,9 +15712,9 @@ } }, "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", "dev": true, "engines": { "node": ">=0.6.19" @@ -15385,52 +15733,58 @@ } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/string.prototype.padend": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", - "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "node_modules/string.prototype.padend": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz", + "integrity": "sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" }, "engines": { "node": ">= 0.4" @@ -15440,28 +15794,28 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15486,12 +15840,16 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-indent": { @@ -15538,6 +15896,19 @@ "node": ">=8" } }, + "node_modules/sucrase/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/sucrase/node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -15625,9 +15996,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -15640,10 +16011,13 @@ "node": ">=10" } }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } }, "node_modules/teex": { "version": "1.0.1", @@ -15668,6 +16042,7 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dependencies": { "glob": "^7.1.3" }, @@ -15688,9 +16063,10 @@ } }, "node_modules/terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.0.tgz", + "integrity": "sha512-JpcpGOQLOXm2jsomozdMDpd5f8ZHh1rR48OFgWUH3QsyZcfPgv2qDCYbcDEAYNd4OZRj2bWYKpwdll/udZCk/Q==", + "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -15828,12 +16204,14 @@ "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true }, "node_modules/terser/node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15852,6 +16230,14 @@ "node": ">=8" } }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/textextensions": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", @@ -16063,9 +16449,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -16109,18 +16495,18 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tty-table": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.2.3.tgz", - "integrity": "sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", + "integrity": "sha512-kRj5CBzOrakV4VRRY5kUWbNYvo/FpOsz65DzI5op9P+cHov3+IqPbo1JE1ZnQGkHdZgNFDsrEjrfqqy/Ply9fw==", "dev": true, "dependencies": { "chalk": "^4.1.2", - "csv": "^5.5.3", - "kleur": "^4.1.5", + "csv": "^5.5.0", + "kleur": "^4.1.4", "smartwrap": "^2.0.2", - "strip-ansi": "^6.0.1", + "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1", - "yargs": "^17.7.1" + "yargs": "^17.1.1" }, "bin": { "tty-table": "adapters/terminal-adapter.js" @@ -16160,6 +16546,20 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/tty-table/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/tty-table/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -16178,6 +16578,21 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/tty-table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/tty-table/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==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/tty-table/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -16187,6 +16602,15 @@ "node": ">=8" } }, + "node_modules/tty-table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/tty-table/node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -16196,6 +16620,20 @@ "node": ">=6" } }, + "node_modules/tty-table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tty-table/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -16208,27 +16646,64 @@ "node": ">=8" } }, + "node_modules/tty-table/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/tty-table/node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tty-table/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/turbo": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.16.tgz", - "integrity": "sha512-2CEaK4FIuSZiP83iFa9GqMTQhroW2QryckVqUydmg4tx78baftTOS0O+oDAhvo9r9Nit4xUEtC1RAHoqs6ZEtg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.6.3.tgz", + "integrity": "sha512-FtfhJLmEEtHveGxW4Ye/QuY85AnZ2ZNVgkTBswoap7UMHB1+oI4diHPNyqrQLG4K1UFtCkjOlVoLsllUh/9QRw==", "dev": true, + "hasInstallScript": true, "bin": { "turbo": "bin/turbo" }, "optionalDependencies": { - "turbo-darwin-64": "1.10.16", - "turbo-darwin-arm64": "1.10.16", - "turbo-linux-64": "1.10.16", - "turbo-linux-arm64": "1.10.16", - "turbo-windows-64": "1.10.16", - "turbo-windows-arm64": "1.10.16" + "turbo-darwin-64": "1.6.3", + "turbo-darwin-arm64": "1.6.3", + "turbo-linux-64": "1.6.3", + "turbo-linux-arm64": "1.6.3", + "turbo-windows-64": "1.6.3", + "turbo-windows-arm64": "1.6.3" } }, "node_modules/turbo-darwin-64": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.10.16.tgz", - "integrity": "sha512-+Jk91FNcp9e9NCLYlvDDlp2HwEDp14F9N42IoW3dmHI5ZkGSXzalbhVcrx3DOox3QfiNUHxzWg4d7CnVNCuuMg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.6.3.tgz", + "integrity": "sha512-QmDIX0Yh1wYQl0bUS0gGWwNxpJwrzZU2GIAYt3aOKoirWA2ecnyb3R6ludcS1znfNV2MfunP+l8E3ncxUHwtjA==", "cpu": [ "x64" ], @@ -16239,9 +16714,9 @@ ] }, "node_modules/turbo-darwin-arm64": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.16.tgz", - "integrity": "sha512-jqGpFZipIivkRp/i+jnL8npX0VssE6IAVNKtu573LXtssZdV/S+fRGYA16tI46xJGxSAivrZ/IcgZrV6Jk80bw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.6.3.tgz", + "integrity": "sha512-75DXhFpwE7CinBbtxTxH08EcWrxYSPFow3NaeFwsG8aymkWXF+U2aukYHJA6I12n9/dGqf7yRXzkF0S/9UtdyQ==", "cpu": [ "arm64" ], @@ -16252,9 +16727,9 @@ ] }, "node_modules/turbo-linux-64": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.10.16.tgz", - "integrity": "sha512-PpqEZHwLoizQ6sTUvmImcRmACyRk9EWLXGlqceogPZsJ1jTRK3sfcF9fC2W56zkSIzuLEP07k5kl+ZxJd8JMcg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.6.3.tgz", + "integrity": "sha512-O9uc6J0yoRPWdPg9THRQi69K6E2iZ98cRHNvus05lZbcPzZTxJYkYGb5iagCmCW/pq6fL4T4oLWAd6evg2LGQA==", "cpu": [ "x64" ], @@ -16265,9 +16740,9 @@ ] }, "node_modules/turbo-linux-arm64": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.10.16.tgz", - "integrity": "sha512-TMjFYz8to1QE0fKVXCIvG/4giyfnmqcQIwjdNfJvKjBxn22PpbjeuFuQ5kNXshUTRaTJihFbuuCcb5OYFNx4uw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.6.3.tgz", + "integrity": "sha512-dCy667qqEtZIhulsRTe8hhWQNCJO0i20uHXv7KjLHuFZGCeMbWxB8rsneRoY+blf8+QNqGuXQJxak7ayjHLxiA==", "cpu": [ "arm64" ], @@ -16278,9 +16753,9 @@ ] }, "node_modules/turbo-windows-64": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.10.16.tgz", - "integrity": "sha512-+jsf68krs0N66FfC4/zZvioUap/Tq3sPFumnMV+EBo8jFdqs4yehd6+MxIwYTjSQLIcpH8KoNMB0gQYhJRLZzw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.6.3.tgz", + "integrity": "sha512-lKRqwL3mrVF09b9KySSaOwetehmGknV9EcQTF7d2dxngGYYX1WXoQLjFP9YYH8ZV07oPm+RUOAKSCQuDuMNhiA==", "cpu": [ "x64" ], @@ -16291,9 +16766,9 @@ ] }, "node_modules/turbo-windows-arm64": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.10.16.tgz", - "integrity": "sha512-sKm3hcMM1bl0B3PLG4ifidicOGfoJmOEacM5JtgBkYM48ncMHjkHfFY7HrJHZHUnXM4l05RQTpLFoOl/uIo2HQ==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.6.3.tgz", + "integrity": "sha512-BXY1sDPEA1DgPwuENvDCD8B7Hb0toscjus941WpL8CVd10hg9pk/MWn9CNgwDO5Q9ks0mw+liDv2EMnleEjeNA==", "cpu": [ "arm64" ], @@ -16308,6 +16783,17 @@ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -16327,80 +16813,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16460,10 +16882,16 @@ "node": ">= 0.10" } }, + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", + "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "peer": true, "engines": { "node": ">=4" } @@ -16472,6 +16900,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "peer": true, "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -16484,6 +16913,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "peer": true, "engines": { "node": ">=4" } @@ -16492,6 +16922,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "peer": true, "engines": { "node": ">=4" } @@ -16590,9 +17021,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "funding": [ { "type": "opencollective", @@ -16608,8 +17039,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -16656,9 +17087,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/v8-to-istanbul": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", - "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -16668,6 +17099,11 @@ "node": ">=10.12.0" } }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", @@ -16773,11 +17209,6 @@ "node": ">= 0.10" } }, - "node_modules/vinyl-sourcemap/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, "node_modules/vinyl-sourcemap/node_modules/normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", @@ -16790,9 +17221,9 @@ } }, "node_modules/vue-jscodeshift-adapter": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vue-jscodeshift-adapter/-/vue-jscodeshift-adapter-2.2.1.tgz", - "integrity": "sha512-4aTkHYknYgP9uk/465MDZjvrotF6o2RMWDy0t+9RUULfgbkT+rHLrNw8onxOk4Y8fCpgcS81b09afodRZY/LuQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vue-jscodeshift-adapter/-/vue-jscodeshift-adapter-2.2.0.tgz", + "integrity": "sha512-hC/eplyzKq68GbCmHKz9xFdiGvl0TSObGUX2SIVOZlcElaJXQiB/H7au5tg7wAcbqv7vDQvNERbtAf/kMsyVHA==", "dependencies": { "vue-sfc-descriptor-to-string": "^1.0.0", "vue-template-compiler": "^2.5.13" @@ -16815,23 +17246,23 @@ } }, "node_modules/vue-template-compiler": { - "version": "2.7.15", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.15.tgz", - "integrity": "sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og==", + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", + "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", "dependencies": { "xml-name-validator": "^4.0.0" }, "engines": { - "node": ">=14" + "node": ">=12" } }, "node_modules/walker": { @@ -16873,9 +17304,9 @@ } }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.88.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz", + "integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -16997,13 +17428,12 @@ } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", - "flat": "^5.0.2", "wildcard": "^2.0.0" }, "engines": { @@ -17179,25 +17609,6 @@ "node": ">=8.15" } }, - "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -17206,12 +17617,46 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -17247,17 +17692,43 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "color-name": "~1.1.4" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -17276,15 +17747,15 @@ } }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "utf-8-validate": "^5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -17325,42 +17796,45 @@ "integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==" }, "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" }, "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", "dev": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.1" } }, "node_modules/yargs-parser": { @@ -17375,12 +17849,170 @@ "node": ">=6" } }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "node_modules/yargs/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "engines": { - "node": ">=12" + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", + "dependencies": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" } }, "node_modules/yazl": { @@ -17407,33 +18039,27 @@ "version": "2.0.2", "license": "MIT", "dependencies": { - "@babel/cli": "7.23.0", - "@babel/core": "7.23.3", - "@babel/preset-env": "7.23.3", - "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "25.0.7", - "@rollup/plugin-json": "6.0.1", "@rollup/plugin-node-resolve": "15.2.3", - "@rollup/plugin-replace": "5.0.5", - "@rollup/plugin-terser": "0.4.4", "@sucrase/jest-plugin": "3.0.0", "@types/gulp": "4.0.17", "@types/jest": "29.5.8", "alias-hq": "6.2.3", - "babel-preset-minify": "0.5.2", "canvas": "^2.11.2", + "esbuild": "0.15.14", "gulp": "4.0.2", "gulp-cli": "2.3.0", - "gulp-file": "^0.4.0", + "gulp-file": "0.4.0", "gulp-rename": "2.0.0", "gulp-replace": "1.1.4", "gulp-zip": "6.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "merge-stream": "2.0.0", - "regenerator-runtime": "0.14.0", "rollup": "4.3.0", - "rollup-plugin-typescript2": "0.36.0", + "rollup-plugin-dts": "5.0.0", + "rollup-plugin-esbuild": "5.0.0", + "rollup-plugin-node-externals": "5.0.2", "sucrase": "3.34.0", "tslib": "2.6.2", "typescript": "^5.2.2" @@ -17442,29 +18068,37 @@ "node": ">=18.0.0" } }, - "packages/config/node_modules/alias-hq": { - "version": "6.2.3", - "license": "MIT", - "dependencies": { - "colors": "^1.4.0", - "get-tsconfig": "^4.7.0", - "glob": "^7.1.6", - "inquirer": "^7.3.3", - "jscodeshift": "^0.13.0", - "json5": "^2.2.3", - "module-alias": "^2.2.2", - "node-fetch": "^2.6.0", - "open": "^7.0.0", - "vue-jscodeshift-adapter": "^2.1.0" - }, + "packages/config/node_modules/rollup": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.3.0.tgz", + "integrity": "sha512-scIi1NrKLDIYSPK66jjECtII7vIgdAMFmFo8h6qm++I6nN9qDSV35Ku6erzGVqYjx+lj+j5wkusRMr++8SyDZg==", "bin": { - "alias-hq": "bin/alias-hq" + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.3.0", + "@rollup/rollup-android-arm64": "4.3.0", + "@rollup/rollup-darwin-arm64": "4.3.0", + "@rollup/rollup-darwin-x64": "4.3.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.3.0", + "@rollup/rollup-linux-arm64-gnu": "4.3.0", + "@rollup/rollup-linux-arm64-musl": "4.3.0", + "@rollup/rollup-linux-x64-gnu": "4.3.0", + "@rollup/rollup-linux-x64-musl": "4.3.0", + "@rollup/rollup-win32-arm64-msvc": "4.3.0", + "@rollup/rollup-win32-ia32-msvc": "4.3.0", + "@rollup/rollup-win32-x64-msvc": "4.3.0", + "fsevents": "~2.3.2" } }, "packages/config/node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17515,7 +18149,8 @@ "dependencies": { "auto-bind": "^4.0.0", "random-words": "^1.1.1", - "seedrandom": "^3.0.5" + "seedrandom": "^3.0.5", + "type-fest": "^2.9.0" }, "devDependencies": { "@fontsource/open-sans": "4.5.3", @@ -17533,6 +18168,17 @@ "webpack-remove-empty-scripts": "^0.7.2" } }, + "packages/jspsych/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/plugin-animation": { "name": "@jspsych/plugin-animation", "version": "1.1.3", @@ -18153,9 +18799,11 @@ "license": "MIT", "devDependencies": { "@jspsych/config": "^2.0.0", + "@jspsych/extension-webgazer": "^1.0.2", "@jspsych/test-utils": "^1.1.2" }, "peerDependencies": { + "@jspsych/extension-webgazer": ">=1.0.0", "jspsych": ">=7.0.0" } }, @@ -18165,9 +18813,11 @@ "license": "MIT", "devDependencies": { "@jspsych/config": "^2.0.0", + "@jspsych/extension-webgazer": "^1.0.2", "@jspsych/test-utils": "^1.1.2" }, "peerDependencies": { + "@jspsych/extension-webgazer": ">=1.0.0", "jspsych": ">=7.0.0" } }, @@ -18177,9 +18827,11 @@ "license": "MIT", "devDependencies": { "@jspsych/config": "^2.0.0", + "@jspsych/extension-webgazer": "^1.0.2", "@jspsych/test-utils": "^1.1.2" }, "peerDependencies": { + "@jspsych/extension-webgazer": ">=1.0.0", "jspsych": ">=7.0.0" } }, @@ -18192,6 +18844,7 @@ "jspsych": "^7.3.4" }, "peerDependencies": { + "@types/jest": "*", "jspsych": ">=7.0.0" } } diff --git a/package.json b/package.json index cdedc57800..735ccf1cc0 100644 --- a/package.json +++ b/package.json @@ -25,16 +25,15 @@ }, "packageManager": "npm@8.3.1", "devDependencies": { - "@changesets/changelog-github": "^0.4.4", - "@changesets/cli": "^2.22.0", - "alias-hq": "github:bjoluc/alias-hq#tsconfig-parsing-quickfix", - "husky": "^8.0.1", + "@changesets/changelog-github": "^0.4.7", + "@changesets/cli": "^2.25.2", + "@jspsych/config": "^1.3.2", + "husky": "^8.0.2", "import-sort-style-module": "^6.0.0", - "jest": "*", - "lint-staged": "^12.4.1", - "prettier": "^2.6.2", + "lint-staged": "^13.0.3", + "prettier": "^2.7.1", "prettier-plugin-import-sort": "^0.0.7", - "turbo": "^1.2.9" + "turbo": "^1.6.3" }, "prettier": { "printWidth": 100 diff --git a/packages/config/babel.cjs b/packages/config/babel.cjs deleted file mode 100644 index 9a46be1c82..0000000000 --- a/packages/config/babel.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: ["@babel/preset-env"], -}; diff --git a/packages/config/jest.cjs b/packages/config/jest.cjs index 250efec987..a905f0c42b 100644 --- a/packages/config/jest.cjs +++ b/packages/config/jest.cjs @@ -8,6 +8,7 @@ module.exports.makePackageConfig = (dirname) => { return { transform: { "\\.(js|jsx|ts|tsx)$": "@sucrase/jest-plugin" }, moduleNameMapper: hq.load(dirname + "/tsconfig.json").get("jest"), + testEnvironment: "jsdom", testEnvironmentOptions: { fetchExternalResources: true, diff --git a/packages/config/package.json b/packages/config/package.json index a136f15a82..9a78422dac 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -4,10 +4,6 @@ "description": "Shared (build) configuration for jsPsych packages", "type": "module", "exports": { - "./babel": { - "import": null, - "require": "./babel.cjs" - }, "./gulp": { "import": "./gulp.js", "require": null @@ -39,33 +35,27 @@ }, "homepage": "https://www.jspsych.org/latest/developers/configuration", "dependencies": { - "@babel/cli": "7.23.0", - "@babel/core": "7.23.3", - "@babel/preset-env": "7.23.3", - "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "25.0.7", - "@rollup/plugin-json": "6.0.1", "@rollup/plugin-node-resolve": "15.2.3", - "@rollup/plugin-replace": "5.0.5", - "@rollup/plugin-terser": "0.4.4", "@sucrase/jest-plugin": "3.0.0", "@types/gulp": "4.0.17", "@types/jest": "29.5.8", "alias-hq": "6.2.3", - "babel-preset-minify": "0.5.2", "canvas": "^2.11.2", + "esbuild": "0.15.14", "gulp": "4.0.2", "gulp-cli": "2.3.0", - "gulp-file": "^0.4.0", + "gulp-file": "0.4.0", "gulp-rename": "2.0.0", "gulp-replace": "1.1.4", "gulp-zip": "6.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "merge-stream": "2.0.0", - "regenerator-runtime": "0.14.0", "rollup": "4.3.0", - "rollup-plugin-typescript2": "0.36.0", + "rollup-plugin-dts": "5.0.0", + "rollup-plugin-esbuild": "5.0.0", + "rollup-plugin-node-externals": "5.0.2", "sucrase": "3.34.0", "tslib": "2.6.2", "typescript": "^5.2.2" diff --git a/packages/config/rollup.js b/packages/config/rollup.js index 2601c90452..d59c500154 100644 --- a/packages/config/rollup.js +++ b/packages/config/rollup.js @@ -1,16 +1,23 @@ import { readFileSync } from "node:fs"; +import path from "path"; -import { DEFAULT_EXTENSIONS as babelDefaultExtensions } from "@babel/core"; -import { babel } from "@rollup/plugin-babel"; import commonjs from "@rollup/plugin-commonjs"; -import json from "@rollup/plugin-json"; import resolve from "@rollup/plugin-node-resolve"; -import replace from "@rollup/plugin-replace"; -import terser from "@rollup/plugin-terser"; import { defineConfig } from "rollup"; -import typescript from "rollup-plugin-typescript2"; +import dts from "rollup-plugin-dts"; +import esbuild from "rollup-plugin-esbuild"; +import externals from "rollup-plugin-node-externals"; import ts from "typescript"; +const getTsCompilerOptions = () => { + const cwd = process.cwd(); + return ts.parseJsonConfigFileContent( + ts.readConfigFile(path.join(cwd, "tsconfig.json"), ts.sys.readFile).config, + ts.sys, + cwd + ).options; +}; + const getPackageInfo = () => { const { name, version } = JSON.parse(readFileSync("./package.json")); return { name, version }; @@ -22,7 +29,7 @@ const makeConfig = ({ iifeOutputOptions = {}, isNodeOnlyBuild = false, }) => { - const source = "src/index"; + const input = "src/index.ts"; const destinationDirectory = "dist"; const destination = `${destinationDirectory}/index`; @@ -31,100 +38,94 @@ const makeConfig = ({ ...outputOptions, }; - const commonConfig = defineConfig({ - input: `${source}.ts`, - plugins: [ - resolve({ preferBuiltins: isNodeOnlyBuild }), - typescript({ - typescript: ts, - tsconfigDefaults: { - exclude: ["./tests", "**/*.spec.ts", "**/*.test.ts", "./dist"], - }, - tsconfigOverride: { + /** @type{import("rollup-plugin-esbuild").Options} */ + const esBuildPluginOptions = { + loaders: { ".json": "json" }, + }; + + /** @type{import("@rollup/plugin-commonjs").RollupCommonJSOptions} */ + const commonjsPluginOptions = { + extensions: [".js", ".json"], + }; + + // Non-babel builds + const config = defineConfig([ + // Type definitions (bundled as a single .d.ts file) + { + input, + output: [{ file: `${destination}.d.ts`, format: "es" }], + plugins: [ + dts({ compilerOptions: { - rootDir: "./src", - outDir: "./dist", + ...getTsCompilerOptions(), + noEmit: false, paths: {}, // Do not include files referenced via `paths` }, - }, - }), - json(), - commonjs(), - ], - ...globalOptions, - }); - - /** @type {import("rollup").OutputOptions} */ - const output = [ - { - // Build file to be used as an ES import - file: `${destination}.js`, - format: "esm", - ...outputOptions, + }), + ], }, + + // Module builds { - // Build commonjs module (for tools that do not fully support ES6 modules) - file: `${destination}.cjs`, - format: "cjs", - ...outputOptions, + ...globalOptions, + input, + plugins: [ + externals(), + esbuild({ ...esBuildPluginOptions, target: "node18" }), + commonjs(commonjsPluginOptions), + ], + output: [ + { file: `${destination}.js`, format: "esm", ...outputOptions }, + { file: `${destination}.cjs`, format: "cjs", ...outputOptions }, + ], }, - ]; + ]); - let sourcemapBaseUrl; if (!isNodeOnlyBuild) { // In builds that are published to NPM (potentially every CI build), point to sourcemaps via the // package's canonical unpkg URL + let sourcemapBaseUrl; if (process.env.CI) { const { name, version } = getPackageInfo(); sourcemapBaseUrl = `https://unpkg.com/${name}@${version}/${destinationDirectory}/`; } - output.push({ - // Build file to be used for tinkering in modern browsers - file: `${destination}.browser.js`, - format: "iife", - sourcemapBaseUrl, - ...outputOptions, - ...iifeOutputOptions, + // IIFE build for tinkering in modern browsers + config.push({ + ...globalOptions, + input, + plugins: [ + externals({ deps: false }), + resolve({ preferBuiltins: false }), + esbuild({ ...esBuildPluginOptions, target: "esnext" }), + commonjs(commonjsPluginOptions), + ], + output: { + file: `${destination}.browser.js`, + format: "iife", + sourcemapBaseUrl, + ...outputOptions, + ...iifeOutputOptions, + }, }); - } - // Non-babel builds - const config = defineConfig([{ ...commonConfig, output }]); - - if (!isNodeOnlyBuild) { - // Babel build + // Minified production IIFE build config.push({ - ...commonConfig, + ...globalOptions, + input, plugins: [ - // Import `regenerator-runtime` if requested: - replace({ - values: { - "// __rollup-babel-import-regenerator-runtime__": - 'import "regenerator-runtime/runtime.js";', - }, - delimiters: ["", ""], - preventAssignment: true, - }), - ...commonConfig.plugins, - babel({ - babelHelpers: "bundled", - extends: "@jspsych/config/babel", - // https://github.com/ezolenko/rollup-plugin-typescript2#rollupplugin-babel - extensions: [...babelDefaultExtensions, ".ts"], - }), - ], - output: [ - { - // Minified production build file - file: `${destination}.browser.min.js`, - format: "iife", - plugins: [terser()], - sourcemapBaseUrl, - ...outputOptions, - ...iifeOutputOptions, - }, + externals({ deps: false }), + resolve({ preferBuiltins: false }), + esbuild({ ...esBuildPluginOptions, target: "es2015", minify: true }), + commonjs(commonjsPluginOptions), ], + output: { + file: `${destination}.browser.min.js`, + format: "iife", + sourcemapBaseUrl, + ...outputOptions, + ...iifeOutputOptions, + }, }); } diff --git a/packages/config/tsconfig.core.json b/packages/config/tsconfig.core.json index 50fedd53e3..b2be38c9b3 100644 --- a/packages/config/tsconfig.core.json +++ b/packages/config/tsconfig.core.json @@ -7,8 +7,7 @@ "jspsych": ["../jspsych/src"], "@jspsych/*": ["../*/src"] }, - // allow resolving json modules in tests (needed for transitive imports of jspsych in tests; - // the jspsych package itself uses https://stackoverflow.com/a/61426303 instead) + // allow resolving json modules in tests (needed for transitive imports of jspsych) "resolveJsonModule": true } } diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json index dd88bc8ca6..8446400eb8 100644 --- a/packages/config/tsconfig.json +++ b/packages/config/tsconfig.json @@ -1,33 +1,21 @@ { - // shared base tsconfig for all jsPsych packages - // based on https://github.com/formium/tsdx/blob/462af2d002987f985695b98400e0344b8f2754b7/templates/basic/tsconfig.json - // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs "compilerOptions": { "target": "ES6", "module": "ESNext", "lib": ["dom", "esnext"], "importHelpers": true, - // output .d.ts declaration files for consumers "declaration": true, - // output .js.map sourcemap files for consumers "sourceMap": true, - // stricter type-checking for stronger correctness. Recommended by TS "strict": false, // should be enabled one lucky day - // linter checks for common issues "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative "noUnusedLocals": false, // should be enabled one lucky day "noUnusedParameters": false, // should be enabled one lucky day - // use Node's module resolution algorithm, instead of the legacy TS one "moduleResolution": "node", - // interop between ESM and CJS modules. Recommended by TS "esModuleInterop": true, - // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS "skipLibCheck": true, - // error out if import and file system have a casing mismatch. Recommended by TS "forceConsistentCasingInFileNames": true, - // do not emit build output when running `tsc` - "noEmit": true + "noEmit": true, + "isolatedModules": true // required by Sucrase } } diff --git a/packages/extension-mouse-tracking/src/index.spec.ts b/packages/extension-mouse-tracking/src/index.spec.ts index 0afbfd1edf..21177b596f 100644 --- a/packages/extension-mouse-tracking/src/index.spec.ts +++ b/packages/extension-mouse-tracking/src/index.spec.ts @@ -20,19 +20,15 @@ describe("Mouse Tracking Extension", () => { }, ]; - const { displayElement, getHTML, getData, expectFinished } = await startTimeline( - timeline, - jsPsych - ); + const { displayElement, getData, expectFinished } = await startTimeline(timeline, jsPsych); const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); - mouseMove(50, 50, displayElement.querySelector("#target")); - mouseMove(55, 50, displayElement.querySelector("#target")); - mouseMove(60, 50, displayElement.querySelector("#target")); - - pressKey("a"); + await mouseMove(50, 50, displayElement.querySelector("#target")); + await mouseMove(55, 50, displayElement.querySelector("#target")); + await mouseMove(60, 50, displayElement.querySelector("#target")); + await pressKey("a"); await expectFinished(); expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ @@ -70,19 +66,15 @@ describe("Mouse Tracking Extension", () => { }, ]; - const { displayElement, getHTML, getData, expectFinished } = await startTimeline( - timeline, - jsPsych - ); + const { displayElement, getData, expectFinished } = await startTimeline(timeline, jsPsych); const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); - mouseDown(50, 50, displayElement.querySelector("#target")); - mouseDown(55, 50, displayElement.querySelector("#target")); - mouseDown(60, 50, displayElement.querySelector("#target")); - - pressKey("a"); + await mouseDown(50, 50, displayElement.querySelector("#target")); + await mouseDown(55, 50, displayElement.querySelector("#target")); + await mouseDown(60, 50, displayElement.querySelector("#target")); + await pressKey("a"); await expectFinished(); expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ @@ -120,19 +112,15 @@ describe("Mouse Tracking Extension", () => { }, ]; - const { displayElement, getHTML, getData, expectFinished } = await startTimeline( - timeline, - jsPsych - ); + const { displayElement, getData, expectFinished } = await startTimeline(timeline, jsPsych); const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); - mouseUp(50, 50, displayElement.querySelector("#target")); - mouseUp(55, 50, displayElement.querySelector("#target")); - mouseUp(60, 50, displayElement.querySelector("#target")); - - pressKey("a"); + await mouseUp(50, 50, displayElement.querySelector("#target")); + await mouseUp(55, 50, displayElement.querySelector("#target")); + await mouseUp(60, 50, displayElement.querySelector("#target")); + await pressKey("a"); await expectFinished(); expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ @@ -170,19 +158,15 @@ describe("Mouse Tracking Extension", () => { }, ]; - const { displayElement, getHTML, getData, expectFinished } = await startTimeline( - timeline, - jsPsych - ); + const { displayElement, getData, expectFinished } = await startTimeline(timeline, jsPsych); const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); - mouseMove(50, 50, displayElement.querySelector("#target")); - mouseMove(55, 50, displayElement.querySelector("#target")); - mouseDown(60, 50, displayElement.querySelector("#target")); - - pressKey("a"); + await mouseMove(50, 50, displayElement.querySelector("#target")); + await mouseMove(55, 50, displayElement.querySelector("#target")); + await mouseDown(60, 50, displayElement.querySelector("#target")); + await pressKey("a"); await expectFinished(); expect(getData().values()[0].mouse_tracking_data.length).toBe(1); @@ -212,16 +196,12 @@ describe("Mouse Tracking Extension", () => { }, ]; - const { displayElement, getHTML, getData, expectFinished } = await startTimeline( - timeline, - jsPsych - ); + const { displayElement, getData, expectFinished } = await startTimeline(timeline, jsPsych); const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); const target2Rect = displayElement.querySelector("#target2").getBoundingClientRect(); - pressKey("a"); - + await pressKey("a"); await expectFinished(); expect(getData().values()[0].mouse_tracking_targets["#target"]).toEqual(targetRect); @@ -241,25 +221,21 @@ describe("Mouse Tracking Extension", () => { }, ]; - const { displayElement, getHTML, getData, expectFinished } = await startTimeline( - timeline, - jsPsych - ); + const { displayElement, getData, expectFinished } = await startTimeline(timeline, jsPsych); const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); - mouseMove(50, 50, displayElement.querySelector("#target")); + await mouseMove(50, 50, displayElement.querySelector("#target")); jest.advanceTimersByTime(50); // this one should be ignored - mouseMove(55, 50, displayElement.querySelector("#target")); + await mouseMove(55, 50, displayElement.querySelector("#target")); jest.advanceTimersByTime(50); // this one should register - mouseMove(60, 50, displayElement.querySelector("#target")); - - pressKey("a"); + await mouseMove(60, 50, displayElement.querySelector("#target")); + await pressKey("a"); await expectFinished(); expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts index e251b5bcb8..cb2aa81c15 100644 --- a/packages/extension-mouse-tracking/src/index.ts +++ b/packages/extension-mouse-tracking/src/index.ts @@ -1,4 +1,6 @@ -import { JsPsych, JsPsychExtension, JsPsychExtensionInfo } from "jspsych"; +import { JsPsych, JsPsychExtension, JsPsychExtensionInfo, ParameterType } from "jspsych"; + +import { version } from "../package.json"; interface InitializeParameters { /** @@ -24,9 +26,75 @@ interface OnStartParameters { events?: Array; } +/** + * https://www.jspsych.org/latest/extensions/mouse-tracking/ + */ class MouseTrackingExtension implements JsPsychExtension { static info: JsPsychExtensionInfo = { name: "mouse-tracking", + version: version, + data: { + /** + * An array of objects containing mouse movement data for the trial. Each object has an `x`, a `y`, a `t`, and an + * `event` property. The `x` and `y` properties specify the mouse coordinates in pixels relative to the top left + * corner of the viewport and `t` specifies the time in milliseconds since the start of the trial. The `event` + * will be either 'mousemove', 'mousedown', or 'mouseup' depending on which event was generated. + */ + mouse_tracking_data: { + type: ParameterType.COMPLEX, + array: true, + nested: { + x: { + type: ParameterType.INT, + }, + y: { + type: ParameterType.INT, + }, + t: { + type: ParameterType.INT, + }, + event: { + type: ParameterType.STRING, + }, + }, + }, + /** + * An object contain the pixel coordinates of elements on the screen specified by the `.targets` parameter. Each key + * in this object will be a `selector` property, containing the CSS selector string used to find the element. The object + * corresponding to each key will contain `x` and `y` properties specifying the top-left corner of the object, `width` + * and `height` values, plus `top`, `bottom`, `left`, and `right` parameters which specify the + * [bounding rectangle](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) of the element. + */ + mouse_tracking_targets: { + type: ParameterType.COMPLEX, + nested: { + x: { + type: ParameterType.INT, + }, + y: { + type: ParameterType.INT, + }, + width: { + type: ParameterType.INT, + }, + height: { + type: ParameterType.INT, + }, + top: { + type: ParameterType.INT, + }, + bottom: { + type: ParameterType.INT, + }, + left: { + type: ParameterType.INT, + }, + right: { + type: ParameterType.INT, + }, + }, + }, + }, }; constructor(private jsPsych: JsPsych) {} @@ -87,6 +155,8 @@ class MouseTrackingExtension implements JsPsychExtension { } return { + extension_type: "mouse-tracking", + extension_version: version, mouse_tracking_data: this.currentTrialData, mouse_tracking_targets: Object.fromEntries(this.currentTrialTargets.entries()), }; diff --git a/packages/extension-record-video/src/index.ts b/packages/extension-record-video/src/index.ts index d2c316c53b..1774cfdbb9 100644 --- a/packages/extension-record-video/src/index.ts +++ b/packages/extension-record-video/src/index.ts @@ -1,9 +1,21 @@ import autoBind from "auto-bind"; -import { JsPsych, JsPsychExtension, JsPsychExtensionInfo } from "jspsych"; +import { JsPsych, JsPsychExtension, JsPsychExtensionInfo, ParameterType } from "jspsych"; +import { version } from "../package.json"; + +/** + * https://www.jspsych.org/latest/extensions/record-video/ + */ class RecordVideoExtension implements JsPsychExtension { static info: JsPsychExtensionInfo = { name: "record-video", + version: version, + data: { + /** [Base 64 encoded](https://developer.mozilla.org/en-US/docs/Glossary/Base64) representation of the video data. */ + record_video_data: { + type: ParameterType.STRING, + }, + }, }; constructor(private jsPsych: JsPsych) { diff --git a/packages/extension-webgazer/src/index.ts b/packages/extension-webgazer/src/index.ts index 67d554634d..2f0f7368f2 100644 --- a/packages/extension-webgazer/src/index.ts +++ b/packages/extension-webgazer/src/index.ts @@ -1,4 +1,6 @@ -import { JsPsych, JsPsychExtension, JsPsychExtensionInfo } from "jspsych"; +import { JsPsych, JsPsychExtension, JsPsychExtensionInfo, ParameterType } from "jspsych"; + +import { version } from "../package.json"; // we have to add webgazer to the global window object because webgazer attaches itself to // the window when it loads @@ -39,9 +41,56 @@ interface OnStartParameters { targets: Array; } +/** + * https://www.jspsych.org/latest/extensions/webgazer/ + */ class WebGazerExtension implements JsPsychExtension { static info: JsPsychExtensionInfo = { name: "webgazer", + version: version, + data: { + /** An array of objects containing gaze data for the trial. Each object has an `x`, a `y`, and a `t` property. The `x` and + * `y` properties specify the gaze location in pixels and `t` specifies the time in milliseconds since the start of the trial. + */ + webgazer_data: { + type: ParameterType.INT, + array: true, + }, + /** An object contain the pixel coordinates of elements on the screen specified by the `.targets` parameter. Each key in this + * object will be a `selector` property, containing the CSS selector string used to find the element. The object corresponding + * to each key will contain `x` and `y` properties specifying the top-left corner of the object, `width` and `height` values, + * plus `top`, `bottom`, `left`, and `right` parameters which specify the [bounding rectangle](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) of the element. + */ + webgazer_targets: { + type: ParameterType.COMPLEX, + nested: { + x: { + type: ParameterType.INT, + }, + y: { + type: ParameterType.INT, + }, + width: { + type: ParameterType.INT, + }, + height: { + type: ParameterType.INT, + }, + top: { + type: ParameterType.INT, + }, + bottom: { + type: ParameterType.INT, + }, + left: { + type: ParameterType.INT, + }, + right: { + type: ParameterType.INT, + }, + }, + }, + }, }; constructor(private jsPsych: JsPsych) {} @@ -162,6 +211,8 @@ class WebGazerExtension implements JsPsychExtension { // send back the gazeData return { + extension_type: "webgazer", + extension_version: version, webgazer_data: this.currentTrialData, webgazer_targets: this.currentTrialTargets, }; diff --git a/packages/jspsych/global.d.ts b/packages/jspsych/global.d.ts deleted file mode 100644 index 675741fa1b..0000000000 --- a/packages/jspsych/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module "*.json"; // https://stackoverflow.com/a/61426303 diff --git a/packages/jspsych/package.json b/packages/jspsych/package.json index 6be73eac6d..905a82b88d 100644 --- a/packages/jspsych/package.json +++ b/packages/jspsych/package.json @@ -43,7 +43,8 @@ "dependencies": { "auto-bind": "^4.0.0", "random-words": "^1.1.1", - "seedrandom": "^3.0.5" + "seedrandom": "^3.0.5", + "type-fest": "^2.9.0" }, "devDependencies": { "@fontsource/open-sans": "4.5.3", @@ -53,11 +54,11 @@ "css-loader": "^6.6.0", "mini-css-extract-plugin": "^2.5.3", "npm-run-all": "^4.1.5", + "replace-in-file-webpack-plugin": "^1.0.6", "sass": "^1.43.5", "sass-loader": "^12.4.0", "webpack": "^5.76.0", "webpack-cli": "^4.9.2", - "webpack-remove-empty-scripts": "^0.7.2", - "replace-in-file-webpack-plugin": "^1.0.6" + "webpack-remove-empty-scripts": "^0.7.2" } } diff --git a/packages/jspsych/src/ExtensionManager.spec.ts b/packages/jspsych/src/ExtensionManager.spec.ts new file mode 100644 index 0000000000..8f634a4bbe --- /dev/null +++ b/packages/jspsych/src/ExtensionManager.spec.ts @@ -0,0 +1,123 @@ +import { Class } from "type-fest"; + +import { TestExtension } from "../tests/extensions/test-extension"; +import { ExtensionManager, ExtensionManagerDependencies } from "./ExtensionManager"; +import { JsPsych } from "./JsPsych"; +import { JsPsychExtension } from "./modules/extensions"; + +jest.mock("../src/JsPsych"); + +export class ExtensionManagerDependenciesMock implements ExtensionManagerDependencies { + instantiateExtension: jest.Mock; + + jsPsych: JsPsych; // to be passed to extensions by `instantiateExtension` + + constructor() { + this.initializeProperties(); + } + + private initializeProperties() { + this.instantiateExtension = jest.fn( + (extensionClass: Class) => new extensionClass(this.jsPsych) + ); + + this.jsPsych = new JsPsych(); + } + + reset() { + this.initializeProperties(); + } +} + +const dependencies = new ExtensionManagerDependenciesMock(); +afterEach(() => { + dependencies.reset(); +}); + +describe("ExtensionManager", () => { + it("instantiates all extensions upon construction", () => { + new ExtensionManager(dependencies, [{ type: TestExtension }]); + + expect(dependencies.instantiateExtension).toHaveBeenCalledTimes(1); + expect(dependencies.instantiateExtension).toHaveBeenCalledWith(TestExtension); + }); + + it("exposes extensions via the `extensions` property", () => { + const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]); + + expect(manager.extensions).toEqual({ test: expect.any(TestExtension) }); + }); + + describe("initialize()", () => { + it("calls `initialize` on all extensions, providing the parameters from the constructor", async () => { + const manager = new ExtensionManager(dependencies, [ + { type: TestExtension, params: { option: 1 } }, + ]); + + await manager.initializeExtensions(); + + expect(manager.extensions.test.initialize).toHaveBeenCalledTimes(1); + expect(manager.extensions.test.initialize).toHaveBeenCalledWith({ option: 1 }); + }); + + it("defaults `params` to an empty object", async () => { + const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]); + + await manager.initializeExtensions(); + expect(manager.extensions.test.initialize).toHaveBeenCalledWith({}); + }); + }); + + describe("onStart()", () => { + it("calls `on_start` on all extensions specified in the provided `extensions` parameter", () => { + const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]); + + const onStartCallback = jest.mocked(manager.extensions.test.on_start); + + manager.onStart(); + expect(onStartCallback).not.toHaveBeenCalled(); + + manager.onStart([{ type: TestExtension, params: { my: "option" } }]); + expect(onStartCallback).toHaveBeenCalledWith({ my: "option" }); + }); + }); + + describe("onLoad()", () => { + it("calls `on_load` on all extensions specified in the provided `extensions` parameter", () => { + const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]); + + const onLoadCallback = jest.mocked(manager.extensions.test.on_load); + + manager.onLoad(); + expect(onLoadCallback).not.toHaveBeenCalled(); + + manager.onLoad([{ type: TestExtension, params: { my: "option" } }]); + expect(onLoadCallback).toHaveBeenCalledWith({ my: "option" }); + }); + }); + + describe("onFinish()", () => { + it("calls `on_finish` on all extensions specified in the provided `extensions` parameter and returns a joint extension results object", async () => { + const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]); + + const onFinishCallback = jest.mocked(manager.extensions.test.on_finish); + onFinishCallback.mockReturnValue({ + extension_type: "test", + extension_version: "1.0", + extension: "result", + }); + + let results = await manager.onFinish(undefined); + expect(onFinishCallback).not.toHaveBeenCalled(); + expect(results).toEqual({}); + + results = await manager.onFinish([{ type: TestExtension, params: { my: "option" } }]); + expect(onFinishCallback).toHaveBeenCalledWith({ my: "option" }); + expect(results).toEqual({ + extension_type: ["test"], + extension_version: ["1.0"], + extension: "result", + }); + }); + }); +}); diff --git a/packages/jspsych/src/ExtensionManager.ts b/packages/jspsych/src/ExtensionManager.ts new file mode 100644 index 0000000000..75802759d2 --- /dev/null +++ b/packages/jspsych/src/ExtensionManager.ts @@ -0,0 +1,81 @@ +import { Class } from "type-fest"; + +import { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions"; +import { TrialExtensionsConfiguration } from "./timeline"; + +export type GlobalExtensionsConfiguration = Array<{ + type: Class; + params?: Record; +}>; + +export interface ExtensionManagerDependencies { + /** + * Given an extension class, create a new instance of it and return it. + */ + instantiateExtension(extensionClass: Class): JsPsychExtension; +} + +export class ExtensionManager { + private static getExtensionNameByClass(extensionClass: Class) { + return (extensionClass["info"] as JsPsychExtensionInfo).name; + } + + public readonly extensions: Record; + + constructor( + private dependencies: ExtensionManagerDependencies, + private extensionsConfiguration: GlobalExtensionsConfiguration + ) { + this.extensions = Object.fromEntries( + extensionsConfiguration.map((extension) => [ + ExtensionManager.getExtensionNameByClass(extension.type), + this.dependencies.instantiateExtension(extension.type), + ]) + ); + } + + private getExtensionInstanceByClass(extensionClass: Class) { + return this.extensions[ExtensionManager.getExtensionNameByClass(extensionClass)]; + } + + public async initializeExtensions() { + await Promise.all( + this.extensionsConfiguration.map(({ type, params = {} }) => + this.getExtensionInstanceByClass(type).initialize(params) + ) + ); + } + + public onStart(trialExtensionsConfiguration: TrialExtensionsConfiguration = []) { + for (const { type, params } of trialExtensionsConfiguration) { + this.getExtensionInstanceByClass(type)?.on_start(params); + } + } + + public onLoad(trialExtensionsConfiguration: TrialExtensionsConfiguration = []) { + for (const { type, params } of trialExtensionsConfiguration) { + this.getExtensionInstanceByClass(type)?.on_load(params); + } + } + + public async onFinish( + trialExtensionsConfiguration: TrialExtensionsConfiguration = [] + ): Promise> { + const results = await Promise.all( + trialExtensionsConfiguration.map(({ type, params }) => + Promise.resolve(this.getExtensionInstanceByClass(type)?.on_finish(params)) + ) + ); + + const extensionInfo = trialExtensionsConfiguration.length + ? { + extension_type: results.map((result) => result.extension_type), + extension_version: results.map((result) => result.extension_version), + } + : {}; + + results.push(extensionInfo); + + return Object.assign({}, ...results); + } +} diff --git a/packages/jspsych/src/JsPsych.ts b/packages/jspsych/src/JsPsych.ts index b1395318dd..c6e63013ac 100644 --- a/packages/jspsych/src/JsPsych.ts +++ b/packages/jspsych/src/JsPsych.ts @@ -1,21 +1,27 @@ import autoBind from "auto-bind"; import { version } from "../package.json"; -import { MigrationError } from "./migration"; -import { JsPsychData } from "./modules/data"; +import { ExtensionManager, ExtensionManagerDependencies } from "./ExtensionManager"; +import { JsPsychData, JsPsychDataDependencies } from "./modules/data"; import { PluginAPI, createJointPluginAPIObject } from "./modules/plugin-api"; -import { ParameterType, universalPluginParameters } from "./modules/plugins"; import * as randomization from "./modules/randomization"; import * as turk from "./modules/turk"; import * as utils from "./modules/utils"; -import { TimelineNode } from "./TimelineNode"; - -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} +import { ProgressBar } from "./ProgressBar"; +import { + SimulationMode, + SimulationOptionsParameter, + TimelineArray, + TimelineDescription, + TimelineNodeDependencies, + TimelineVariable, + TrialResult, +} from "./timeline"; +import { Timeline } from "./timeline/Timeline"; +import { Trial } from "./timeline/Trial"; +import { PromiseWrapper } from "./timeline/util"; export class JsPsych { - extensions = {}; turk = turk; randomization = randomization; utils = utils; @@ -26,75 +32,32 @@ export class JsPsych { return version; } - // - // private variables - // + /** Options */ + private options: any = {}; - /** - * options - */ - private opts: any = {}; + /** Experiment timeline */ + private timeline?: Timeline; - /** - * experiment timeline - */ - private timeline: TimelineNode; - private timelineDescription: any[]; + /** Target DOM element */ + private displayContainerElement: HTMLElement; + private displayElement: HTMLElement; - // flow control - private global_trial_index = 0; - private current_trial: any = {}; - private current_trial_finished = false; - - // target DOM element - private DOM_container: HTMLElement; - private DOM_target: HTMLElement; + /** Time that the experiment began */ + private experimentStartTime: Date; /** - * time that the experiment began + * Whether the page is retrieved directly via the `file://` protocol (true) or hosted on a web + * server (false) */ - private exp_start_time; + private isFileProtocolUsed = false; - /** - * is the experiment paused? - */ - private paused = false; - private waiting = false; + /** The simulation mode (if the experiment is being simulated) */ + private simulationMode?: SimulationMode; - /** - * is the page retrieved directly via file:// protocol (true) or hosted on a server (false)? - */ - private file_protocol = false; + /** Simulation options passed in via `simulate()` */ + private simulationOptions: Record; - /** - * Promise that is resolved when `finishExperiment()` is called - */ - 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; - - internal = { - /** - * this flag is used to determine whether we are in a scope where - * jsPsych.timelineVariable() should be executed immediately or - * whether it should return a function to access the variable later. - * - **/ - call_immediate: false, - }; + private extensionManager: ExtensionManager; constructor(options?) { // override default options if user specifies an option @@ -107,7 +70,6 @@ export class JsPsych { on_interaction_data_update: () => {}, on_close: () => {}, use_webaudio: true, - exclusions: {}, show_progress_bar: false, message_progress_bar: "Completion Progress", auto_update_progress_bar: true, @@ -119,22 +81,18 @@ export class JsPsych { extensions: [], ...options, }; - this.opts = options; + this.options = options; autoBind(this); // so we can pass JsPsych methods as callbacks and `this` remains the JsPsych instance - this.webaudio_context = - typeof window !== "undefined" && typeof window.AudioContext !== "undefined" - ? new AudioContext() - : null; - - // detect whether page is running in browser as a local file, and if so, disable web audio and video preloading to prevent CORS issues + // detect whether page is running in browser as a local file, and if so, disable web audio and + // video preloading to prevent CORS issues if ( window.location.protocol == "file:" && (options.override_safe_mode === false || typeof options.override_safe_mode === "undefined") ) { options.use_webaudio = false; - this.file_protocol = true; + this.isFileProtocolUsed = true; console.warn( "jsPsych detected that it is running via the file:// protocol and not on a web server. " + "To prevent issues with cross-origin requests, Web Audio and video preloading have been disabled. " + @@ -144,27 +102,26 @@ export class JsPsych { } // initialize modules - this.data = new JsPsychData(this); + this.data = new JsPsychData(this.dataDependencies); this.pluginAPI = createJointPluginAPIObject(this); - // create instances of extensions - for (const extension of options.extensions) { - this.extensions[extension.type.info.name] = new extension.type(this); - } - - // initialize audio context based on options and browser capabilities - this.pluginAPI.initAudio(); + this.extensionManager = new ExtensionManager( + this.extensionManagerDependencies, + options.extensions + ); } + private endMessage?: string; + /** * Starts an experiment using the provided timeline and returns a promise that is resolved when * the experiment is finished. * * @param timeline The timeline to be run */ - async run(timeline: any[]) { + async run(timeline: TimelineDescription | TimelineArray) { if (typeof timeline === "undefined") { - console.error("No timeline declared in jsPsych.run. Cannot start experiment."); + console.error("No timeline declared in jsPsych.run(). Cannot start experiment."); } if (timeline.length === 0) { @@ -174,17 +131,23 @@ export class JsPsych { } // create experiment timeline - this.timelineDescription = timeline; - this.timeline = new TimelineNode(this, { timeline }); + this.timeline = new Timeline(this.timelineDependencies, timeline); await this.prepareDom(); - await this.checkExclusions(this.opts.exclusions); - await this.loadExtensions(this.opts.extensions); + await this.extensionManager.initializeExtensions(); document.documentElement.setAttribute("jspsych", "present"); - this.startExperiment(); - await this.finished; + this.experimentStartTime = new Date(); + + await this.timeline.run(); + await Promise.resolve(this.options.on_finish(this.data.get())); + + if (this.endMessage) { + this.getDisplayElement().innerHTML = this.endMessage; + } + + this.data.removeInteractionListeners(); } async simulate( @@ -192,220 +155,110 @@ export class JsPsych { simulation_mode: "data-only" | "visual" = "data-only", simulation_options = {} ) { - this.simulation_mode = simulation_mode; - this.simulation_options = simulation_options; + this.simulationMode = simulation_mode; + this.simulationOptions = simulation_options; await this.run(timeline); } + public progressBar?: ProgressBar; + getProgress() { return { - total_trials: typeof this.timeline === "undefined" ? undefined : this.timeline.length(), - current_trial_global: this.global_trial_index, - percent_complete: typeof this.timeline === "undefined" ? 0 : this.timeline.percentComplete(), + total_trials: this.timeline?.getNaiveTrialCount(), + current_trial_global: this.timeline?.getLatestNode().index ?? 0, + percent_complete: this.timeline?.getNaiveProgress() * 100, }; } getStartTime() { - return this.exp_start_time; + return this.experimentStartTime; // TODO This seems inconsistent, given that `getTotalTime()` returns a number, not a `Date` } getTotalTime() { - if (typeof this.exp_start_time === "undefined") { + if (!this.experimentStartTime) { return 0; } - return new Date().getTime() - this.exp_start_time.getTime(); + return new Date().getTime() - this.experimentStartTime.getTime(); } getDisplayElement() { - return this.DOM_target; + return this.displayElement; } getDisplayContainerElement() { - return this.DOM_container; + return this.displayContainerElement; } - finishTrial(data = {}) { - if (this.current_trial_finished) { - return; - } - this.current_trial_finished = true; - - // remove any CSS classes that were added to the DOM via css_classes parameter - if ( - typeof this.current_trial.css_classes !== "undefined" && - Array.isArray(this.current_trial.css_classes) - ) { - this.DOM_target.classList.remove(...this.current_trial.css_classes); - } - - // write the data from the trial - this.data.write(data); - - // get back the data with all of the defaults in - const trial_data = this.data.getLastTrialData(); - - // for trial-level callbacks, we just want to pass in a reference to the values - // of the DataCollection, for easy access and editing. - const trial_data_values = trial_data.values()[0]; - - const current_trial = this.current_trial; - - if (typeof current_trial.save_trial_parameters === "object") { - for (const key of Object.keys(current_trial.save_trial_parameters)) { - const key_val = current_trial.save_trial_parameters[key]; - if (key_val === true) { - if (typeof current_trial[key] === "undefined") { - console.warn( - `Invalid parameter specified in save_trial_parameters. Trial has no property called "${key}".` - ); - } else if (typeof current_trial[key] === "function") { - trial_data_values[key] = current_trial[key].toString(); - } else { - trial_data_values[key] = current_trial[key]; - } - } - if (key_val === false) { - // we don't allow internal_node_id or trial_index to be deleted because it would break other things - if (key !== "internal_node_id" && key !== "trial_index") { - delete trial_data_values[key]; - } - } - } - } - - // handle extension callbacks - - const extensionCallbackResults = ((current_trial.extensions ?? []) as any[]).map((extension) => - this.extensions[extension.type.info.name].on_finish(extension.params) - ); - - const onExtensionCallbacksFinished = () => { - // about to execute lots of callbacks, so switch context. - this.internal.call_immediate = true; - - // handle callback at plugin level - if (typeof current_trial.on_finish === "function") { - current_trial.on_finish(trial_data_values); - } - - // handle callback at whole-experiment level - this.opts.on_trial_finish(trial_data_values); - - // after the above callbacks are complete, then the data should be finalized - // for this trial. call the on_data_update handler, passing in the same - // data object that just went through the trial's finish handlers. - this.opts.on_data_update(trial_data_values); - - // done with callbacks - this.internal.call_immediate = false; - - // wait for iti - if (this.simulation_mode === "data-only") { - this.nextTrial(); - } else if ( - typeof current_trial.post_trial_gap === null || - typeof current_trial.post_trial_gap === "undefined" - ) { - if (this.opts.default_iti > 0) { - setTimeout(this.nextTrial, this.opts.default_iti); - } else { - this.nextTrial(); - } - } else { - if (current_trial.post_trial_gap > 0) { - setTimeout(this.nextTrial, current_trial.post_trial_gap); - } else { - this.nextTrial(); - } - } - }; - - // Strictly using Promise.resolve to turn all values into promises would be cleaner here, but it - // would require user test code to make the event loop tick after every simulated key press even - // if there are no async `on_finish` methods. Hence, in order to avoid a breaking change, we - // only rely on the event loop if at least one `on_finish` method returns a promise. - if (extensionCallbackResults.some((result) => typeof result.then === "function")) { - Promise.all( - extensionCallbackResults.map((result) => - Promise.resolve(result).then((ext_data_values) => { - Object.assign(trial_data_values, ext_data_values); - }) - ) - ).then(onExtensionCallbacksFinished); - } else { - for (const values of extensionCallbackResults) { - Object.assign(trial_data_values, values); - } - onExtensionCallbacksFinished(); - } - } - - endExperiment(end_message = "", data = {}) { - this.timeline.end_message = end_message; - this.timeline.end(); + abortExperiment(endMessage?: string, data = {}) { + this.endMessage = endMessage; + this.timeline.abort(); this.pluginAPI.cancelAllKeyboardResponses(); this.pluginAPI.clearAllTimeouts(); this.finishTrial(data); } - endCurrentTimeline() { - this.timeline.endActiveNode(); - } - - getCurrentTrial() { - return this.current_trial; + abortCurrentTimeline() { + let currentTimeline = this.timeline?.getLatestNode(); + if (currentTimeline instanceof Trial) { + currentTimeline = currentTimeline.parent; + } + if (currentTimeline instanceof Timeline) { + currentTimeline.abort(); + } } - getInitSettings() { - return this.opts; + /** + * Aborts a named timeline. The timeline must be currently running in order to abort it. + * + * @param name The name of the timeline to abort. Timelines can be given names by setting the `name` parameter in the description of the timeline. + */ + abortTimelineByName(name: string): void { + const timeline = this.timeline?.getActiveTimelineByName(name); + if (timeline) { + timeline.abort(); + } } - getCurrentTimelineNodeID() { - return this.timeline.activeID(); + getCurrentTrial() { + const activeNode = this.timeline?.getLatestNode(); + if (activeNode instanceof Trial) { + return activeNode.description; + } + return undefined; } - timelineVariable(varname: string, immediate = false) { - if (this.internal.call_immediate || immediate === true) { - return this.timeline.timelineVariable(varname); - } else { - return { - timelineVariablePlaceholder: true, - timelineVariableFunction: () => this.timeline.timelineVariable(varname), - }; - } + getInitSettings() { + return this.options; } - getAllTimelineVariables() { - return this.timeline.allTimelineVariables(); + timelineVariable(variableName: string) { + return new TimelineVariable(variableName); } - addNodeToEndOfTimeline(new_timeline, preload_callback?) { - this.timeline.insert(new_timeline); + evaluateTimelineVariable(variableName: string) { + return this.timeline + ?.getLatestNode() + ?.evaluateTimelineVariable(new TimelineVariable(variableName)); } pauseExperiment() { - this.paused = true; + this.timeline?.pause(); } resumeExperiment() { - this.paused = false; - if (this.waiting) { - this.waiting = false; - this.nextTrial(); - } - } - - loadFail(message) { - message = message || "

The experiment failed to load.

"; - this.DOM_target.innerHTML = message; + this.timeline?.resume(); } getSafeModeStatus() { - return this.file_protocol; + return this.isFileProtocolUsed; } getTimeline() { - return this.timelineDescription; + return this.timeline?.description.timeline; + } + + get extensions() { + return this.extensionManager?.extensions ?? {}; } private async prepareDom() { @@ -416,24 +269,25 @@ export class JsPsych { }); } - const options = this.opts; + const options = this.options; // set DOM element where jsPsych will render content // if undefined, then jsPsych will use the tag and the entire page if (typeof options.display_element === "undefined") { // check if there is a body element on the page - const body = document.querySelector("body"); - if (body === null) { - document.documentElement.appendChild(document.createElement("body")); + let body = document.body; + if (!body) { + body = document.createElement("body"); + document.documentElement.appendChild(body); } - // using the full page, so we need the HTML element to - // have 100% height, and body to be full width and height with - // no margin + // using the full page, so we need the HTML element to have 100% height, and body to be full + // width and height with no margin document.querySelector("html").style.height = "100%"; - document.querySelector("body").style.margin = "0px"; - document.querySelector("body").style.height = "100%"; - document.querySelector("body").style.width = "100%"; - options.display_element = document.querySelector("body"); + + body.style.margin = "0px"; + body.style.height = "100%"; + body.style.width = "100%"; + options.display_element = body; } else { // make sure that the display element exists on the page const display = @@ -447,468 +301,119 @@ export class JsPsych { } } - options.display_element.innerHTML = - '
'; - this.DOM_container = options.display_element; - this.DOM_target = document.querySelector("#jspsych-content"); + const contentElement = document.createElement("div"); + contentElement.id = "jspsych-content"; + + const contentWrapperElement = document.createElement("div"); + contentWrapperElement.className = "jspsych-content-wrapper"; + contentWrapperElement.appendChild(contentElement); + + this.displayContainerElement = options.display_element; + this.displayContainerElement.appendChild(contentWrapperElement); + this.displayElement = contentElement; // set experiment_width if not null if (options.experiment_width !== null) { - this.DOM_target.style.width = options.experiment_width + "px"; + this.displayElement.style.width = options.experiment_width + "px"; } // add tabIndex attribute to scope event listeners options.display_element.tabIndex = 0; - // add CSS class to DOM_target - if (options.display_element.className.indexOf("jspsych-display-element") === -1) { - options.display_element.className += " jspsych-display-element"; - } - this.DOM_target.className += "jspsych-content"; + // Add CSS classes to container and display elements + this.displayContainerElement.classList.add("jspsych-display-element"); + this.displayElement.classList.add("jspsych-content"); // create listeners for user browser interaction this.data.createInteractionListeners(); // add event for closing window window.addEventListener("beforeunload", options.on_close); - } - - private async loadExtensions(extensions) { - // run the .initialize method of any extensions that are in use - // these should return a Promise to indicate when loading is complete - - try { - await Promise.all( - extensions.map((extension) => - this.extensions[extension.type.info.name].initialize(extension.params || {}) - ) - ); - } catch (error_message) { - console.error(error_message); - throw new Error(error_message); - } - } - - private startExperiment() { - this.finished = new Promise((resolve) => { - this.resolveFinishedPromise = resolve; - }); - - // show progress bar if requested - if (this.opts.show_progress_bar === true) { - this.drawProgressBar(this.opts.message_progress_bar); - } - // record the start time - this.exp_start_time = new Date(); + if (this.options.show_progress_bar) { + const progressBarContainer = document.createElement("div"); + progressBarContainer.id = "jspsych-progressbar-container"; - // begin! - this.timeline.advance(); - this.doTrial(this.timeline.trial()); - } - - private finishExperiment() { - const finish_result = this.opts.on_finish(this.data.get()); - - const done_handler = () => { - if (typeof this.timeline.end_message !== "undefined") { - this.DOM_target.innerHTML = this.timeline.end_message; - } - this.resolveFinishedPromise(); - }; + this.progressBar = new ProgressBar(progressBarContainer, this.options.message_progress_bar); - if (finish_result) { - Promise.resolve(finish_result).then(done_handler); - } else { - done_handler(); + this.getDisplayContainerElement().insertAdjacentElement("afterbegin", progressBarContainer); } } - private nextTrial() { - // if experiment is paused, don't do anything. - if (this.paused) { - this.waiting = true; - return; - } - - this.global_trial_index++; - - // advance timeline - this.timeline.markCurrentTrialComplete(); - const complete = this.timeline.advance(); - - // update progress bar if shown - if (this.opts.show_progress_bar === true && this.opts.auto_update_progress_bar === true) { - this.updateProgressBar(); - } - - // check if experiment is over - if (complete) { - this.finishExperiment(); - return; - } - - this.doTrial(this.timeline.trial()); + private finishTrialPromise = new PromiseWrapper(); + finishTrial(data?: TrialResult) { + this.finishTrialPromise.resolve(data); } - private doTrial(trial) { - this.current_trial = trial; - this.current_trial_finished = false; + private timelineDependencies: TimelineNodeDependencies = { + onTrialStart: (trial: Trial) => { + this.options.on_trial_start(trial.trialObject); - // process all timeline variables for this trial - this.evaluateTimelineVariables(trial); + // apply the focus to the element containing the experiment. + this.getDisplayContainerElement().focus(); + // reset the scroll on the DOM target + this.getDisplayElement().scrollTop = 0; + }, - if (typeof trial.type === "string") { - throw new MigrationError( - "A string was provided as the trial's `type` parameter. Since jsPsych v7, the `type` parameter needs to be a plugin object." - ); - } - - // instantiate the plugin for this trial - trial.type = { - // this is a hack to internally keep the old plugin object structure and prevent touching more - // of the core jspsych code - ...autoBind(new trial.type(this)), - info: trial.type.info, - }; - - // evaluate variables that are functions - this.evaluateFunctionParameters(trial); - - // get default values for parameters - this.setDefaultValues(trial); - - // about to execute callbacks - this.internal.call_immediate = true; - - // call experiment wide callback - this.opts.on_trial_start(trial); - - // call trial specific callback if it exists - if (typeof trial.on_start === "function") { - trial.on_start(trial); - } - - // call any on_start functions for extensions - if (Array.isArray(trial.extensions)) { - for (const extension of trial.extensions) { - this.extensions[extension.type.info.name].on_start(extension.params); + onTrialResultAvailable: (trial: Trial) => { + const result = trial.getResult(); + if (result) { + result.time_elapsed = this.getTotalTime(); + this.data.write(trial); } - } - - // apply the focus to the element containing the experiment. - this.DOM_container.focus(); + }, - // reset the scroll on the DOM target - this.DOM_target.scrollTop = 0; + onTrialFinished: (trial: Trial) => { + const result = trial.getResult(); + this.options.on_trial_finish(result); - // add CSS classes to the DOM_target if they exist in trial.css_classes - if (typeof trial.css_classes !== "undefined") { - if (!Array.isArray(trial.css_classes) && typeof trial.css_classes === "string") { - trial.css_classes = [trial.css_classes]; - } - if (Array.isArray(trial.css_classes)) { - this.DOM_target.classList.add(...trial.css_classes); + if (result) { + this.options.on_data_update(result); } - } - // setup on_load event callback - const load_callback = () => { - if (typeof trial.on_load === "function") { - trial.on_load(); + if (this.progressBar && this.options.auto_update_progress_bar) { + this.progressBar.progress = this.timeline.getNaiveProgress(); } + }, - // call any on_load functions for extensions - if (Array.isArray(trial.extensions)) { - for (const extension of trial.extensions) { - this.extensions[extension.type.info.name].on_load(extension.params); - } - } - }; + runOnStartExtensionCallbacks: (extensionsConfiguration) => + this.extensionManager.onStart(extensionsConfiguration), - let trial_complete; - let trial_sim_opts; - let trial_sim_opts_merged; - if (!this.simulation_mode) { - trial_complete = trial.type.trial(this.DOM_target, trial, load_callback); - } - if (this.simulation_mode) { - // check if the trial supports simulation - if (trial.type.simulate) { - 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; - } - } - // merge in default options that aren't overriden by the trial's simulation_options - // including nested parameters in the simulation_options - trial_sim_opts_merged = this.utils.deepMerge( - this.simulation_options.default, - trial_sim_opts - ); - - trial_sim_opts_merged = this.utils.deepCopy(trial_sim_opts_merged); - trial_sim_opts_merged = this.replaceFunctionsWithValues(trial_sim_opts_merged, null); - - if (trial_sim_opts_merged?.simulate === false) { - trial_complete = trial.type.trial(this.DOM_target, trial, load_callback); - } else { - trial_complete = trial.type.simulate( - trial, - trial_sim_opts_merged?.mode || this.simulation_mode, - trial_sim_opts_merged, - 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); - } - } + runOnLoadExtensionCallbacks: (extensionsConfiguration) => + this.extensionManager.onLoad(extensionsConfiguration), - // see if trial_complete is a Promise by looking for .then() function - const is_promise = trial_complete && typeof trial_complete.then == "function"; + runOnFinishExtensionCallbacks: (extensionsConfiguration) => + this.extensionManager.onFinish(extensionsConfiguration), - // in simulation mode we let the simulate function call the load_callback always, - // so we don't need to call it here. however, if we are in simulation mode but not simulating - // this particular trial we need to call it. - if ( - !is_promise && - (!this.simulation_mode || (this.simulation_mode && trial_sim_opts_merged?.simulate === false)) - ) { - load_callback(); - } - - // done with callbacks - this.internal.call_immediate = false; - } - - private evaluateTimelineVariables(trial) { - for (const key of Object.keys(trial)) { - if ( - typeof trial[key] === "object" && - trial[key] !== null && - typeof trial[key].timelineVariablePlaceholder !== "undefined" - ) { - trial[key] = trial[key].timelineVariableFunction(); - } - // timeline variables that are nested in objects - if ( - typeof trial[key] === "object" && - trial[key] !== null && - key !== "timeline" && - key !== "timeline_variables" - ) { - this.evaluateTimelineVariables(trial[key]); - } - } - } + getSimulationMode: () => this.simulationMode, - private evaluateFunctionParameters(trial) { - // set a flag so that jsPsych.timelineVariable() is immediately executed in this context - this.internal.call_immediate = true; - - // iterate over each parameter - for (const key of Object.keys(trial)) { - // check to make sure parameter is not "type", since that was eval'd above. - if (key !== "type") { - // this if statement is checking to see if the parameter type is expected to be a function, in which case we should NOT evaluate it. - // the first line checks if the parameter is defined in the universalPluginParameters set - // the second line checks the plugin-specific parameters - if ( - typeof universalPluginParameters[key] !== "undefined" && - universalPluginParameters[key].type !== ParameterType.FUNCTION - ) { - trial[key] = this.replaceFunctionsWithValues(trial[key], null); - } - if ( - typeof trial.type.info.parameters[key] !== "undefined" && - trial.type.info.parameters[key].type !== ParameterType.FUNCTION - ) { - trial[key] = this.replaceFunctionsWithValues(trial[key], trial.type.info.parameters[key]); - } - } - } - // reset so jsPsych.timelineVariable() is no longer immediately executed - this.internal.call_immediate = false; - } + getGlobalSimulationOptions: () => this.simulationOptions, - private replaceFunctionsWithValues(obj, info) { - // null typeof is 'object' (?!?!), so need to run this first! - if (obj === null) { - return obj; - } - // arrays - else if (Array.isArray(obj)) { - for (let i = 0; i < obj.length; i++) { - obj[i] = this.replaceFunctionsWithValues(obj[i], info); - } - } - // objects - else if (typeof obj === "object") { - if (info === null || !info.nested) { - for (const key of Object.keys(obj)) { - if (key === "type" || key === "timeline" || key === "timeline_variables") { - // Ignore the object's `type` field because it contains a plugin and we do not want to - // call plugin functions. Also ignore `timeline` and `timeline_variables` because they - // are used in the `trials` parameter of the preload plugin and we don't want to actually - // evaluate those in that context. - continue; - } - obj[key] = this.replaceFunctionsWithValues(obj[key], null); - } - } else { - for (const key of Object.keys(obj)) { - if ( - typeof info.nested[key] === "object" && - info.nested[key].type !== ParameterType.FUNCTION - ) { - obj[key] = this.replaceFunctionsWithValues(obj[key], info.nested[key]); - } - } - } - } else if (typeof obj === "function") { - return obj(); - } - return obj; - } + instantiatePlugin: (pluginClass) => new pluginClass(this), - private setDefaultValues(trial) { - for (const param in trial.type.info.parameters) { - // check if parameter is complex with nested defaults - if (trial.type.info.parameters[param].type === ParameterType.COMPLEX) { - // check if parameter is undefined and has a default value - if (typeof trial[param] === "undefined" && trial.type.info.parameters[param].default) { - trial[param] = trial.type.info.parameters[param].default; - } - // if parameter is an array, iterate over each entry after confirming that there are - // entries to iterate over. this is common when some parameters in a COMPLEX type have - // default values and others do not. - if (trial.type.info.parameters[param].array === true && Array.isArray(trial[param])) { - // iterate over each entry in the array - trial[param].forEach(function (ip, i) { - // check each parameter in the plugin description - for (const p in trial.type.info.parameters[param].nested) { - if (typeof trial[param][i][p] === "undefined" || trial[param][i][p] === null) { - if (typeof trial.type.info.parameters[param].nested[p].default === "undefined") { - console.error( - `You must specify a value for the ${p} parameter (nested in the ${param} parameter) in the ${trial.type.info.name} plugin.` - ); - } else { - trial[param][i][p] = trial.type.info.parameters[param].nested[p].default; - } - } - } - }); - } - } - // if it's not nested, checking is much easier and do that here: - else if (typeof trial[param] === "undefined" || trial[param] === null) { - if (typeof trial.type.info.parameters[param].default === "undefined") { - console.error( - `You must specify a value for the ${param} parameter in the ${trial.type.info.name} plugin.` - ); - } else { - trial[param] = trial.type.info.parameters[param].default; - } - } - } - } + getDisplayElement: () => this.getDisplayElement(), - private async checkExclusions(exclusions) { - if (exclusions.min_width || exclusions.min_height || exclusions.audio) { - console.warn( - "The exclusions option in `initJsPsych()` is deprecated and will be removed in a future version. We recommend using the browser-check plugin instead. See https://www.jspsych.org/latest/plugins/browser-check/." - ); - } - // MINIMUM SIZE - if (exclusions.min_width || exclusions.min_height) { - const mw = exclusions.min_width || 0; - const mh = exclusions.min_height || 0; - - if (window.innerWidth < mw || window.innerHeight < mh) { - this.getDisplayElement().innerHTML = - "

Your browser window is too small to complete this experiment. " + - "Please maximize the size of your browser window. If your browser window is already maximized, " + - "you will not be able to complete this experiment.

" + - "

The minimum width is " + - mw + - "px. Your current width is " + - window.innerWidth + - "px.

" + - "

The minimum height is " + - mh + - "px. Your current height is " + - window.innerHeight + - "px.

"; - - // Wait for window size to increase - while (window.innerWidth < mw || window.innerHeight < mh) { - await delay(100); - } - - this.getDisplayElement().innerHTML = ""; - } - } + getDefaultIti: () => this.getInitSettings().default_iti, - // WEB AUDIO API - if (typeof exclusions.audio !== "undefined" && exclusions.audio) { - if (!window.hasOwnProperty("AudioContext") && !window.hasOwnProperty("webkitAudioContext")) { - this.getDisplayElement().innerHTML = - "

Your browser does not support the WebAudio API, which means that you will not " + - "be able to complete the experiment.

Browsers that support the WebAudio API include " + - "Chrome, Firefox, Safari, and Edge.

"; - throw new Error(); - } - } - } + finishTrialPromise: this.finishTrialPromise, - private drawProgressBar(msg) { - document - .querySelector(".jspsych-display-element") - .insertAdjacentHTML( - "afterbegin", - '
' + - "" + - msg + - "" + - '
' + - '
' + - "
" - ); - } + clearAllTimeouts: () => this.pluginAPI.clearAllTimeouts(), + }; - private updateProgressBar() { - this.setProgressBar(this.getProgress().percent_complete / 100); - } + private extensionManagerDependencies: ExtensionManagerDependencies = { + instantiateExtension: (extensionClass) => new extensionClass(this), + }; - private progress_bar_amount = 0; + private dataDependencies: JsPsychDataDependencies = { + getProgress: () => ({ + time: this.getTotalTime(), + trial: this.timeline?.getLatestNode().index ?? 0, + }), - setProgressBar(proportion_complete) { - proportion_complete = Math.max(Math.min(1, proportion_complete), 0); - document.querySelector("#jspsych-progressbar-inner").style.width = - proportion_complete * 100 + "%"; - this.progress_bar_amount = proportion_complete; - } + onInteractionRecordAdded: (record) => { + this.options.on_interaction_data_update(record); + }, - getProgressBarCompleted() { - return this.progress_bar_amount; - } + getDisplayElement: () => this.getDisplayElement(), + }; } diff --git a/packages/jspsych/src/ProgressBar.spec.ts b/packages/jspsych/src/ProgressBar.spec.ts new file mode 100644 index 0000000000..acefc985b7 --- /dev/null +++ b/packages/jspsych/src/ProgressBar.spec.ts @@ -0,0 +1,60 @@ +import { ProgressBar } from "./ProgressBar"; + +describe("ProgressBar", () => { + let containerElement: HTMLDivElement; + let progressBar: ProgressBar; + + beforeEach(() => { + containerElement = document.createElement("div"); + progressBar = new ProgressBar(containerElement, "My message"); + }); + + it("sets up proper HTML markup when created", () => { + expect(containerElement.innerHTML).toMatchInlineSnapshot( + '"My message
"' + ); + }); + + describe("progress", () => { + it("updates the bar width accordingly", () => { + expect(progressBar.progress).toEqual(0); + expect(containerElement.innerHTML).toContain('style="width: 0%;"'); + progressBar.progress = 0.5; + expect(progressBar.progress).toEqual(0.5); + expect(containerElement.innerHTML).toContain('style="width: 50%;"'); + + progressBar.progress = 1; + expect(progressBar.progress).toEqual(1); + expect(containerElement.innerHTML).toContain('style="width: 100%;"'); + }); + + it("errors if an invalid progress value is provided", () => { + expect(() => { + // @ts-expect-error + progressBar.progress = "0"; + }).toThrowErrorMatchingInlineSnapshot( + '"jsPsych.progressBar.progress must be a number between 0 and 1"' + ); + expect(() => { + progressBar.progress = -0.1; + }).toThrowErrorMatchingInlineSnapshot( + '"jsPsych.progressBar.progress must be a number between 0 and 1"' + ); + expect(() => (progressBar.progress = 1.1)).toThrowErrorMatchingInlineSnapshot( + '"jsPsych.progressBar.progress must be a number between 0 and 1"' + ); + }); + + it("should work when message is a function", () => { + // Override default container element and progress bar + containerElement = document.createElement("div"); + progressBar = new ProgressBar(containerElement, (progress: number) => String(progress)); + let messageSpan: HTMLSpanElement = containerElement.querySelector("span"); + + expect(messageSpan.innerHTML).toEqual("0"); + + progressBar.progress = 0.5; + expect(messageSpan.innerHTML).toEqual("0.5"); + }); + }); +}); diff --git a/packages/jspsych/src/ProgressBar.ts b/packages/jspsych/src/ProgressBar.ts new file mode 100644 index 0000000000..f8196d4f71 --- /dev/null +++ b/packages/jspsych/src/ProgressBar.ts @@ -0,0 +1,60 @@ +/** + * Maintains a visual progress bar using HTML and CSS + */ +export class ProgressBar { + constructor( + private readonly containerElement: HTMLDivElement, + private readonly message: string | ((progress: number) => string) + ) { + this.setupElements(); + } + + private _progress = 0; + + private innerDiv: HTMLDivElement; + private messageSpan: HTMLSpanElement; + + /** Adds the progress bar HTML code into `this.containerElement` */ + private setupElements() { + this.messageSpan = document.createElement("span"); + + this.innerDiv = document.createElement("div"); + this.innerDiv.id = "jspsych-progressbar-inner"; + this.update(); + + const outerDiv = document.createElement("div"); + outerDiv.id = "jspsych-progressbar-outer"; + outerDiv.appendChild(this.innerDiv); + + this.containerElement.appendChild(this.messageSpan); + this.containerElement.appendChild(outerDiv); + } + + /** Updates the progress bar according to `this.progress` */ + private update() { + this.innerDiv.style.width = this._progress * 100 + "%"; + + if (typeof this.message === "function") { + this.messageSpan.innerHTML = this.message(this._progress); + } else { + this.messageSpan.innerHTML = this.message; + } + } + + /** + * The bar's current position as a number in the closed interval [0, 1]. Set this to update the + * progress bar accordingly. + */ + set progress(progress: number) { + if (typeof progress !== "number" || progress < 0 || progress > 1) { + throw new Error("jsPsych.progressBar.progress must be a number between 0 and 1"); + } + + this._progress = progress; + this.update(); + } + + get progress() { + return this._progress; + } +} diff --git a/packages/jspsych/src/TimelineNode.ts b/packages/jspsych/src/TimelineNode.ts deleted file mode 100644 index 1c2f62536a..0000000000 --- a/packages/jspsych/src/TimelineNode.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { JsPsych } from "./JsPsych"; -import { - repeat, - sampleWithReplacement, - sampleWithoutReplacement, - shuffle, - shuffleAlternateGroups, -} from "./modules/randomization"; -import { deepCopy } from "./modules/utils"; - -export class TimelineNode { - // a unique ID for this node, relative to the parent - relative_id; - - // store the parent for this node - parent_node; - - // parameters for the trial if the node contains a trial - trial_parameters; - - // parameters for nodes that contain timelines - timeline_parameters; - - // stores trial information on a node that contains a timeline - // used for adding new trials - node_trial_data; - - // track progress through the node - progress = { - current_location: -1, // where on the timeline (which timelinenode) - current_variable_set: 0, // which set of variables to use from timeline_variables - current_repetition: 0, // how many times through the variable set on this run of the node - current_iteration: 0, // how many times this node has been revisited - done: false, - }; - - end_message?: string; - - // constructor - constructor(private jsPsych: JsPsych, parameters, parent?, relativeID?) { - // store a link to the parent of this node - this.parent_node = parent; - - // create the ID for this node - this.relative_id = typeof parent === "undefined" ? 0 : relativeID; - - // check if there is a timeline parameter - // if there is, then this node has its own timeline - if (typeof parameters.timeline !== "undefined") { - // create timeline properties - this.timeline_parameters = { - timeline: [], - loop_function: parameters.loop_function, - conditional_function: parameters.conditional_function, - sample: parameters.sample, - randomize_order: - typeof parameters.randomize_order == "undefined" ? false : parameters.randomize_order, - repetitions: typeof parameters.repetitions == "undefined" ? 1 : parameters.repetitions, - timeline_variables: - typeof parameters.timeline_variables == "undefined" - ? [{}] - : parameters.timeline_variables, - on_timeline_finish: parameters.on_timeline_finish, - on_timeline_start: parameters.on_timeline_start, - }; - - this.setTimelineVariablesOrder(); - - // extract all of the node level data and parameters - // but remove all of the timeline-level specific information - // since this will be used to copy things down hierarchically - var node_data = Object.assign({}, parameters); - delete node_data.timeline; - delete node_data.conditional_function; - delete node_data.loop_function; - delete node_data.randomize_order; - delete node_data.repetitions; - delete node_data.timeline_variables; - delete node_data.sample; - delete node_data.on_timeline_start; - delete node_data.on_timeline_finish; - this.node_trial_data = node_data; // store for later... - - // create a TimelineNode for each element in the timeline - for (var i = 0; i < parameters.timeline.length; i++) { - // merge parameters - var merged_parameters = Object.assign({}, node_data, parameters.timeline[i]); - // merge any data from the parent node into child nodes - if (typeof node_data.data == "object" && typeof parameters.timeline[i].data == "object") { - var merged_data = Object.assign({}, node_data.data, parameters.timeline[i].data); - merged_parameters.data = merged_data; - } - this.timeline_parameters.timeline.push( - new TimelineNode(this.jsPsych, merged_parameters, this, i) - ); - } - } - // if there is no timeline parameter, then this node is a trial node - else { - // check to see if a valid trial type is defined - if (typeof parameters.type === "undefined") { - console.error( - 'Trial level node is missing the "type" parameter. The parameters for the node are: ' + - JSON.stringify(parameters) - ); - } - // create a deep copy of the parameters for the trial - this.trial_parameters = { ...parameters }; - } - } - - // recursively get the next trial to run. - // if this node is a leaf (trial), then return the trial. - // otherwise, recursively find the next trial in the child timeline. - trial() { - if (typeof this.timeline_parameters == "undefined") { - // returns a clone of the trial_parameters to - // protect functions. - return deepCopy(this.trial_parameters); - } else { - if (this.progress.current_location >= this.timeline_parameters.timeline.length) { - return null; - } else { - return this.timeline_parameters.timeline[this.progress.current_location].trial(); - } - } - } - - markCurrentTrialComplete() { - if (typeof this.timeline_parameters === "undefined") { - this.progress.done = true; - } else { - this.timeline_parameters.timeline[this.progress.current_location].markCurrentTrialComplete(); - } - } - - nextRepetiton() { - this.setTimelineVariablesOrder(); - this.progress.current_location = -1; - this.progress.current_variable_set = 0; - this.progress.current_repetition++; - for (var i = 0; i < this.timeline_parameters.timeline.length; i++) { - this.timeline_parameters.timeline[i].reset(); - } - } - - // set the order for going through the timeline variables array - setTimelineVariablesOrder() { - const timeline_parameters = this.timeline_parameters; - - // check to make sure this node has variables - if ( - typeof timeline_parameters === "undefined" || - typeof timeline_parameters.timeline_variables === "undefined" - ) { - return; - } - - var order = []; - for (var i = 0; i < timeline_parameters.timeline_variables.length; i++) { - order.push(i); - } - - if (typeof timeline_parameters.sample !== "undefined") { - if (timeline_parameters.sample.type == "custom") { - order = timeline_parameters.sample.fn(order); - } else if (timeline_parameters.sample.type == "with-replacement") { - order = sampleWithReplacement( - order, - timeline_parameters.sample.size, - timeline_parameters.sample.weights - ); - } else if (timeline_parameters.sample.type == "without-replacement") { - order = sampleWithoutReplacement(order, timeline_parameters.sample.size); - } else if (timeline_parameters.sample.type == "fixed-repetitions") { - order = repeat(order, timeline_parameters.sample.size, false); - } else if (timeline_parameters.sample.type == "alternate-groups") { - order = shuffleAlternateGroups( - timeline_parameters.sample.groups, - timeline_parameters.sample.randomize_group_order - ); - } else { - console.error( - 'Invalid type in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"' - ); - } - } - - if (timeline_parameters.randomize_order) { - order = shuffle(order); - } - - this.progress.order = order; - } - - // next variable set - nextSet() { - this.progress.current_location = -1; - this.progress.current_variable_set++; - for (var i = 0; i < this.timeline_parameters.timeline.length; i++) { - this.timeline_parameters.timeline[i].reset(); - } - } - - // update the current trial node to be completed - // returns true if the node is complete after advance (all subnodes are also complete) - // returns false otherwise - advance() { - const progress = this.progress; - const timeline_parameters = this.timeline_parameters; - const internal = this.jsPsych.internal; - - // first check to see if done - if (progress.done) { - return true; - } - - // if node has not started yet (progress.current_location == -1), - // then try to start the node. - if (progress.current_location == -1) { - // check for on_timeline_start and conditonal function on nodes with timelines - if (typeof timeline_parameters !== "undefined") { - // only run the conditional function if this is the first repetition of the timeline when - // repetitions > 1, and only when on the first variable set - if ( - typeof timeline_parameters.conditional_function !== "undefined" && - progress.current_repetition == 0 && - progress.current_variable_set == 0 - ) { - internal.call_immediate = true; - var conditional_result = timeline_parameters.conditional_function(); - internal.call_immediate = false; - // if the conditional_function() returns false, then the timeline - // doesn't run and is marked as complete. - if (conditional_result == false) { - progress.done = true; - return true; - } - } - - // if we reach this point then the node has its own timeline and will start - // so we need to check if there is an on_timeline_start function if we are on the first variable set - if ( - typeof timeline_parameters.on_timeline_start !== "undefined" && - progress.current_variable_set == 0 - ) { - timeline_parameters.on_timeline_start(); - } - } - // if we reach this point, then either the node doesn't have a timeline of the - // conditional function returned true and it can start - progress.current_location = 0; - // call advance again on this node now that it is pointing to a new location - return this.advance(); - } - - // if this node has a timeline, propogate down to the current trial. - if (typeof timeline_parameters !== "undefined") { - var have_node_to_run = false; - // keep incrementing the location in the timeline until one of the nodes reached is incomplete - while ( - progress.current_location < timeline_parameters.timeline.length && - have_node_to_run == false - ) { - // check to see if the node currently pointed at is done - var target_complete = timeline_parameters.timeline[progress.current_location].advance(); - if (!target_complete) { - have_node_to_run = true; - return false; - } else { - progress.current_location++; - } - } - - // if we've reached the end of the timeline (which, if the code is here, we have) - - // there are a few steps to see what to do next... - - // first, check the timeline_variables to see if we need to loop through again - // with a new set of variables - if (progress.current_variable_set < progress.order.length - 1) { - // reset the progress of the node to be with the new set - this.nextSet(); - // then try to advance this node again. - return this.advance(); - } - - // if we're all done with the timeline_variables, then check to see if there are more repetitions - else if (progress.current_repetition < timeline_parameters.repetitions - 1) { - this.nextRepetiton(); - // check to see if there is an on_timeline_finish function - if (typeof timeline_parameters.on_timeline_finish !== "undefined") { - timeline_parameters.on_timeline_finish(); - } - return this.advance(); - } - - // if we're all done with the repetitions... - else { - // check to see if there is an on_timeline_finish function - if (typeof timeline_parameters.on_timeline_finish !== "undefined") { - timeline_parameters.on_timeline_finish(); - } - - // if we're all done with the repetitions, check if there is a loop function. - if (typeof timeline_parameters.loop_function !== "undefined") { - internal.call_immediate = true; - if (timeline_parameters.loop_function(this.generatedData())) { - this.reset(); - internal.call_immediate = false; - return this.parent_node.advance(); - } else { - progress.done = true; - internal.call_immediate = false; - return true; - } - } - } - - // no more loops on this timeline, we're done! - progress.done = true; - return true; - } - } - - // check the status of the done flag - isComplete() { - return this.progress.done; - } - - // getter method for timeline variables - getTimelineVariableValue(variable_name: string) { - if (typeof this.timeline_parameters == "undefined") { - return undefined; - } - var v = - this.timeline_parameters.timeline_variables[ - this.progress.order[this.progress.current_variable_set] - ][variable_name]; - return v; - } - - // recursive upward search for timeline variables - findTimelineVariable(variable_name) { - var v = this.getTimelineVariableValue(variable_name); - if (typeof v == "undefined") { - if (typeof this.parent_node !== "undefined") { - return this.parent_node.findTimelineVariable(variable_name); - } else { - return undefined; - } - } else { - return v; - } - } - - // recursive downward search for active trial to extract timeline variable - timelineVariable(variable_name: string) { - if (typeof this.timeline_parameters == "undefined") { - const val = this.findTimelineVariable(variable_name); - if (typeof val === "undefined") { - console.warn("Timeline variable " + variable_name + " not found."); - } - return val; - } else { - // if progress.current_location is -1, then the timeline variable is being evaluated - // in a function that runs prior to the trial starting, so we should treat that trial - // as being the active trial for purposes of finding the value of the timeline variable - var loc = Math.max(0, this.progress.current_location); - // if loc is greater than the number of elements on this timeline, then the timeline - // variable is being evaluated in a function that runs after the trial on the timeline - // are complete but before advancing to the next (like a loop_function). - // treat the last active trial as the active trial for this purpose. - if (loc == this.timeline_parameters.timeline.length) { - loc = loc - 1; - } - // now find the variable - const val = this.timeline_parameters.timeline[loc].timelineVariable(variable_name); - if (typeof val === "undefined") { - console.warn("Timeline variable " + variable_name + " not found."); - } - return val; - } - } - - // recursively get all the timeline variables for this trial - allTimelineVariables() { - var all_tvs = this.allTimelineVariablesNames(); - var all_tvs_vals = {}; - for (var i = 0; i < all_tvs.length; i++) { - all_tvs_vals[all_tvs[i]] = this.timelineVariable(all_tvs[i]); - } - return all_tvs_vals; - } - - // helper to get all the names at this stage. - allTimelineVariablesNames(so_far = []) { - if (typeof this.timeline_parameters !== "undefined") { - so_far = so_far.concat( - Object.keys( - this.timeline_parameters.timeline_variables[ - this.progress.order[this.progress.current_variable_set] - ] - ) - ); - // if progress.current_location is -1, then the timeline variable is being evaluated - // in a function that runs prior to the trial starting, so we should treat that trial - // as being the active trial for purposes of finding the value of the timeline variable - var loc = Math.max(0, this.progress.current_location); - // if loc is greater than the number of elements on this timeline, then the timeline - // variable is being evaluated in a function that runs after the trial on the timeline - // are complete but before advancing to the next (like a loop_function). - // treat the last active trial as the active trial for this purpose. - if (loc == this.timeline_parameters.timeline.length) { - loc = loc - 1; - } - // now find the variable - return this.timeline_parameters.timeline[loc].allTimelineVariablesNames(so_far); - } - if (typeof this.timeline_parameters == "undefined") { - return so_far; - } - } - - // recursively get the number of **trials** contained in the timeline - // assuming that while loops execute exactly once and if conditionals - // always run - length() { - var length = 0; - if (typeof this.timeline_parameters !== "undefined") { - for (var i = 0; i < this.timeline_parameters.timeline.length; i++) { - length += this.timeline_parameters.timeline[i].length(); - } - } else { - return 1; - } - return length; - } - - // return the percentage of trials completed, grouped at the first child level - // counts a set of trials as complete when the child node is done - percentComplete() { - var total_trials = this.length(); - var completed_trials = 0; - for (var i = 0; i < this.timeline_parameters.timeline.length; i++) { - if (this.timeline_parameters.timeline[i].isComplete()) { - completed_trials += this.timeline_parameters.timeline[i].length(); - } - } - return (completed_trials / total_trials) * 100; - } - - // resets the node and all subnodes to original state - // but increments the current_iteration counter - reset() { - this.progress.current_location = -1; - this.progress.current_repetition = 0; - this.progress.current_variable_set = 0; - this.progress.current_iteration++; - this.progress.done = false; - this.setTimelineVariablesOrder(); - if (typeof this.timeline_parameters != "undefined") { - for (var i = 0; i < this.timeline_parameters.timeline.length; i++) { - this.timeline_parameters.timeline[i].reset(); - } - } - } - - // mark this node as finished - end() { - this.progress.done = true; - } - - // recursively end whatever sub-node is running the current trial - endActiveNode() { - if (typeof this.timeline_parameters == "undefined") { - this.end(); - this.parent_node.end(); - } else { - this.timeline_parameters.timeline[this.progress.current_location].endActiveNode(); - } - } - - // get a unique ID associated with this node - // the ID reflects the current iteration through this node. - ID() { - var id = ""; - if (typeof this.parent_node == "undefined") { - return "0." + this.progress.current_iteration; - } else { - id += this.parent_node.ID() + "-"; - id += this.relative_id + "." + this.progress.current_iteration; - return id; - } - } - - // get the ID of the active trial - activeID() { - if (typeof this.timeline_parameters == "undefined") { - return this.ID(); - } else { - return this.timeline_parameters.timeline[this.progress.current_location].activeID(); - } - } - - // get all the data generated within this node - generatedData() { - return this.jsPsych.data.getDataByTimelineNode(this.ID()); - } - - // get all the trials of a particular type - trialsOfType(type) { - if (typeof this.timeline_parameters == "undefined") { - if (this.trial_parameters.type == type) { - return this.trial_parameters; - } else { - return []; - } - } else { - var trials = []; - for (var i = 0; i < this.timeline_parameters.timeline.length; i++) { - var t = this.timeline_parameters.timeline[i].trialsOfType(type); - trials = trials.concat(t); - } - return trials; - } - } - - // add new trials to end of this timeline - insert(parameters) { - if (typeof this.timeline_parameters === "undefined") { - console.error("Cannot add new trials to a trial-level node."); - } else { - this.timeline_parameters.timeline.push( - new TimelineNode( - this.jsPsych, - { ...this.node_trial_data, ...parameters }, - this, - this.timeline_parameters.timeline.length - ) - ); - } - } -} diff --git a/packages/jspsych/src/index.scss b/packages/jspsych/src/index.scss index 5722a2b21e..6e55f8d23d 100644 --- a/packages/jspsych/src/index.scss +++ b/packages/jspsych/src/index.scss @@ -30,7 +30,6 @@ } .jspsych-content { - max-width: 95%; /* this is mainly an IE 10-11 fix */ text-align: center; margin: auto; /* this is for overflowing content */ } @@ -58,11 +57,25 @@ font-size: 14px; } -/* borrowing Bootstrap style for btn elements, but combining styles a bit */ +/* Buttons and Button Groups */ + +.jspsych-btn-group-flex { + display: flex; + justify-content: center; +} + +.jspsych-btn-group-grid { + display: grid; + grid-auto-columns: max-content; + max-width: fit-content; + margin-right: auto; + margin-left: auto; +} + .jspsych-btn { display: inline-block; - padding: 6px 12px; - margin: 0px; + padding: 8px 12px; + margin: 0.75em; font-size: 14px; font-weight: 400; font-family: "Open Sans", "Arial", sans-serif; @@ -108,10 +121,11 @@ width: 100%; background: transparent; } + .jspsych-slider:focus { outline: none; } -/* track */ + .jspsych-slider::-webkit-slider-runnable-track { appearance: none; -webkit-appearance: none; @@ -123,6 +137,7 @@ border-radius: 2px; border: 1px solid #aaa; } + .jspsych-slider::-moz-range-track { appearance: none; width: 100%; @@ -133,6 +148,7 @@ border-radius: 2px; border: 1px solid #aaa; } + .jspsych-slider::-ms-track { appearance: none; width: 99%; @@ -143,7 +159,7 @@ border-radius: 2px; border: 1px solid #aaa; } -/* thumb */ + .jspsych-slider::-webkit-slider-thumb { border: 1px solid #666; height: 24px; @@ -154,6 +170,7 @@ -webkit-appearance: none; margin-top: -9px; } + .jspsych-slider::-moz-range-thumb { border: 1px solid #666; height: 24px; @@ -162,6 +179,7 @@ background: #ffffff; cursor: pointer; } + .jspsych-slider::-ms-thumb { border: 1px solid #666; height: 20px; @@ -172,7 +190,7 @@ margin-top: -2px; } -/* jsPsych progress bar */ +/* progress bar */ #jspsych-progressbar-container { color: #555; @@ -184,10 +202,12 @@ width: 100%; line-height: 1em; } + #jspsych-progressbar-container span { font-size: 14px; padding-right: 14px; } + #jspsych-progressbar-outer { background-color: #eee; width: 50%; @@ -197,13 +217,14 @@ vertical-align: middle; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); } + #jspsych-progressbar-inner { background-color: #aaa; width: 0%; height: 100%; } -/* Control appearance of jsPsych.data.displayData() */ +/* Appearance of jsPsych.data.displayData() */ #jspsych-data-display { text-align: left; } diff --git a/packages/jspsych/src/index.ts b/packages/jspsych/src/index.ts index dffb615f95..15e13fe7d5 100755 --- a/packages/jspsych/src/index.ts +++ b/packages/jspsych/src/index.ts @@ -62,12 +62,7 @@ export function initJsPsych(options?) { } export { JsPsych } from "./JsPsych"; -export { - JsPsychPlugin, - PluginInfo, - TrialType, - ParameterType, - universalPluginParameters, - UniversalPluginParameters, -} from "./modules/plugins"; -export { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions"; +export type { JsPsychPlugin, PluginInfo, TrialType } from "./modules/plugins"; +export { ParameterType } from "./modules/plugins"; +export type { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions"; +export { DataCollection } from "./modules/data/DataCollection"; diff --git a/packages/jspsych/src/modules/data/DataCollection.ts b/packages/jspsych/src/modules/data/DataCollection.ts index a31b2d2798..cac33dd254 100644 --- a/packages/jspsych/src/modules/data/DataCollection.ts +++ b/packages/jspsych/src/modules/data/DataCollection.ts @@ -89,7 +89,7 @@ export class DataCollection { } addToLast(properties) { - if (this.trials.length != 0) { + if (this.trials.length > 0) { Object.assign(this.trials[this.trials.length - 1], properties); } return this; diff --git a/packages/jspsych/src/modules/data/DataColumn.ts b/packages/jspsych/src/modules/data/DataColumn.ts index 6e1153e3c7..ff29922fc2 100644 --- a/packages/jspsych/src/modules/data/DataColumn.ts +++ b/packages/jspsych/src/modules/data/DataColumn.ts @@ -10,7 +10,18 @@ export class DataColumn { } mean() { - return this.sum() / this.count(); + let sum = 0; + let count = 0; + for (const value of this.values) { + if (typeof value !== "undefined" && value !== null) { + sum += value; + count++; + } + } + if (count === 0) { + return undefined; + } + return sum / count; } median() { diff --git a/packages/jspsych/src/modules/data/index.ts b/packages/jspsych/src/modules/data/index.ts index 13d1144b3d..34247a880f 100644 --- a/packages/jspsych/src/modules/data/index.ts +++ b/packages/jspsych/src/modules/data/index.ts @@ -1,106 +1,104 @@ -import { JsPsych } from "../../JsPsych"; +import { TrialResult } from "../../timeline"; +import { Trial } from "../../timeline/Trial"; import { DataCollection } from "./DataCollection"; import { getQueryString } from "./utils"; +export type InteractionEvent = "blur" | "focus" | "fullscreenenter" | "fullscreenexit"; + +export interface InteractionRecord { + event: InteractionEvent; + trial: number; + time: number; +} + +/** + * Functions and options needed by the `JsPsychData` module + */ +export interface JsPsychDataDependencies { + /** + * Returns progress information for interaction records. + */ + getProgress: () => { trial: number; time: number }; + + onInteractionRecordAdded: (record: InteractionRecord) => void; + + getDisplayElement: () => HTMLElement; +} + export class JsPsychData { - // data storage object - private allData: DataCollection; + private results: DataCollection; + private resultToTrialMap: WeakMap; - // browser interaction event data - private interactionData: DataCollection; + /** Browser interaction event data */ + private interactionRecords: DataCollection; - // data properties for all trials + /** Data properties for all trials */ private dataProperties = {}; // cache the query_string private query_string; - constructor(private jsPsych: JsPsych) { + constructor(private dependencies: JsPsychDataDependencies) { this.reset(); } reset() { - this.allData = new DataCollection(); - this.interactionData = new DataCollection(); + this.results = new DataCollection(); + this.resultToTrialMap = new WeakMap(); + this.interactionRecords = new DataCollection(); } get() { - return this.allData; + return this.results; } getInteractionData() { - return this.interactionData; + return this.interactionRecords; } - write(data_object) { - const progress = this.jsPsych.getProgress(); - const trial = this.jsPsych.getCurrentTrial(); - - //var trial_opt_data = typeof trial.data == 'function' ? trial.data() : trial.data; - - const default_data = { - trial_type: trial.type.info.name, - trial_index: progress.current_trial_global, - time_elapsed: this.jsPsych.getTotalTime(), - internal_node_id: this.jsPsych.getCurrentTimelineNodeID(), - }; - - this.allData.push({ - ...data_object, - ...trial.data, - ...default_data, - ...this.dataProperties, - }); + write(trial: Trial) { + const result = trial.getResult(); + Object.assign(result, this.dataProperties); + this.results.push(result); + this.resultToTrialMap.set(result, trial); } addProperties(properties) { // first, add the properties to all data that's already stored - this.allData.addToAll(properties); + this.results.addToAll(properties); // now add to list so that it gets appended to all future data this.dataProperties = Object.assign({}, this.dataProperties, properties); } addDataToLastTrial(data) { - this.allData.addToLast(data); - } - - getDataByTimelineNode(node_id) { - return this.allData.filterCustom( - (x) => x.internal_node_id.slice(0, node_id.length) === node_id - ); + this.results.addToLast(data); } getLastTrialData() { - return this.allData.top(); + return this.results.top(); } getLastTimelineData() { - const lasttrial = this.getLastTrialData(); - const node_id = lasttrial.select("internal_node_id").values[0]; - if (typeof node_id === "undefined") { - return new DataCollection(); - } else { - const parent_node_id = node_id.substr(0, node_id.lastIndexOf("-")); - const lastnodedata = this.getDataByTimelineNode(parent_node_id); - return lastnodedata; - } + const lastResult = this.getLastTrialData().values()[0]; + + return new DataCollection( + lastResult ? this.resultToTrialMap.get(lastResult).parent.getResults() : [] + ); } displayData(format = "json") { format = format.toLowerCase(); - if (format != "json" && format != "csv") { + if (format !== "json" && format !== "csv") { console.log("Invalid format declared for displayData function. Using json as default."); format = "json"; } - const data_string = format === "json" ? this.allData.json(true) : this.allData.csv(); + const dataContainer = document.createElement("pre"); + dataContainer.id = "jspsych-data-display"; + dataContainer.textContent = format === "json" ? this.results.json(true) : this.results.csv(); - const display_element = this.jsPsych.getDisplayElement(); - - display_element.innerHTML = '
';
-
-    document.getElementById("jspsych-data-display").textContent = data_string;
+    this.dependencies.getDisplayElement().replaceChildren(dataContainer);
   }
 
   urlVariables() {
@@ -114,61 +112,52 @@ export class JsPsychData {
     return this.urlVariables()[whichvar];
   }
 
-  createInteractionListeners() {
-    // blur event capture
-    window.addEventListener("blur", () => {
-      const data = {
-        event: "blur",
-        trial: this.jsPsych.getProgress().current_trial_global,
-        time: this.jsPsych.getTotalTime(),
-      };
-      this.interactionData.push(data);
-      this.jsPsych.getInitSettings().on_interaction_data_update(data);
-    });
-
-    // focus event capture
-    window.addEventListener("focus", () => {
-      const data = {
-        event: "focus",
-        trial: this.jsPsych.getProgress().current_trial_global,
-        time: this.jsPsych.getTotalTime(),
-      };
-      this.interactionData.push(data);
-      this.jsPsych.getInitSettings().on_interaction_data_update(data);
-    });
-
-    // fullscreen change capture
-    const fullscreenchange = () => {
-      const data = {
-        event:
-          // @ts-expect-error
-          document.isFullScreen ||
+  private addInteractionRecord(event: InteractionEvent) {
+    const record: InteractionRecord = { event, ...this.dependencies.getProgress() };
+    this.interactionRecords.push(record);
+    this.dependencies.onInteractionRecordAdded(record);
+  }
+
+  private interactionListeners = {
+    blur: () => {
+      this.addInteractionRecord("blur");
+    },
+    focus: () => {
+      this.addInteractionRecord("focus");
+    },
+    fullscreenchange: () => {
+      this.addInteractionRecord(
+        // @ts-expect-error
+        document.isFullScreen ||
           // @ts-expect-error
           document.webkitIsFullScreen ||
           // @ts-expect-error
           document.mozIsFullScreen ||
           document.fullscreenElement
-            ? "fullscreenenter"
-            : "fullscreenexit",
-        trial: this.jsPsych.getProgress().current_trial_global,
-        time: this.jsPsych.getTotalTime(),
-      };
-      this.interactionData.push(data);
-      this.jsPsych.getInitSettings().on_interaction_data_update(data);
-    };
-
-    document.addEventListener("fullscreenchange", fullscreenchange);
-    document.addEventListener("mozfullscreenchange", fullscreenchange);
-    document.addEventListener("webkitfullscreenchange", fullscreenchange);
-  }
+          ? "fullscreenenter"
+          : "fullscreenexit"
+      );
+    },
+  };
+
+  createInteractionListeners() {
+    window.addEventListener("blur", this.interactionListeners.blur);
+    window.addEventListener("focus", this.interactionListeners.focus);
 
-  // public methods for testing purposes. not recommended for use.
-  _customInsert(data) {
-    this.allData = new DataCollection(data);
+    document.addEventListener("fullscreenchange", this.interactionListeners.fullscreenchange);
+    document.addEventListener("mozfullscreenchange", this.interactionListeners.fullscreenchange);
+    document.addEventListener("webkitfullscreenchange", this.interactionListeners.fullscreenchange);
   }
 
-  _fullreset() {
-    this.reset();
-    this.dataProperties = {};
+  removeInteractionListeners() {
+    window.removeEventListener("blur", this.interactionListeners.blur);
+    window.removeEventListener("focus", this.interactionListeners.focus);
+
+    document.removeEventListener("fullscreenchange", this.interactionListeners.fullscreenchange);
+    document.removeEventListener("mozfullscreenchange", this.interactionListeners.fullscreenchange);
+    document.removeEventListener(
+      "webkitfullscreenchange",
+      this.interactionListeners.fullscreenchange
+    );
   }
 }
diff --git a/packages/jspsych/src/modules/extensions.ts b/packages/jspsych/src/modules/extensions.ts
index e0e283cfaa..483aad324e 100644
--- a/packages/jspsych/src/modules/extensions.ts
+++ b/packages/jspsych/src/modules/extensions.ts
@@ -1,5 +1,9 @@
+import { ParameterInfos } from "./plugins";
+
 export interface JsPsychExtensionInfo {
   name: string;
+  version?: string;
+  data?: ParameterInfos;
 }
 
 export interface JsPsychExtension {
diff --git a/packages/jspsych/src/modules/plugin-api/AudioPlayer.ts b/packages/jspsych/src/modules/plugin-api/AudioPlayer.ts
new file mode 100644
index 0000000000..f36d4a4c98
--- /dev/null
+++ b/packages/jspsych/src/modules/plugin-api/AudioPlayer.ts
@@ -0,0 +1,101 @@
+export interface AudioPlayerOptions {
+  useWebAudio: boolean;
+  audioContext?: AudioContext;
+}
+
+export interface AudioPlayerInterface {
+  load(): Promise;
+  play(): void;
+  stop(): void;
+  addEventListener(eventName: string, callback: EventListenerOrEventListenerObject): void;
+  removeEventListener(eventName: string, callback: EventListenerOrEventListenerObject): void;
+}
+
+export class AudioPlayer implements AudioPlayerInterface {
+  private audio: HTMLAudioElement | AudioBufferSourceNode;
+  private webAudioBuffer: AudioBuffer;
+  private audioContext: AudioContext | null;
+  private useWebAudio: boolean;
+  private src: string;
+
+  constructor(src: string, options: AudioPlayerOptions = { useWebAudio: false }) {
+    this.src = src;
+    this.useWebAudio = options.useWebAudio;
+    this.audioContext = options.audioContext || null;
+  }
+
+  async load() {
+    if (this.useWebAudio) {
+      this.webAudioBuffer = await this.preloadWebAudio(this.src);
+    } else {
+      this.audio = await this.preloadHTMLAudio(this.src);
+    }
+  }
+
+  play() {
+    if (this.audio instanceof HTMLAudioElement) {
+      this.audio.play();
+    } else {
+      // If audio is not HTMLAudioElement, it must be a WebAudio API object, so create a source node.
+      if (!this.audio) this.audio = this.getAudioSourceNode(this.webAudioBuffer);
+      this.audio.start();
+    }
+  }
+
+  stop() {
+    if (this.audio instanceof HTMLAudioElement) {
+      this.audio.pause();
+      this.audio.currentTime = 0;
+    } else {
+      this.audio!.stop();
+      // Regenerate source node for audio since the previous one is stopped and unusable.
+      this.audio = this.getAudioSourceNode(this.webAudioBuffer);
+    }
+  }
+
+  addEventListener(eventName: string, callback: EventListenerOrEventListenerObject) {
+    // If WebAudio buffer exists but source node doesn't, create it.
+    if (!this.audio && this.webAudioBuffer)
+      this.audio = this.getAudioSourceNode(this.webAudioBuffer);
+    this.audio.addEventListener(eventName, callback);
+  }
+
+  removeEventListener(eventName: string, callback: EventListenerOrEventListenerObject) {
+    // If WebAudio buffer exists but source node doesn't, create it.
+    if (!this.audio && this.webAudioBuffer)
+      this.audio = this.getAudioSourceNode(this.webAudioBuffer);
+    this.audio.removeEventListener(eventName, callback);
+  }
+
+  private getAudioSourceNode(audioBuffer: AudioBuffer): AudioBufferSourceNode {
+    const source = this.audioContext!.createBufferSource();
+    source.buffer = audioBuffer;
+    source.connect(this.audioContext!.destination);
+    return source;
+  }
+
+  private async preloadWebAudio(src: string): Promise {
+    const buffer = await fetch(src);
+    const arrayBuffer = await buffer.arrayBuffer();
+    const audioBuffer = await this.audioContext!.decodeAudioData(arrayBuffer);
+    const source = this.audioContext!.createBufferSource();
+    source.buffer = audioBuffer;
+    source.connect(this.audioContext!.destination);
+    return audioBuffer;
+  }
+
+  private async preloadHTMLAudio(src: string): Promise {
+    return new Promise((resolve, reject) => {
+      const audio = new Audio(src);
+      audio.addEventListener("canplaythrough", () => {
+        resolve(audio);
+      });
+      audio.addEventListener("error", (err) => {
+        reject(err);
+      });
+      audio.addEventListener("abort", (err) => {
+        reject(err);
+      });
+    });
+  }
+}
diff --git a/packages/jspsych/src/modules/plugin-api/HardwareAPI.ts b/packages/jspsych/src/modules/plugin-api/HardwareAPI.ts
deleted file mode 100644
index fe148cd33a..0000000000
--- a/packages/jspsych/src/modules/plugin-api/HardwareAPI.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-export class HardwareAPI {
-  /**
-   * Indicates whether this instance of jspsych has opened a hardware connection through our browser
-   * extension
-   **/
-  hardwareConnected = false;
-
-  constructor() {
-    //it might be useful to open up a line of communication from the extension back to this page
-    //script, again, this will have to pass through DOM events. For now speed is of no concern so I
-    //will use jQuery
-    document.addEventListener("jspsych-activate", (evt) => {
-      this.hardwareConnected = true;
-    });
-  }
-
-  /**
-   * Allows communication with user hardware through our custom Google Chrome extension + native C++ program
-   * @param		mess	The message to be passed to our extension, see its documentation for the expected members of this object.
-   * @author	Daniel Rivas
-   *
-   */
-  hardware(mess) {
-    //since Chrome extension content-scripts do not share the javascript environment with the page
-    //script that loaded jspsych, we will need to use hacky methods like communicating through DOM
-    //events.
-    const jspsychEvt = new CustomEvent("jspsych", { detail: mess });
-    document.dispatchEvent(jspsychEvt);
-    //And voila! it will be the job of the content script injected by the extension to listen for
-    //the event and do the appropriate actions.
-  }
-}
diff --git a/packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts b/packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts
index b6aea6936e..82e1ed7f96 100644
--- a/packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts
+++ b/packages/jspsych/src/modules/plugin-api/KeyboardListenerAPI.ts
@@ -125,7 +125,7 @@ export class KeyboardListenerAPI {
           this.cancelKeyboardResponse(listener);
         }
 
-        callback_function({ key, rt });
+        callback_function({ key: e.key, rt });
       }
     };
 
diff --git a/packages/jspsych/src/modules/plugin-api/MediaAPI.ts b/packages/jspsych/src/modules/plugin-api/MediaAPI.ts
index 034b2edb5c..6dabb44760 100644
--- a/packages/jspsych/src/modules/plugin-api/MediaAPI.ts
+++ b/packages/jspsych/src/modules/plugin-api/MediaAPI.ts
@@ -1,5 +1,6 @@
 import { ParameterType } from "../../modules/plugins";
 import { unique } from "../utils";
+import { AudioPlayer } from "./AudioPlayer";
 
 const preloadParameterTypes = [
   ParameterType.AUDIO,
@@ -9,7 +10,15 @@ const preloadParameterTypes = [
 type PreloadType = typeof preloadParameterTypes[number];
 
 export class MediaAPI {
-  constructor(private useWebaudio: boolean, private webaudioContext?: AudioContext) {}
+  constructor(public useWebaudio: boolean) {
+    if (
+      this.useWebaudio &&
+      typeof window !== "undefined" &&
+      typeof window.AudioContext !== "undefined"
+    ) {
+      this.context = new AudioContext();
+    }
+  }
 
   // video //
   private video_buffers = {};
@@ -21,45 +30,27 @@ export class MediaAPI {
   }
 
   // audio //
-  private context = null;
+  private context: AudioContext = null;
   private audio_buffers = [];
 
-  initAudio() {
-    this.context = this.useWebaudio ? this.webaudioContext : null;
-  }
-
-  audioContext() {
-    if (this.context !== null) {
-      if (this.context.state !== "running") {
-        this.context.resume();
-      }
+  audioContext(): AudioContext {
+    if (this.context && this.context.state !== "running") {
+      this.context.resume();
     }
     return this.context;
   }
 
-  getAudioBuffer(audioID) {
-    return new Promise((resolve, reject) => {
-      // check whether audio file already preloaded
-      if (
-        typeof this.audio_buffers[audioID] == "undefined" ||
-        this.audio_buffers[audioID] == "tmp"
-      ) {
-        // if audio is not already loaded, try to load it
-        this.preloadAudio(
-          [audioID],
-          () => {
-            resolve(this.audio_buffers[audioID]);
-          },
-          () => {},
-          (e) => {
-            reject(e.error);
-          }
-        );
-      } else {
-        // audio is already loaded
-        resolve(this.audio_buffers[audioID]);
-      }
-    });
+  async getAudioPlayer(audioID: string): Promise {
+    if (this.audio_buffers[audioID] instanceof AudioPlayer) {
+      return this.audio_buffers[audioID];
+    } else {
+      this.audio_buffers[audioID] = new AudioPlayer(audioID, {
+        useWebAudio: this.useWebaudio,
+        audioContext: this.context,
+      });
+      await this.audio_buffers[audioID].load();
+      return this.audio_buffers[audioID];
+    }
   }
 
   // preloading stimuli //
@@ -70,8 +61,8 @@ export class MediaAPI {
   preloadAudio(
     files,
     callback_complete = () => {},
-    callback_load = (filepath) => {},
-    callback_error = (error_msg) => {}
+    callback_load = (filepath: string) => {},
+    callback_error = (error) => {}
   ) {
     files = unique(files.flat());
 
@@ -82,80 +73,31 @@ export class MediaAPI {
       return;
     }
 
-    const load_audio_file_webaudio = (source, count = 1) => {
-      const request = new XMLHttpRequest();
-      request.open("GET", source, true);
-      request.responseType = "arraybuffer";
-      request.onload = () => {
-        this.context.decodeAudioData(
-          request.response,
-          (buffer) => {
-            this.audio_buffers[source] = buffer;
-            n_loaded++;
-            callback_load(source);
-            if (n_loaded == files.length) {
-              callback_complete();
-            }
-          },
-          (e) => {
-            callback_error({ source: source, error: e });
-          }
-        );
-      };
-      request.onerror = (e) => {
-        let err: ProgressEvent | string = e;
-        if (request.status == 404) {
-          err = "404";
-        }
-        callback_error({ source: source, error: err });
-      };
-      request.onloadend = (e) => {
-        if (request.status == 404) {
-          callback_error({ source: source, error: "404" });
-        }
-      };
-      request.send();
-      this.preload_requests.push(request);
-    };
-
-    const load_audio_file_html5audio = (source, count = 1) => {
-      const audio = new Audio();
-      const handleCanPlayThrough = () => {
-        this.audio_buffers[source] = audio;
-        n_loaded++;
-        callback_load(source);
-        if (n_loaded == files.length) {
-          callback_complete();
-        }
-        audio.removeEventListener("canplaythrough", handleCanPlayThrough);
-      };
-      audio.addEventListener("canplaythrough", handleCanPlayThrough);
-      audio.addEventListener("error", function handleError(e) {
-        callback_error({ source: audio.src, error: e });
-        audio.removeEventListener("error", handleError);
-      });
-      audio.addEventListener("abort", function handleAbort(e) {
-        callback_error({ source: audio.src, error: e });
-        audio.removeEventListener("abort", handleAbort);
-      });
-      audio.src = source;
-      this.preload_requests.push(audio);
-    };
-
     for (const file of files) {
-      if (typeof this.audio_buffers[file] !== "undefined") {
+      // check if file was already loaded
+      if (this.audio_buffers[file] instanceof AudioPlayer) {
         n_loaded++;
         callback_load(file);
         if (n_loaded == files.length) {
           callback_complete();
         }
       } else {
-        this.audio_buffers[file] = "tmp";
-        if (this.audioContext() !== null) {
-          load_audio_file_webaudio(file);
-        } else {
-          load_audio_file_html5audio(file);
-        }
+        this.audio_buffers[file] = new AudioPlayer(file, {
+          useWebAudio: this.useWebaudio,
+          audioContext: this.context,
+        });
+        this.audio_buffers[file]
+          .load()
+          .then(() => {
+            n_loaded++;
+            callback_load(file);
+            if (n_loaded == files.length) {
+              callback_complete();
+            }
+          })
+          .catch((e) => {
+            callback_error(e);
+          });
       }
     }
   }
@@ -221,7 +163,7 @@ export class MediaAPI {
       const request = new XMLHttpRequest();
       request.open("GET", video, true);
       request.responseType = "blob";
-      request.onload =  () => {
+      request.onload = () => {
         if (request.status === 200 || request.status === 0) {
           const videoBlob = request.response;
           video_buffers[video] = URL.createObjectURL(videoBlob); // IE10+
@@ -232,14 +174,14 @@ export class MediaAPI {
           }
         }
       };
-      request.onerror =  (e) => {
+      request.onerror = (e) => {
         let err: ProgressEvent | string = e;
         if (request.status == 404) {
           err = "404";
         }
         callback_error({ source: video, error: err });
       };
-      request.onloadend =  (e) => {
+      request.onloadend = (e) => {
         if (request.status == 404) {
           callback_error({ source: video, error: "404" });
         }
diff --git a/packages/jspsych/src/modules/plugin-api/__mocks__/AudioPlayer.ts b/packages/jspsych/src/modules/plugin-api/__mocks__/AudioPlayer.ts
new file mode 100644
index 0000000000..0f8484efea
--- /dev/null
+++ b/packages/jspsych/src/modules/plugin-api/__mocks__/AudioPlayer.ts
@@ -0,0 +1,38 @@
+import { AudioPlayerOptions } from "../AudioPlayer";
+
+const actual = jest.requireActual("../AudioPlayer");
+
+export const mockStop = jest.fn();
+
+export const AudioPlayer = jest
+  .fn()
+  .mockImplementation((src: string, options: AudioPlayerOptions = { useWebAudio: false }) => {
+    let eventHandlers = {};
+
+    const mockInstance = Object.create(actual.AudioPlayer.prototype);
+
+    return Object.assign(mockInstance, {
+      load: jest.fn(),
+      play: jest.fn(() => {
+        setTimeout(() => {
+          if (eventHandlers["ended"]) {
+            for (const handler of eventHandlers["ended"]) {
+              handler();
+            }
+          }
+        }, 1000);
+      }),
+      stop: mockStop,
+      addEventListener: jest.fn((event, handler) => {
+        if (!eventHandlers[event]) {
+          eventHandlers[event] = [];
+        }
+        eventHandlers[event].push(handler);
+      }),
+      removeEventListener: jest.fn((event, handler) => {
+        if (eventHandlers[event] === handler) {
+          eventHandlers[event] = eventHandlers[event].filter((h) => h !== handler);
+        }
+      }),
+    });
+  });
diff --git a/packages/jspsych/src/modules/plugin-api/index.ts b/packages/jspsych/src/modules/plugin-api/index.ts
index fc66a50003..d09c81fa8c 100644
--- a/packages/jspsych/src/modules/plugin-api/index.ts
+++ b/packages/jspsych/src/modules/plugin-api/index.ts
@@ -1,7 +1,6 @@
 import autoBind from "auto-bind";
 
 import { JsPsych } from "../../JsPsych";
-import { HardwareAPI } from "./HardwareAPI";
 import { KeyboardListenerAPI } from "./KeyboardListenerAPI";
 import { MediaAPI } from "./MediaAPI";
 import { SimulationAPI } from "./SimulationAPI";
@@ -9,23 +8,21 @@ import { TimeoutAPI } from "./TimeoutAPI";
 
 export function createJointPluginAPIObject(jsPsych: JsPsych) {
   const settings = jsPsych.getInitSettings();
-  const keyboardListenerAPI = autoBind(
-    new KeyboardListenerAPI(
-      jsPsych.getDisplayContainerElement,
-      settings.case_sensitive_responses,
-      settings.minimum_valid_rt
-    )
+  const keyboardListenerAPI = new KeyboardListenerAPI(
+    jsPsych.getDisplayContainerElement,
+    settings.case_sensitive_responses,
+    settings.minimum_valid_rt
   );
-  const timeoutAPI = autoBind(new TimeoutAPI());
-  const mediaAPI = autoBind(new MediaAPI(settings.use_webaudio, jsPsych.webaudio_context));
-  const hardwareAPI = autoBind(new HardwareAPI());
-  const simulationAPI = autoBind(
-    new SimulationAPI(jsPsych.getDisplayContainerElement, timeoutAPI.setTimeout)
+  const timeoutAPI = new TimeoutAPI();
+  const mediaAPI = new MediaAPI(settings.use_webaudio);
+  const simulationAPI = new SimulationAPI(
+    jsPsych.getDisplayContainerElement,
+    timeoutAPI.setTimeout.bind(timeoutAPI)
   );
   return Object.assign(
     {},
-    ...[keyboardListenerAPI, timeoutAPI, mediaAPI, hardwareAPI, simulationAPI]
-  ) as KeyboardListenerAPI & TimeoutAPI & MediaAPI & HardwareAPI & SimulationAPI;
+    ...[keyboardListenerAPI, timeoutAPI, mediaAPI, simulationAPI].map((object) => autoBind(object))
+  ) as KeyboardListenerAPI & TimeoutAPI & MediaAPI & SimulationAPI;
 }
 
 export type PluginAPI = ReturnType;
diff --git a/packages/jspsych/src/modules/plugins.ts b/packages/jspsych/src/modules/plugins.ts
index 08c35948a5..4f6922d908 100644
--- a/packages/jspsych/src/modules/plugins.ts
+++ b/packages/jspsych/src/modules/plugins.ts
@@ -1,16 +1,6 @@
-/**
-Flatten the type output to improve type hints shown in editors.
-Borrowed from type-fest
-*/
-type Simplify = { [KeyType in keyof T]: T[KeyType] };
+import { SetRequired } from "type-fest";
 
-/**
-Create a type that makes the given keys required. The remaining keys are kept as is.
-Borrowed from type-fest
-*/
-type SetRequired = Simplify<
-  Omit & Required>
->;
+import { SimulationMode, SimulationOptions, TrialDescription, TrialResult } from "../timeline";
 
 /**
  * Parameter types for plugins
@@ -51,17 +41,19 @@ type ParameterTypeMap = {
   [ParameterType.TIMELINE]: any;
 };
 
-export interface ParameterInfo {
-  type: ParameterType;
+type PreloadParameterType = ParameterType.AUDIO | ParameterType.VIDEO | ParameterType.IMAGE;
+
+export type ParameterInfo = (
+  | { type: Exclude }
+  | { type: ParameterType.COMPLEX; nested?: ParameterInfos }
+  | { type: PreloadParameterType; preload?: boolean }
+) & {
   array?: boolean;
   pretty_name?: string;
   default?: any;
-  preload?: boolean;
-}
+};
 
-export interface ParameterInfos {
-  [key: string]: ParameterInfo;
-}
+export type ParameterInfos = Record;
 
 type InferredParameter = I["array"] extends true
   ? Array
@@ -123,7 +115,7 @@ export const universalPluginParameters = {
   post_trial_gap: {
     type: ParameterType.INT,
     pretty_name: "Post trial gap",
-    default: null,
+    default: 0,
   },
   /**
    * A list of CSS classes to add to the jsPsych display element for the duration of this trial
@@ -131,14 +123,14 @@ export const universalPluginParameters = {
   css_classes: {
     type: ParameterType.STRING,
     pretty_name: "Custom CSS classes",
-    default: null,
+    default: "",
   },
   /**
    * Options to control simulation mode for the trial.
    */
   simulation_options: {
     type: ParameterType.COMPLEX,
-    default: null,
+    default: {},
   },
 };
 
@@ -146,9 +138,9 @@ export type UniversalPluginParameters = InferredParameters {
@@ -156,10 +148,17 @@ export interface JsPsychPlugin {
     display_element: HTMLElement,
     trial: TrialType,
     on_load?: () => void
-  ): void | Promise;
+  ): void | Promise;
+
+  simulate?(
+    trial: TrialType,
+    simulation_mode: SimulationMode,
+    simulation_options: SimulationOptions,
+    on_load?: () => void
+  ): void | Promise;
 }
 
 export type TrialType = InferredParameters &
-  UniversalPluginParameters;
+  TrialDescription;
 
 export type PluginParameters = InferredParameters;
diff --git a/packages/jspsych/src/modules/randomization.ts b/packages/jspsych/src/modules/randomization.ts
index f54d62a29f..6343d76205 100644
--- a/packages/jspsych/src/modules/randomization.ts
+++ b/packages/jspsych/src/modules/randomization.ts
@@ -264,7 +264,7 @@ export function randomID(length = 32) {
  */
 export function randomInt(lower: number, upper: number) {
   if (upper < lower) {
-    throw new Error("Upper boundary must be less than or equal to lower boundary");
+    throw new Error("Upper boundary must be greater than or equal to lower boundary");
   }
   return lower + Math.floor(Math.random() * (upper - lower + 1));
 }
diff --git a/packages/jspsych/src/timeline/Timeline.spec.ts b/packages/jspsych/src/timeline/Timeline.spec.ts
new file mode 100644
index 0000000000..b87e4f4929
--- /dev/null
+++ b/packages/jspsych/src/timeline/Timeline.spec.ts
@@ -0,0 +1,921 @@
+import { flushPromises } from "@jspsych/test-utils";
+
+import { TimelineNodeDependenciesMock, createSnapshotUtils } from "../../tests/test-utils";
+import TestPlugin from "../../tests/TestPlugin";
+import { DataCollection } from "../modules/data/DataCollection";
+import {
+  repeat,
+  sampleWithReplacement,
+  sampleWithoutReplacement,
+  shuffle,
+  shuffleAlternateGroups,
+} from "../modules/randomization";
+import { Timeline } from "./Timeline";
+import { Trial } from "./Trial";
+import {
+  SampleOptions,
+  TimelineArray,
+  TimelineDescription,
+  TimelineNodeStatus,
+  TimelineVariable,
+} from ".";
+
+jest.useFakeTimers();
+
+jest.mock("../modules/randomization");
+
+const exampleTimeline: TimelineDescription = {
+  timeline: [{ type: TestPlugin }, { type: TestPlugin }, { timeline: [{ type: TestPlugin }] }],
+};
+
+describe("Timeline", () => {
+  let dependencies: TimelineNodeDependenciesMock;
+
+  beforeEach(() => {
+    dependencies = new TimelineNodeDependenciesMock();
+    TestPlugin.reset();
+  });
+
+  const createTimeline = (description: TimelineDescription | TimelineArray, parent?: Timeline) =>
+    new Timeline(dependencies, description, parent);
+
+  describe("run()", () => {
+    it("instantiates proper child nodes", async () => {
+      const timeline = createTimeline([
+        { type: TestPlugin },
+        { timeline: [{ type: TestPlugin }, { type: TestPlugin }] },
+        { timeline: [{ type: TestPlugin }] },
+      ]);
+
+      await timeline.run();
+
+      const children = timeline.children;
+      expect(children).toEqual([expect.any(Trial), expect.any(Timeline), expect.any(Timeline)]);
+      expect((children[1] as Timeline).children).toEqual([expect.any(Trial), expect.any(Trial)]);
+      expect((children[2] as Timeline).children).toEqual([expect.any(Trial)]);
+
+      expect(children.map((child) => child.index)).toEqual([0, 1, 3]);
+      expect((children[1] as Timeline).children.map((child) => child.index)).toEqual([1, 2]);
+    });
+
+    describe("with `pause()` and `resume()` calls`", () => {
+      beforeEach(() => {
+        TestPlugin.setManualFinishTrialMode();
+      });
+
+      it("pauses, resumes, and updates the results of getStatus()", async () => {
+        const timeline = createTimeline({
+          timeline: [
+            { type: TestPlugin },
+            { type: TestPlugin },
+            { timeline: [{ type: TestPlugin }, { type: TestPlugin }] },
+          ],
+        });
+        const runPromise = timeline.run();
+
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.RUNNING);
+        expect(timeline.children[0].getStatus()).toBe(TimelineNodeStatus.RUNNING);
+        await TestPlugin.finishTrial();
+
+        expect(timeline.children[0].getStatus()).toBe(TimelineNodeStatus.COMPLETED);
+        expect(timeline.children[1].getStatus()).toBe(TimelineNodeStatus.RUNNING);
+        timeline.pause();
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.PAUSED);
+
+        await TestPlugin.finishTrial();
+        expect(timeline.children[1].getStatus()).toBe(TimelineNodeStatus.COMPLETED);
+        expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.PENDING);
+
+        // Resolving the next trial promise shouldn't continue the experiment since no trial should be running.
+        await TestPlugin.finishTrial();
+
+        expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.PENDING);
+
+        timeline.resume();
+        await flushPromises();
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.RUNNING);
+        expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.RUNNING);
+
+        // The child timeline is running. Let's pause the parent timeline to check whether the child
+        // gets paused too
+        timeline.pause();
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.PAUSED);
+        expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.PAUSED);
+
+        await TestPlugin.finishTrial();
+        timeline.resume();
+        await flushPromises();
+        expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.RUNNING);
+
+        await TestPlugin.finishTrial();
+
+        expect(timeline.children[2].getStatus()).toBe(TimelineNodeStatus.COMPLETED);
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.COMPLETED);
+
+        await runPromise;
+      });
+
+      // https://www.jspsych.org/7.1/reference/jspsych/#jspsychresumeexperiment
+      it("doesn't affect `post_trial_gap`", async () => {
+        const timeline = createTimeline([{ type: TestPlugin, post_trial_gap: 200 }]);
+        const runPromise = timeline.run();
+        let hasTimelineCompleted = false;
+        runPromise.then(() => {
+          hasTimelineCompleted = true;
+        });
+
+        expect(hasTimelineCompleted).toBe(false);
+        await TestPlugin.finishTrial();
+        expect(hasTimelineCompleted).toBe(false);
+
+        timeline.pause();
+        jest.advanceTimersByTime(100);
+        timeline.resume();
+        await flushPromises();
+        expect(hasTimelineCompleted).toBe(false);
+
+        jest.advanceTimersByTime(100);
+        await flushPromises();
+        expect(hasTimelineCompleted).toBe(true);
+      });
+    });
+
+    describe("abort()", () => {
+      beforeEach(() => {
+        TestPlugin.setManualFinishTrialMode();
+      });
+
+      describe("aborts the timeline after the current trial ends, updating the result of getStatus()", () => {
+        test("when the timeline is running", async () => {
+          const timeline = createTimeline(exampleTimeline);
+          const runPromise = timeline.run();
+
+          expect(timeline.getStatus()).toBe(TimelineNodeStatus.RUNNING);
+          timeline.abort();
+          expect(timeline.getStatus()).toBe(TimelineNodeStatus.RUNNING);
+          await TestPlugin.finishTrial();
+          expect(timeline.getStatus()).toBe(TimelineNodeStatus.ABORTED);
+          await runPromise;
+        });
+
+        test("when the timeline is paused", async () => {
+          const timeline = createTimeline(exampleTimeline);
+          timeline.run();
+
+          timeline.pause();
+          await TestPlugin.finishTrial();
+          expect(timeline.getStatus()).toBe(TimelineNodeStatus.PAUSED);
+          timeline.abort();
+          await flushPromises();
+          expect(timeline.getStatus()).toBe(TimelineNodeStatus.ABORTED);
+        });
+      });
+
+      it("aborts child timelines too", async () => {
+        const timeline = createTimeline({
+          timeline: [{ timeline: [{ type: TestPlugin }, { type: TestPlugin }] }],
+        });
+        const runPromise = timeline.run();
+
+        expect(timeline.children[0].getStatus()).toBe(TimelineNodeStatus.RUNNING);
+        timeline.abort();
+        await TestPlugin.finishTrial();
+        expect(timeline.children[0].getStatus()).toBe(TimelineNodeStatus.ABORTED);
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.ABORTED);
+        await runPromise;
+      });
+
+      it("doesn't affect the timeline when it is neither running nor paused", async () => {
+        const timeline = createTimeline([{ type: TestPlugin }]);
+
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.PENDING);
+        timeline.abort();
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.PENDING);
+
+        // Complete the timeline
+        const runPromise = timeline.run();
+        await TestPlugin.finishTrial();
+        await runPromise;
+
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.COMPLETED);
+        timeline.abort();
+        expect(timeline.getStatus()).toBe(TimelineNodeStatus.COMPLETED);
+      });
+    });
+
+    it("repeats a timeline according to `repetitions`", async () => {
+      const timeline = createTimeline({ ...exampleTimeline, repetitions: 2 });
+
+      await timeline.run();
+
+      expect(timeline.children.length).toEqual(6);
+    });
+
+    it("repeats a timeline according to `loop_function`", async () => {
+      const loopFunction = jest.fn();
+      loopFunction.mockReturnValue(false);
+      loopFunction.mockReturnValueOnce(true);
+
+      const timeline = createTimeline({ ...exampleTimeline, loop_function: loopFunction });
+
+      await timeline.run();
+      expect(loopFunction).toHaveBeenCalledTimes(2);
+
+      for (const call of loopFunction.mock.calls) {
+        expect(call[0]).toBeInstanceOf(DataCollection);
+        expect((call[0] as DataCollection).values()).toEqual(
+          Array(3).fill(expect.objectContaining({ my: "result" }))
+        );
+      }
+
+      expect(timeline.children.length).toEqual(6);
+    });
+
+    it("repeats a timeline according to `repetitions` and `loop_function`", async () => {
+      const loopFunction = jest.fn();
+      loopFunction.mockReturnValue(false);
+      loopFunction.mockReturnValueOnce(true);
+      loopFunction.mockReturnValueOnce(false);
+      loopFunction.mockReturnValueOnce(true);
+
+      const timeline = createTimeline({
+        ...exampleTimeline,
+        repetitions: 2,
+        loop_function: loopFunction,
+      });
+
+      await timeline.run();
+      expect(loopFunction).toHaveBeenCalledTimes(4);
+      expect(timeline.children.length).toEqual(12);
+    });
+
+    it("skips execution if `conditional_function` returns `false`", async () => {
+      const timeline = createTimeline({
+        ...exampleTimeline,
+        conditional_function: jest.fn(() => false),
+      });
+
+      await timeline.run();
+      expect(timeline.children.length).toEqual(0);
+    });
+
+    it("executes regularly if `conditional_function` returns `true`", async () => {
+      const timeline = createTimeline({
+        ...exampleTimeline,
+        conditional_function: jest.fn(() => true),
+      });
+
+      await timeline.run();
+      expect(timeline.children.length).toEqual(3);
+    });
+
+    it("invokes `on_timeline_start` and `on_timeline_finished` callbacks at the beginning and at the end of the timeline, respectively", async () => {
+      TestPlugin.setManualFinishTrialMode();
+
+      const onTimelineStart = jest.fn();
+      const onTimelineFinish = jest.fn();
+
+      const timeline = createTimeline({
+        timeline: [{ type: TestPlugin }],
+        on_timeline_start: onTimelineStart,
+        on_timeline_finish: onTimelineFinish,
+        repetitions: 2,
+      });
+      timeline.run();
+      expect(onTimelineStart).toHaveBeenCalledTimes(1);
+      expect(onTimelineFinish).toHaveBeenCalledTimes(0);
+
+      await TestPlugin.finishTrial();
+      await TestPlugin.finishTrial();
+
+      expect(onTimelineStart).toHaveBeenCalledTimes(1);
+      expect(onTimelineFinish).toHaveBeenCalledTimes(1);
+    });
+
+    it("loop function ignores data from trials where `record_data` is false", async () => {
+      const loopFunction = jest.fn();
+      loopFunction.mockReturnValue(false);
+
+      const timeline = createTimeline({
+        timeline: [{ type: TestPlugin, record_data: false }, { type: TestPlugin }],
+        loop_function: loopFunction,
+      });
+
+      await timeline.run();
+      expect((loopFunction.mock.calls[0][0] as DataCollection).count()).toBe(1);
+    });
+
+    describe("with timeline variables", () => {
+      it("repeats all trials for each set of variables", async () => {
+        const xValues = [];
+        TestPlugin.trial = async () => {
+          xValues.push(timeline.evaluateTimelineVariable(new TimelineVariable("x")));
+        };
+
+        const timeline = createTimeline({
+          timeline: [{ type: TestPlugin }],
+          timeline_variables: [{ x: 0 }, { x: 1 }, { x: 2 }, { x: 3 }],
+        });
+
+        await timeline.run();
+        expect(timeline.children.length).toEqual(4);
+        expect(xValues).toEqual([0, 1, 2, 3]);
+      });
+
+      it("respects the `randomize_order` and `sample` options", async () => {
+        let xValues: number[];
+
+        const createSampleTimeline = (sample: SampleOptions, randomize_order?: boolean) => {
+          xValues = [];
+          const timeline = createTimeline({
+            timeline: [{ type: TestPlugin }],
+            timeline_variables: [{ x: 0 }, { x: 1 }],
+            sample,
+            randomize_order,
+          });
+          TestPlugin.trial = async () => {
+            xValues.push(timeline.evaluateTimelineVariable(new TimelineVariable("x")));
+          };
+          return timeline;
+        };
+
+        // `randomize_order`
+        jest.mocked(shuffle).mockReturnValue([1, 0]);
+        await createSampleTimeline(undefined, true).run();
+        expect(shuffle).toHaveBeenCalledWith([0, 1]);
+        expect(xValues).toEqual([1, 0]);
+
+        // with-replacement
+        jest.mocked(sampleWithReplacement).mockReturnValue([0, 0]);
+        await createSampleTimeline({ type: "with-replacement", size: 2, weights: [1, 1] }).run();
+        expect(sampleWithReplacement).toHaveBeenCalledWith([0, 1], 2, [1, 1]);
+        expect(xValues).toEqual([0, 0]);
+
+        // without-replacement
+        jest.mocked(sampleWithoutReplacement).mockReturnValue([1, 0]);
+        await createSampleTimeline({ type: "without-replacement", size: 2 }).run();
+        expect(sampleWithoutReplacement).toHaveBeenCalledWith([0, 1], 2);
+        expect(xValues).toEqual([1, 0]);
+
+        // fixed-repetitions
+        jest.mocked(repeat).mockReturnValue([0, 0, 1, 1]);
+        await createSampleTimeline({ type: "fixed-repetitions", size: 2 }).run();
+        expect(repeat).toHaveBeenCalledWith([0, 1], 2);
+        expect(xValues).toEqual([0, 0, 1, 1]);
+
+        // alternate-groups
+        jest.mocked(shuffleAlternateGroups).mockReturnValue([1, 0]);
+        await createSampleTimeline({
+          type: "alternate-groups",
+          groups: [[0], [1]],
+          randomize_group_order: true,
+        }).run();
+        expect(shuffleAlternateGroups).toHaveBeenCalledWith([[0], [1]], true);
+        expect(xValues).toEqual([1, 0]);
+
+        // custom function
+        const sampleFunction = jest.fn(() => [0]);
+        await createSampleTimeline({ type: "custom", fn: sampleFunction }).run();
+        expect(sampleFunction).toHaveBeenCalledTimes(1);
+        expect(xValues).toEqual([0]);
+
+        await expect(
+          // @ts-expect-error non-existing type
+          createSampleTimeline({ type: "invalid" }).run()
+        ).rejects.toThrow('Invalid type "invalid" in timeline sample parameters.');
+      });
+
+      it("samples on each loop iteration (be it via `repetitions` or `loop_function`)", async () => {
+        const sampleFunction = jest.fn(() => [0]);
+
+        await createTimeline({
+          timeline: [{ type: TestPlugin }],
+          timeline_variables: [{ x: 0 }],
+          sample: { type: "custom", fn: sampleFunction },
+          repetitions: 2,
+          loop_function: jest.fn().mockReturnValue(false).mockReturnValueOnce(true),
+        }).run();
+
+        // 2 repetitions + 1 loop in the first repitition = 3 sample function calls
+        expect(sampleFunction).toHaveBeenCalledTimes(3);
+      });
+
+      it("makes variables available to callbacks", async () => {
+        const variableResults: Record = {};
+        const makeCallback = (resultName: string, callbackReturnValue?: any) => () => {
+          variableResults[resultName] = timeline.evaluateTimelineVariable(
+            new TimelineVariable("x")
+          );
+          return callbackReturnValue;
+        };
+
+        const timeline = createTimeline({
+          timeline: [{ type: TestPlugin }],
+          timeline_variables: [{ x: 0 }],
+          on_timeline_start: jest.fn().mockImplementation(makeCallback("on_timeline_start")),
+          on_timeline_finish: jest.fn().mockImplementation(makeCallback("on_timeline_finish")),
+          conditional_function: jest
+            .fn()
+            .mockImplementation(makeCallback("conditional_function", true)),
+          loop_function: jest.fn().mockImplementation(makeCallback("loop_function", false)),
+        });
+
+        await timeline.run();
+        expect(variableResults).toEqual({
+          on_timeline_start: 0,
+          on_timeline_finish: 0,
+          conditional_function: 0,
+          loop_function: 0,
+        });
+      });
+    });
+  });
+
+  describe("getAllTimelineVariables()", () => {
+    it("returns the current values of all timeline variables, including those from parent timelines", async () => {
+      const timeline = createTimeline({
+        timeline: [{ timeline: [{ type: TestPlugin }], timeline_variables: [{ y: 1, z: 1 }] }],
+        timeline_variables: [{ x: 0, y: 0 }],
+      });
+
+      await timeline.run();
+
+      expect(timeline.getAllTimelineVariables()).toEqual({ x: 0, y: 0 });
+      expect((timeline.children[0] as Timeline).getAllTimelineVariables()).toEqual({
+        x: 0,
+        y: 1,
+        z: 1,
+      });
+    });
+  });
+
+  describe("evaluateTimelineVariable()", () => {
+    describe("if a local timeline variable exists", () => {
+      it("returns the local timeline variable", async () => {
+        const timeline = createTimeline({
+          timeline: [{ type: TestPlugin }],
+          timeline_variables: [{ x: 0 }],
+        });
+
+        await timeline.run();
+        expect(timeline.evaluateTimelineVariable(new TimelineVariable("x"))).toEqual(0);
+      });
+    });
+
+    describe("if a timeline variable is not defined locally", () => {
+      it("falls back to parent timeline variables", async () => {
+        const timeline = createTimeline({
+          timeline: [{ timeline: [{ type: TestPlugin }], timeline_variables: [{ x: undefined }] }],
+          timeline_variables: [{ x: 0, y: 0 }],
+        });
+
+        await timeline.run();
+        expect(timeline.evaluateTimelineVariable(new TimelineVariable("x"))).toEqual(0);
+        expect(timeline.evaluateTimelineVariable(new TimelineVariable("y"))).toEqual(0);
+
+        const childTimeline = timeline.children[0] as Timeline;
+        expect(childTimeline.evaluateTimelineVariable(new TimelineVariable("x"))).toBeUndefined();
+        expect(childTimeline.evaluateTimelineVariable(new TimelineVariable("y"))).toEqual(0);
+      });
+
+      it("throws an exception if there are no parents or none of them has a value for the variable", async () => {
+        const timeline = createTimeline({
+          timeline: [{ timeline: [{ type: TestPlugin }] }],
+        });
+
+        const variable = new TimelineVariable("x");
+
+        await timeline.run();
+        expect(() => timeline.evaluateTimelineVariable(variable)).toThrowError("");
+        expect(() =>
+          (timeline.children[0] as Timeline).evaluateTimelineVariable(variable)
+        ).toThrowError("");
+      });
+    });
+  });
+
+  describe("getParameterValue()", () => {
+    // Note: This includes test cases for the implementation provided by `BaseTimelineNode`.
+
+    it("returns the local parameter value, if it exists", async () => {
+      const timeline = createTimeline({ timeline: [], my_parameter: "test" });
+
+      expect(timeline.getParameterValue("my_parameter")).toEqual("test");
+      expect(timeline.getParameterValue("other_parameter")).toBeUndefined();
+    });
+
+    it("falls back to parent parameter values if `recursive` is not `false`", async () => {
+      const parentTimeline = createTimeline({
+        timeline: [],
+        first_parameter: "test",
+        second_parameter: "test",
+      });
+      const childTimeline = createTimeline(
+        { timeline: [], first_parameter: undefined },
+        parentTimeline
+      );
+
+      expect(childTimeline.getParameterValue("second_parameter", { cacheResult: false })).toEqual(
+        "test"
+      );
+      expect(
+        childTimeline.getParameterValue("second_parameter", { recursive: false })
+      ).toBeUndefined();
+
+      expect(childTimeline.getParameterValue("first_parameter")).toBeUndefined();
+      expect(childTimeline.getParameterValue("other_parameter")).toBeUndefined();
+    });
+
+    it("evaluates timeline variables", async () => {
+      const timeline = createTimeline({
+        timeline: [{ timeline: [], child_parameter: new TimelineVariable("x") }],
+        timeline_variables: [{ x: 0 }],
+        parent_parameter: new TimelineVariable("x"),
+      });
+
+      await timeline.run();
+
+      expect(timeline.children[0].getParameterValue("child_parameter")).toEqual(0);
+      expect(timeline.children[0].getParameterValue("parent_parameter")).toEqual(0);
+    });
+
+    it("evaluates functions unless `evaluateFunctions` is set to `false`", async () => {
+      const timeline = createTimeline({
+        timeline: [],
+        function_parameter: jest.fn(() => "result"),
+      });
+
+      expect(timeline.getParameterValue("function_parameter", { cacheResult: false })).toEqual(
+        "result"
+      );
+
+      expect(
+        timeline.getParameterValue("function_parameter", {
+          evaluateFunctions: true,
+          cacheResult: false,
+        })
+      ).toEqual("result");
+
+      expect(
+        typeof timeline.getParameterValue("function_parameter", { evaluateFunctions: false })
+      ).toEqual("function");
+    });
+
+    it("considers nested properties if `parameterName` is an array", async () => {
+      const timeline = createTimeline({
+        timeline: [],
+        object: {
+          childString: "foo",
+          childObject: {
+            childString: "bar",
+          },
+        },
+      });
+
+      expect(timeline.getParameterValue(["object", "childString"])).toEqual("foo");
+      expect(timeline.getParameterValue(["object", "childObject"])).toEqual({ childString: "bar" });
+      expect(timeline.getParameterValue(["object", "childObject", "childString"])).toEqual("bar");
+    });
+
+    it("respects the `replaceResult` function", () => {
+      const timeline = createTimeline({ timeline: [] });
+
+      expect(timeline.getParameterValue("key", { replaceResult: () => "value" })).toBe("value");
+    });
+
+    it("caches results and uses them for nested lookups", async () => {
+      const timeline = createTimeline({ timeline: [], object: () => ({ child: "foo" }) });
+
+      expect(
+        timeline.getParameterValue("object", {
+          replaceResult: () => ({ child: "bar" }),
+        })
+      ).toEqual({ child: "bar" });
+      expect(timeline.getParameterValue(["object", "child"])).toEqual("bar");
+    });
+
+    it("does not cache results when `cacheResult` is set to false", async () => {
+      const timeline = createTimeline({ timeline: [], object: { child: "foo" } });
+
+      expect(
+        timeline.getParameterValue("object", {
+          replaceResult: () => ({ child: "bar" }),
+          cacheResult: false,
+        })
+      ).toEqual({ child: "bar" });
+      expect(timeline.getParameterValue(["object", "child"])).toEqual("foo");
+    });
+
+    test("all result caches are reset after every trial", async () => {
+      TestPlugin.setManualFinishTrialMode();
+
+      const timeline = createTimeline({
+        timeline: [
+          {
+            timeline: [{ type: TestPlugin }, { type: TestPlugin }],
+            object1: jest.fn().mockReturnValueOnce({ child: "foo" }),
+          },
+        ],
+        object2: jest.fn().mockReturnValueOnce({ child: "foo" }),
+      });
+
+      timeline.run();
+      const childTimeline = timeline.children[0];
+
+      // First trial
+      for (const parameter of ["object1", "object2"]) {
+        expect(childTimeline.getParameterValue(parameter)).toEqual({ child: "foo" });
+        expect(childTimeline.getParameterValue([parameter, "child"])).toEqual("foo");
+      }
+
+      await TestPlugin.finishTrial();
+
+      // Second trial, caches should have been reset
+      for (const parameter of ["object1", "object2"]) {
+        expect(childTimeline.getParameterValue(parameter)).toBeUndefined();
+        expect(childTimeline.getParameterValue([parameter, "child"])).toBeUndefined();
+      }
+    });
+  });
+
+  describe("getDataParameter()", () => {
+    it("works when the `data` parameter is a function", async () => {
+      const timeline = createTimeline({ timeline: [], data: () => ({ custom: "value" }) });
+      expect(timeline.getDataParameter()).toEqual({ custom: "value" });
+    });
+
+    it("evaluates nested functions and timeline variables", async () => {
+      const timeline = createTimeline({
+        timeline: [],
+        timeline_variables: [{ x: 1 }],
+        data: {
+          custom: () => "value",
+          variable: new TimelineVariable("x"),
+        },
+      });
+
+      await timeline.run(); // required to properly evaluate timeline variables
+
+      expect(timeline.getDataParameter()).toEqual({ custom: "value", variable: 1 });
+    });
+
+    it("merges in all parent node `data` parameters", async () => {
+      const timeline = createTimeline({
+        timeline: [{ timeline: [], data: { custom: "value" } }],
+        data: { other: "value" },
+      });
+
+      await timeline.run();
+
+      expect((timeline.children[0] as Timeline).getDataParameter()).toEqual({
+        custom: "value",
+        other: "value",
+      });
+    });
+  });
+
+  describe("getResults()", () => {
+    it("recursively returns all results", async () => {
+      const timeline = createTimeline(exampleTimeline);
+      await timeline.run();
+      expect(timeline.getResults()).toEqual(
+        Array(3).fill(expect.objectContaining({ my: "result" }))
+      );
+    });
+
+    it("does not include `undefined` results", async () => {
+      const timeline = createTimeline(exampleTimeline);
+      await timeline.run();
+
+      jest.spyOn(timeline.children[0] as Trial, "getResult").mockReturnValue(undefined);
+      expect(timeline.getResults()).toEqual(
+        Array(2).fill(expect.objectContaining({ my: "result" }))
+      );
+    });
+  });
+
+  describe("getNaiveProgress()", () => {
+    it("returns the progress of a timeline at any time", async () => {
+      TestPlugin.setManualFinishTrialMode();
+      const { snapshots, createSnapshotCallback } = createSnapshotUtils(() =>
+        timeline.getNaiveProgress()
+      );
+
+      const timeline = createTimeline({
+        on_timeline_start: createSnapshotCallback("mainTimelineStart"),
+        on_timeline_finish: createSnapshotCallback("mainTimelineFinish"),
+        timeline: [
+          {
+            type: TestPlugin,
+            on_start: createSnapshotCallback("trial1Start"),
+            on_finish: createSnapshotCallback("trial1Finish"),
+          },
+          {
+            on_timeline_start: createSnapshotCallback("nestedTimelineStart"),
+            on_timeline_finish: createSnapshotCallback("nestedTimelineFinish"),
+            timeline: [{ type: TestPlugin }, { type: TestPlugin }],
+            repetitions: 2,
+          },
+        ],
+      });
+      expect(timeline.getNaiveProgress()).toEqual(0);
+
+      const runPromise = timeline.run();
+      expect(timeline.getNaiveProgress()).toEqual(0);
+      expect(snapshots.mainTimelineStart).toEqual(0);
+      expect(snapshots.trial1Start).toEqual(0);
+
+      await TestPlugin.finishTrial();
+      expect(timeline.getNaiveProgress()).toEqual(0.2);
+      expect(snapshots.trial1Finish).toEqual(0.2);
+      expect(snapshots.nestedTimelineStart).toEqual(0.2);
+
+      await TestPlugin.finishTrial();
+      expect(timeline.getNaiveProgress()).toEqual(0.4);
+
+      await TestPlugin.finishTrial();
+      expect(timeline.getNaiveProgress()).toEqual(0.6);
+
+      await TestPlugin.finishTrial();
+      expect(timeline.getNaiveProgress()).toEqual(0.8);
+
+      await TestPlugin.finishTrial();
+      expect(timeline.getNaiveProgress()).toEqual(1);
+      expect(snapshots.nestedTimelineFinish).toEqual(1);
+      expect(snapshots.mainTimelineFinish).toEqual(1);
+
+      await runPromise;
+      expect(timeline.getNaiveProgress()).toEqual(1);
+    });
+
+    it("does not return values above 1", async () => {
+      const timeline = createTimeline({
+        timeline: [{ type: TestPlugin }],
+        loop_function: jest.fn().mockReturnValue(false).mockReturnValueOnce(true),
+      });
+
+      await timeline.run();
+      expect(timeline.getNaiveProgress()).toEqual(1);
+    });
+  });
+
+  describe("getNaiveTrialCount()", () => {
+    it("correctly estimates the length of a timeline (including nested timelines)", async () => {
+      const timeline = createTimeline({
+        timeline: [
+          { type: TestPlugin },
+          { timeline: [{ type: TestPlugin }], repetitions: 2, timeline_variables: [] },
+          { timeline: [{ type: TestPlugin }], repetitions: 5 },
+        ],
+        repetitions: 3,
+        timeline_variables: [{ x: 1 }, { x: 2 }],
+      });
+
+      const estimate = (1 + 1 * 2 + 1 * 5) * 3 * 2;
+      expect(timeline.getNaiveTrialCount()).toEqual(estimate);
+    });
+
+    describe("when the `sample` option is used", () => {
+      it("handles `with-replacement` sampling", async () => {
+        expect(
+          createTimeline({
+            timeline: [{ type: TestPlugin }],
+            timeline_variables: [{}, {}],
+            sample: { type: "with-replacement", size: 5 },
+          }).getNaiveTrialCount()
+        ).toEqual(5);
+      });
+
+      it("handles `without-replacement` sampling", async () => {
+        expect(
+          createTimeline({
+            timeline: [{ type: TestPlugin }],
+            timeline_variables: [{}, {}],
+            sample: { type: "without-replacement", size: 5 },
+          }).getNaiveTrialCount()
+        ).toEqual(5);
+      });
+
+      it("handles `fixed-repetitions` sampling", async () => {
+        expect(
+          createTimeline({
+            timeline: [{ type: TestPlugin }],
+            timeline_variables: [{}, {}],
+            sample: { type: "fixed-repetitions", size: 5 },
+          }).getNaiveTrialCount()
+        ).toEqual(10);
+      });
+
+      it("handles `alternate-groups` sampling", async () => {
+        expect(
+          createTimeline({
+            timeline: [{ type: TestPlugin }],
+            timeline_variables: [{}, {}, {}, {}],
+            sample: {
+              type: "alternate-groups",
+              groups: [
+                [0, 1],
+                [2, 3],
+              ],
+            },
+          }).getNaiveTrialCount()
+        ).toEqual(4);
+
+        expect(
+          createTimeline({
+            timeline: [{ type: TestPlugin }],
+            timeline_variables: [{}, {}, {}, {}],
+            sample: {
+              type: "alternate-groups",
+              groups: [[0, 1], [2]],
+            },
+          }).getNaiveTrialCount()
+        ).toEqual(3);
+      });
+    });
+  });
+
+  describe("getLatestNode()", () => {
+    it("returns the latest `TimelineNode` or `undefined` when no node is active", async () => {
+      TestPlugin.setManualFinishTrialMode();
+      const { snapshots, createSnapshotCallback } = createSnapshotUtils(() =>
+        timeline.getLatestNode()
+      );
+
+      const timeline = createTimeline({
+        timeline: [
+          { type: TestPlugin },
+          {
+            timeline: [{ type: TestPlugin }],
+            on_timeline_start: createSnapshotCallback("innerTimelineStart"),
+            on_timeline_finish: createSnapshotCallback("innerTimelineFinish"),
+          },
+        ],
+        on_timeline_start: createSnapshotCallback("outerTimelineStart"),
+        on_timeline_finish: createSnapshotCallback("outerTimelineFinish"),
+      });
+
+      expect(timeline.getLatestNode()).toBe(timeline);
+
+      timeline.run();
+
+      expect(snapshots.outerTimelineStart).toBe(timeline);
+      expect(timeline.getLatestNode()).toBeInstanceOf(Trial);
+      expect(timeline.getLatestNode()).toBe(timeline.children[0]);
+
+      await TestPlugin.finishTrial();
+      expect(snapshots.innerTimelineStart).toBeInstanceOf(Timeline);
+      expect(snapshots.innerTimelineStart).toBe(timeline.children[1]);
+
+      const nestedTrial = (timeline.children[1] as Timeline).children[0];
+      expect(timeline.getLatestNode()).toBeInstanceOf(Trial);
+      expect(timeline.getLatestNode()).toBe(nestedTrial);
+
+      await TestPlugin.finishTrial();
+      expect(snapshots.innerTimelineFinish).toBe(nestedTrial);
+      expect(snapshots.outerTimelineFinish).toBe(nestedTrial);
+      expect(timeline.getLatestNode()).toBe(nestedTrial);
+    });
+  });
+
+  describe("getActiveTimelineByName()", () => {
+    it("returns the timeline with the given name", async () => {
+      TestPlugin.setManualFinishTrialMode();
+
+      const timeline = createTimeline({
+        timeline: [{ timeline: [{ type: TestPlugin }], name: "innerTimeline" }],
+        name: "outerTimeline",
+      });
+
+      timeline.run();
+
+      expect(timeline.getActiveTimelineByName("outerTimeline")).toBe(timeline);
+      expect(timeline.getActiveTimelineByName("innerTimeline")).toBe(
+        timeline.children[0] as Timeline
+      );
+    });
+
+    it("returns only active timelines", async () => {
+      TestPlugin.setManualFinishTrialMode();
+
+      const timeline = createTimeline({
+        timeline: [
+          { type: TestPlugin },
+          { timeline: [{ type: TestPlugin }], name: "innerTimeline" },
+        ],
+        name: "outerTimeline",
+      });
+
+      timeline.run();
+
+      expect(timeline.getActiveTimelineByName("outerTimeline")).toBe(timeline);
+      expect(timeline.getActiveTimelineByName("innerTimeline")).toBeUndefined();
+
+      await TestPlugin.finishTrial();
+
+      expect(timeline.getActiveTimelineByName("innerTimeline")).toBe(
+        timeline.children[1] as Timeline
+      );
+    });
+  });
+});
diff --git a/packages/jspsych/src/timeline/Timeline.ts b/packages/jspsych/src/timeline/Timeline.ts
new file mode 100644
index 0000000000..8628f60324
--- /dev/null
+++ b/packages/jspsych/src/timeline/Timeline.ts
@@ -0,0 +1,342 @@
+import { DataCollection } from "../modules/data/DataCollection";
+import {
+  repeat,
+  sampleWithReplacement,
+  sampleWithoutReplacement,
+  shuffle,
+  shuffleAlternateGroups,
+} from "../modules/randomization";
+import { TimelineNode } from "./TimelineNode";
+import { Trial } from "./Trial";
+import { PromiseWrapper } from "./util";
+import {
+  TimelineArray,
+  TimelineDescription,
+  TimelineNodeDependencies,
+  TimelineNodeStatus,
+  TimelineVariable,
+  TrialDescription,
+  TrialResult,
+  isTimelineDescription,
+  isTrialDescription,
+} from ".";
+
+export class Timeline extends TimelineNode {
+  public readonly children: TimelineNode[] = [];
+  public readonly description: TimelineDescription;
+
+  constructor(
+    dependencies: TimelineNodeDependencies,
+    description: TimelineDescription | TimelineArray,
+    public readonly parent?: Timeline
+  ) {
+    super(dependencies);
+    this.description = Array.isArray(description) ? { timeline: description } : description;
+    this.initializeParameterValueCache();
+  }
+
+  private currentChild?: TimelineNode;
+  private shouldAbort = false;
+
+  public async run() {
+    if (typeof this.index === "undefined") {
+      // We're the first timeline node to run. Otherwise, another node would have set our index
+      // right before running us.
+      this.index = 0;
+    }
+
+    this.status = TimelineNodeStatus.RUNNING;
+
+    const { conditional_function, loop_function, repetitions = 1 } = this.description;
+
+    // Generate initial timeline variable order so the first set of timeline variables is already
+    // available to the `on_timeline_start` and `conditional_function` callbacks
+    let timelineVariableOrder = this.generateTimelineVariableOrder();
+    this.setCurrentTimelineVariablesByIndex(timelineVariableOrder[0]);
+    let isInitialTimelineVariableOrder = true; // So we don't regenerate the order in the first iteration
+
+    let currentLoopIterationResults: TrialResult[];
+
+    if (!conditional_function || conditional_function()) {
+      this.onStart();
+
+      for (let repetition = 0; repetition < repetitions; repetition++) {
+        do {
+          currentLoopIterationResults = [];
+
+          // Generate a new timeline variable order in each iteration except for the first one where
+          // it has been done before
+          if (isInitialTimelineVariableOrder) {
+            isInitialTimelineVariableOrder = false;
+          } else {
+            timelineVariableOrder = this.generateTimelineVariableOrder();
+          }
+
+          for (const timelineVariableIndex of timelineVariableOrder) {
+            this.setCurrentTimelineVariablesByIndex(timelineVariableIndex);
+
+            for (const childNode of this.instantiateChildNodes()) {
+              const previousChild = this.currentChild;
+              this.currentChild = childNode;
+              childNode.index = previousChild
+                ? previousChild.getLatestNode().index + 1
+                : this.index;
+
+              await childNode.run();
+              // @ts-expect-error TS thinks `this.status` must be `RUNNING` now, but it might have
+              // changed while `await`ing
+              if (this.status === TimelineNodeStatus.PAUSED) {
+                await this.resumePromise.get();
+              }
+              if (this.shouldAbort) {
+                this.status = TimelineNodeStatus.ABORTED;
+                return;
+              }
+
+              currentLoopIterationResults.push(...this.currentChild.getResults());
+            }
+          }
+        } while (loop_function && loop_function(new DataCollection(currentLoopIterationResults)));
+      }
+
+      this.onFinish();
+    }
+
+    this.status = TimelineNodeStatus.COMPLETED;
+  }
+
+  private onStart() {
+    if (this.description.on_timeline_start) {
+      this.description.on_timeline_start();
+    }
+  }
+
+  private onFinish() {
+    if (this.description.on_timeline_finish) {
+      this.description.on_timeline_finish();
+    }
+  }
+
+  pause() {
+    if (this.currentChild instanceof Timeline) {
+      this.currentChild.pause();
+    }
+    this.status = TimelineNodeStatus.PAUSED;
+  }
+
+  private resumePromise = new PromiseWrapper();
+  resume() {
+    if (this.status == TimelineNodeStatus.PAUSED) {
+      if (this.currentChild instanceof Timeline) {
+        this.currentChild.resume();
+      }
+      this.status = TimelineNodeStatus.RUNNING;
+      this.resumePromise.resolve();
+    }
+  }
+
+  /**
+   * If the timeline is running or paused, aborts the timeline after the current trial has completed
+   */
+  abort() {
+    if (this.status === TimelineNodeStatus.RUNNING || this.status === TimelineNodeStatus.PAUSED) {
+      if (this.currentChild instanceof Timeline) {
+        this.currentChild.abort();
+      }
+
+      this.shouldAbort = true;
+      if (this.status === TimelineNodeStatus.PAUSED) {
+        this.resume();
+      }
+    }
+  }
+
+  private instantiateChildNodes() {
+    const newChildNodes = this.description.timeline.map((childDescription) => {
+      return isTimelineDescription(childDescription)
+        ? new Timeline(this.dependencies, childDescription, this)
+        : new Trial(this.dependencies, childDescription, this);
+    });
+    this.children.push(...newChildNodes);
+    return newChildNodes;
+  }
+
+  private currentTimelineVariables: Record;
+  private setCurrentTimelineVariablesByIndex(index: number | null) {
+    this.currentTimelineVariables = {
+      ...this.parent?.getAllTimelineVariables(),
+      ...(index === null ? undefined : this.description.timeline_variables[index]),
+    };
+  }
+
+  /**
+   * If the timeline has timeline variables, returns the order of `timeline_variables` array indices
+   * to be used, according to the timeline's `sample` setting. If the timeline has no timeline
+   * variables, returns `[null]`.
+   */
+  private generateTimelineVariableOrder() {
+    const timelineVariableLength = this.description.timeline_variables?.length;
+    if (!timelineVariableLength) {
+      return [null];
+    }
+
+    let order = [...Array(timelineVariableLength).keys()];
+
+    const sample = this.description.sample;
+
+    if (sample) {
+      switch (sample.type) {
+        case "custom":
+          order = sample.fn(order);
+          break;
+
+        case "with-replacement":
+          order = sampleWithReplacement(order, sample.size, sample.weights);
+          break;
+
+        case "without-replacement":
+          order = sampleWithoutReplacement(order, sample.size);
+          break;
+
+        case "fixed-repetitions":
+          order = repeat(order, sample.size);
+          break;
+
+        case "alternate-groups":
+          order = shuffleAlternateGroups(sample.groups, sample.randomize_group_order);
+          break;
+
+        default:
+          throw new Error(
+            `Invalid type "${
+              // @ts-expect-error TS doesn't have a type for `sample` in this case
+              sample.type
+            }" in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"`
+          );
+      }
+    }
+
+    if (this.description.randomize_order) {
+      order = shuffle(order);
+    }
+
+    return order;
+  }
+
+  /**
+   * Returns the current values of all timeline variables, including those from parent timelines
+   */
+  public getAllTimelineVariables() {
+    return this.currentTimelineVariables;
+  }
+
+  public evaluateTimelineVariable(variable: TimelineVariable) {
+    if (this.currentTimelineVariables?.hasOwnProperty(variable.name)) {
+      return this.currentTimelineVariables[variable.name];
+    }
+    throw new Error(`Timeline variable ${variable.name} not found.`);
+  }
+
+  public getResults() {
+    const results: TrialResult[] = [];
+    for (const child of this.children) {
+      if (child instanceof Trial) {
+        const childResult = child.getResult();
+        if (childResult) {
+          results.push(childResult);
+        }
+      } else if (child instanceof Timeline) {
+        results.push(...child.getResults());
+      }
+    }
+
+    return results;
+  }
+
+  /**
+   * Returns the naive progress of the timeline (as a fraction), without considering conditional or
+   * loop functions.
+   */
+  public getNaiveProgress() {
+    if (this.status === TimelineNodeStatus.PENDING) {
+      return 0;
+    }
+
+    const activeNode = this.getLatestNode();
+    if (!activeNode) {
+      return 1;
+    }
+
+    let completedTrials = activeNode.index;
+    if (activeNode.getStatus() === TimelineNodeStatus.COMPLETED) {
+      completedTrials++;
+    }
+
+    return Math.min(completedTrials / this.getNaiveTrialCount(), 1);
+  }
+
+  /**
+   * Recursively computes the naive number of trials in the timeline, without considering
+   * conditional or loop functions.
+   */
+  public getNaiveTrialCount() {
+    // Since child timeline nodes are instantiated lazily, we cannot rely on them but instead have
+    // to recurse the description programmatically.
+
+    const getTrialCount = (description: TimelineArray | TimelineDescription | TrialDescription) => {
+      const getTimelineArrayTrialCount = (description: TimelineArray) =>
+        description
+          .map((childDescription) => getTrialCount(childDescription))
+          .reduce((a, b) => a + b);
+
+      if (Array.isArray(description)) {
+        return getTimelineArrayTrialCount(description);
+      }
+
+      if (isTrialDescription(description)) {
+        return 1;
+      }
+      if (isTimelineDescription(description)) {
+        let conditionCount = description.timeline_variables?.length || 1;
+
+        switch (description.sample?.type) {
+          case "with-replacement":
+          case "without-replacement":
+            conditionCount = description.sample.size;
+            break;
+
+          case "fixed-repetitions":
+            conditionCount *= description.sample.size;
+            break;
+
+          case "alternate-groups":
+            conditionCount = description.sample.groups
+              .map((group) => group.length)
+              .reduce((a, b) => a + b, 0);
+            break;
+        }
+
+        return (
+          getTimelineArrayTrialCount(description.timeline) *
+          (description.repetitions ?? 1) *
+          conditionCount
+        );
+      }
+      return 0;
+    };
+
+    return getTrialCount(this.description);
+  }
+
+  public getLatestNode() {
+    return this.currentChild?.getLatestNode() ?? this;
+  }
+
+  public getActiveTimelineByName(name: string) {
+    if (this.description.name === name) {
+      return this;
+    }
+
+    return this.currentChild?.getActiveTimelineByName(name);
+  }
+}
diff --git a/packages/jspsych/src/timeline/TimelineNode.ts b/packages/jspsych/src/timeline/TimelineNode.ts
new file mode 100644
index 0000000000..ee24e8e94c
--- /dev/null
+++ b/packages/jspsych/src/timeline/TimelineNode.ts
@@ -0,0 +1,174 @@
+import type { Timeline } from "./Timeline";
+import { ParameterObjectPathCache } from "./util";
+import {
+  TimelineArray,
+  TimelineDescription,
+  TimelineNodeDependencies,
+  TimelineNodeStatus,
+  TimelineVariable,
+  TrialDescription,
+  TrialResult,
+} from ".";
+
+export type GetParameterValueOptions = {
+  /**
+   * If true, and the retrieved parameter value is a function, invoke the function and return its
+   * return value (defaults to `true`)
+   */
+  evaluateFunctions?: boolean;
+
+  /**
+   * Whether to fall back to parent timeline node parameters (defaults to `true`)
+   */
+  recursive?: boolean;
+
+  /**
+   * Whether the timeline node should cache the parameter lookup result for successive lookups,
+   * including those of nested properties or array elements (defaults to `true`)
+   */
+  cacheResult?: boolean;
+
+  /**
+   * A function that will be invoked with the original result of the parameter value lookup.
+   * Whatever it returns will subsequently be used instead of the original result. This allows to
+   * modify results before they are cached.
+   */
+  replaceResult?: (originalResult: any) => any;
+};
+
+export abstract class TimelineNode {
+  public abstract readonly description: TimelineDescription | TrialDescription | TimelineArray;
+
+  /**
+   * The globally unique trial index of this node. It is set when the node is run. Timeline nodes
+   * have the same trial index as their first trial.
+   */
+  public index?: number;
+
+  public abstract readonly parent?: Timeline;
+
+  abstract run(): Promise;
+
+  /**
+   * Returns a flat array of all currently available results of this node
+   */
+  abstract getResults(): TrialResult[];
+
+  /**
+   * Recursively evaluates the given timeline variable, starting at the current timeline node.
+   * Returns the result, or `undefined` if the variable is neither specified in the timeline
+   * description of this node, nor in the description of any parent node.
+   */
+  abstract evaluateTimelineVariable(variable: TimelineVariable): any;
+
+  /**
+   * Returns the most recent (child) TimelineNode. For trial nodes, this is always the trial node
+   * itself since trial nodes do not have child nodes. For timeline nodes, the return value is a
+   * Trial object most of the time, but it may also be a Timeline object when a timeline hasn't yet
+   * instantiated its children (e.g. during initial timeline callback functions).
+   */
+  abstract getLatestNode(): TimelineNode;
+
+  /**
+   * Returns an active child timeline (or itself) that matches the given name, or `undefined` if no such child exists.
+   */
+  abstract getActiveTimelineByName(name: string): Timeline | undefined;
+
+  protected status = TimelineNodeStatus.PENDING;
+
+  constructor(protected readonly dependencies: TimelineNodeDependencies) {}
+
+  getStatus() {
+    return this.status;
+  }
+
+  private parameterValueCache = new ParameterObjectPathCache();
+
+  /**
+   * Initializes the parameter value cache with `this.description`. To be called by subclass
+   * constructors after setting `this.description`.
+   */
+  protected initializeParameterValueCache() {
+    this.parameterValueCache.initialize(this.description);
+  }
+
+  /**
+   * Resets all cached parameter values in this timeline node and all of its parents. This is
+   * necessary to re-evaluate function parameters and timeline variables at each new trial.
+   */
+  protected resetParameterValueCache() {
+    this.parameterValueCache.reset();
+    this.parent?.resetParameterValueCache();
+  }
+
+  /**
+   * Retrieves a parameter value from the description of this timeline node, recursively falling
+   * back to the description of each parent timeline node unless `recursive` is set to `false`. If
+   * the parameter...
+   *
+   * * is a timeline variable, evaluates the variable and returns the result.
+   * * is not specified, returns `undefined`.
+   * * is a function and `evaluateFunctions` is not set to `false`, invokes the function and returns
+   *   its return value
+   * * has previously been looked up, return the cached result of the previous lookup
+   *
+   * @param parameterPath The path of the respective parameter in the timeline node description. If
+   * the path is an array, nested object properties or array items will be looked up.
+   * @param options See {@link GetParameterValueOptions}
+   */
+  public getParameterValue(
+    parameterPath: string | string[],
+    options: GetParameterValueOptions = {}
+  ): any {
+    const {
+      evaluateFunctions = true,
+      recursive = true,
+      cacheResult = true,
+      replaceResult,
+    } = options;
+
+    if (typeof parameterPath === "string") {
+      parameterPath = [parameterPath];
+    }
+
+    let { doesPathExist, value: result } = this.parameterValueCache.lookup(parameterPath);
+    if (!doesPathExist && recursive && this.parent) {
+      result = this.parent.getParameterValue(parameterPath, options);
+    }
+
+    if (typeof result === "function" && evaluateFunctions) {
+      result = result();
+    }
+    if (result instanceof TimelineVariable) {
+      result = this.evaluateTimelineVariable(result);
+    }
+
+    if (typeof replaceResult === "function") {
+      result = replaceResult(result);
+    }
+
+    if (cacheResult) {
+      this.parameterValueCache.set(parameterPath, result);
+    }
+
+    return result;
+  }
+
+  /**
+   * Retrieves and evaluates the `data` parameter. It is different from other parameters in that
+   * it's properties may be functions that have to be evaluated, and parent nodes' data parameter
+   * properties are merged into the result.
+   */
+  public getDataParameter(): Record | undefined {
+    const data = this.getParameterValue("data", { recursive: false });
+
+    return {
+      ...Object.fromEntries(
+        typeof data === "object"
+          ? Object.keys(data).map((key) => [key, this.getParameterValue(["data", key])])
+          : []
+      ),
+      ...this.parent?.getDataParameter(),
+    };
+  }
+}
diff --git a/packages/jspsych/src/timeline/Trial.spec.ts b/packages/jspsych/src/timeline/Trial.spec.ts
new file mode 100644
index 0000000000..04fdad9f79
--- /dev/null
+++ b/packages/jspsych/src/timeline/Trial.spec.ts
@@ -0,0 +1,897 @@
+import { flushPromises } from "@jspsych/test-utils";
+import { ConditionalKeys } from "type-fest";
+
+import { TimelineNodeDependenciesMock, createInvocationOrderUtils } from "../../tests/test-utils";
+import TestPlugin from "../../tests/TestPlugin";
+import { JsPsychPlugin, ParameterType } from "../modules/plugins";
+import { Timeline } from "./Timeline";
+import { Trial } from "./Trial";
+import { PromiseWrapper, parameterPathArrayToString } from "./util";
+import {
+  SimulationOptionsParameter,
+  TimelineVariable,
+  TrialDescription,
+  TrialExtensionsConfiguration,
+} from ".";
+
+jest.useFakeTimers();
+
+jest.mock("./Timeline");
+
+describe("Trial", () => {
+  let dependencies: TimelineNodeDependenciesMock;
+  let timeline: Timeline;
+
+  beforeEach(() => {
+    dependencies = new TimelineNodeDependenciesMock();
+    TestPlugin.reset();
+
+    timeline = new Timeline(dependencies, { timeline: [] });
+    timeline.index = 0;
+  });
+
+  const createTrial = (description: TrialDescription) => {
+    const trial = new Trial(dependencies, description, timeline);
+    trial.index = timeline.index;
+    return trial;
+  };
+
+  describe("run()", () => {
+    it("instantiates the corresponding plugin", async () => {
+      const trial = createTrial({ type: TestPlugin });
+
+      await trial.run();
+
+      expect(trial.pluginInstance).toBeInstanceOf(TestPlugin);
+    });
+
+    it("invokes the local `on_start` and the global `onTrialStart` callback", async () => {
+      const onStartCallback = jest.fn();
+      const description = { type: TestPlugin, on_start: onStartCallback };
+      const trial = createTrial(description);
+      await trial.run();
+
+      expect(onStartCallback).toHaveBeenCalledTimes(1);
+      expect(onStartCallback).toHaveBeenCalledWith(description);
+      expect(dependencies.onTrialStart).toHaveBeenCalledTimes(1);
+      expect(dependencies.onTrialStart).toHaveBeenCalledWith(trial);
+    });
+
+    it("properly invokes the plugin's `trial` method", async () => {
+      const trial = createTrial({ type: TestPlugin });
+
+      await trial.run();
+
+      expect(trial.pluginInstance.trial).toHaveBeenCalledTimes(1);
+      expect(trial.pluginInstance.trial).toHaveBeenCalledWith(
+        expect.any(HTMLElement),
+        { type: TestPlugin },
+        expect.any(Function)
+      );
+    });
+
+    it("accepts changes to the trial description made by the `on_start` callback", async () => {
+      const onStartCallback = jest.fn();
+      const description = { type: TestPlugin, on_start: onStartCallback };
+
+      onStartCallback.mockImplementation((trial) => {
+        // We should have a writeable copy here, not the original trial description:
+        expect(trial).not.toBe(description);
+        trial.stimulus = "changed";
+      });
+
+      const trial = createTrial(description);
+      await trial.run();
+
+      expect(trial.pluginInstance.trial).toHaveBeenCalledWith(
+        expect.anything(),
+        expect.objectContaining({ stimulus: "changed" }),
+        expect.anything()
+      );
+    });
+
+    describe("if `trial` returns a promise", () => {
+      it("doesn't automatically invoke the `on_load` callback", async () => {
+        const onLoadCallback = jest.fn();
+        const trial = createTrial({ type: TestPlugin, on_load: onLoadCallback });
+
+        await trial.run();
+
+        // TestPlugin invokes the callback for us in the `trial` method
+        expect(onLoadCallback).toHaveBeenCalledTimes(1);
+      });
+
+      it("picks up the result data from the promise or the `finishTrial()` function (where the latter one takes precedence)", async () => {
+        const trial1 = createTrial({ type: TestPlugin });
+        await trial1.run();
+        expect(trial1.getResult()).toEqual(expect.objectContaining({ my: "result" }));
+
+        TestPlugin.trial = async (display_element, trial, on_load) => {
+          on_load();
+          dependencies.finishTrialPromise.resolve({ finishTrial: "result" });
+          return { my: "result" };
+        };
+
+        const trial2 = createTrial({ type: TestPlugin });
+        await trial2.run();
+        expect(trial2.getResult()).toEqual(expect.objectContaining({ finishTrial: "result" }));
+      });
+    });
+
+    describe("if `trial` returns no promise", () => {
+      beforeAll(() => {
+        TestPlugin.trial = () => {
+          dependencies.finishTrialPromise.resolve({ my: "result" });
+        };
+      });
+
+      it("invokes the local `on_load` callback", async () => {
+        const onLoadCallback = jest.fn();
+        const trial = createTrial({ type: TestPlugin, on_load: onLoadCallback });
+        await trial.run();
+
+        expect(onLoadCallback).toHaveBeenCalledTimes(1);
+      });
+
+      it("picks up the result data from the `finishTrial()` function", async () => {
+        const trial = createTrial({ type: TestPlugin });
+
+        await trial.run();
+        expect(trial.getResult()).toEqual(expect.objectContaining({ my: "result" }));
+      });
+    });
+
+    it("respects the `css_classes` trial parameter", async () => {
+      const displayElement = dependencies.getDisplayElement();
+
+      let trial = createTrial({ type: TestPlugin, css_classes: "class1" });
+      expect(displayElement.classList.value).toEqual("");
+      trial.run();
+      expect(displayElement.classList.value).toEqual("class1");
+      await TestPlugin.finishTrial();
+      expect(displayElement.classList.value).toEqual("");
+
+      trial = createTrial({ type: TestPlugin, css_classes: ["class1", "class2"] });
+      expect(displayElement.classList.value).toEqual("");
+      trial.run();
+      expect(displayElement.classList.value).toEqual("class1 class2");
+      await TestPlugin.finishTrial();
+      expect(displayElement.classList.value).toEqual("");
+    });
+
+    it("invokes the local `on_finish` callback with the result data", async () => {
+      const onFinishCallback = jest.fn();
+      const trial = createTrial({ type: TestPlugin, on_finish: onFinishCallback });
+      await trial.run();
+
+      expect(onFinishCallback).toHaveBeenCalledTimes(1);
+      expect(onFinishCallback).toHaveBeenCalledWith(expect.objectContaining({ my: "result" }));
+    });
+
+    it("awaits async `on_finish` callbacks", async () => {
+      const onFinishCallbackPromise = new PromiseWrapper();
+      const trial = createTrial({
+        type: TestPlugin,
+        on_finish: () => onFinishCallbackPromise.get(),
+      });
+
+      let hasTrialCompleted = false;
+      trial.run().then(() => {
+        hasTrialCompleted = true;
+      });
+
+      await flushPromises();
+      expect(hasTrialCompleted).toBe(false);
+
+      onFinishCallbackPromise.resolve();
+      await flushPromises();
+
+      expect(hasTrialCompleted).toBe(true);
+    });
+
+    it("invokes the global `onTrialResultAvailable` and `onTrialFinished` callbacks", async () => {
+      const invocations: string[] = [];
+      dependencies.onTrialResultAvailable.mockImplementationOnce(() => {
+        invocations.push("onTrialResultAvailable");
+      });
+      dependencies.onTrialFinished.mockImplementationOnce(() => {
+        invocations.push("onTrialFinished");
+      });
+
+      const trial = createTrial({ type: TestPlugin });
+      await trial.run();
+
+      expect(dependencies.onTrialResultAvailable).toHaveBeenCalledTimes(1);
+      expect(dependencies.onTrialResultAvailable).toHaveBeenCalledWith(trial);
+
+      expect(dependencies.onTrialFinished).toHaveBeenCalledTimes(1);
+      expect(dependencies.onTrialFinished).toHaveBeenCalledWith(trial);
+
+      expect(invocations).toEqual(["onTrialResultAvailable", "onTrialFinished"]);
+    });
+
+    it("includes result data from the `data` parameter", async () => {
+      const trial = createTrial({ type: TestPlugin, data: { custom: "value" } });
+      await trial.run();
+      expect(trial.getResult()).toEqual(expect.objectContaining({ my: "result", custom: "value" }));
+    });
+
+    it("includes a set of trial-specific result properties", async () => {
+      const trial = createTrial({ type: TestPlugin });
+      await trial.run();
+      expect(trial.getResult()).toEqual(
+        expect.objectContaining({ trial_type: "test", trial_index: 0 })
+      );
+    });
+
+    it("respects the `save_trial_parameters` parameter", async () => {
+      const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
+
+      TestPlugin.setParameterInfos({
+        stringParameter1: { type: ParameterType.STRING },
+        stringParameter2: { type: ParameterType.STRING },
+        stringParameter3: { type: ParameterType.STRING },
+        stringParameter4: { type: ParameterType.STRING },
+        complexArrayParameter: { type: ParameterType.COMPLEX, array: true },
+        functionParameter: { type: ParameterType.FUNCTION },
+      });
+      TestPlugin.defaultTrialResult = {
+        result: "foo",
+        stringParameter2: "string",
+        stringParameter3: "string",
+      };
+      const trial = createTrial({
+        type: TestPlugin,
+        stringParameter1: "string",
+        stringParameter2: "string",
+        stringParameter3: "string",
+        stringParameter4: "string",
+        functionParameter: jest.fn(),
+        complexArrayParameter: [{ child: "foo" }, () => ({ child: "bar" })],
+
+        save_trial_parameters: {
+          stringParameter3: false,
+          stringParameter4: true,
+          functionParameter: true,
+          complexArrayParameter: true,
+          result: false, // Since `result` is not a parameter, this should be ignored
+        },
+      });
+      await trial.run();
+      const result = trial.getResult();
+
+      // By default, parameters should not be added:
+      expect(result).not.toHaveProperty("stringParameter1");
+
+      // If the plugin adds them, they should not be removed either:
+      expect(result).toHaveProperty("stringParameter2", "string");
+
+      // When explicitly set to false, parameters should be removed if the plugin adds them
+      expect(result).not.toHaveProperty("stringParameter3");
+
+      // When set to true, parameters should be added
+      expect(result).toHaveProperty("stringParameter4", "string");
+
+      // Function parameters should be stringified
+      expect(result).toHaveProperty("functionParameter", jest.fn().toString());
+
+      // Non-parameter data should be left untouched and a warning should be issued
+      expect(result).toHaveProperty("result", "foo");
+      expect(consoleSpy).toHaveBeenCalledTimes(1);
+      expect(consoleSpy).toHaveBeenCalledWith(
+        'Non-existent parameter "result" specified in save_trial_parameters.'
+      );
+      consoleSpy.mockRestore();
+    });
+
+    it("respects the `save_timeline_variables` parameter", async () => {
+      jest.mocked(timeline.getAllTimelineVariables).mockReturnValue({ a: 1, b: 2, c: 3 });
+
+      let trial = createTrial({ type: TestPlugin });
+      await trial.run();
+      expect(trial.getResult().timeline_variables).toBeUndefined();
+
+      trial = createTrial({ type: TestPlugin, save_timeline_variables: true });
+      await trial.run();
+      expect(trial.getResult().timeline_variables).toEqual({ a: 1, b: 2, c: 3 });
+
+      trial = createTrial({ type: TestPlugin, save_timeline_variables: ["a", "d"] });
+      await trial.run();
+      expect(trial.getResult().timeline_variables).toEqual({ a: 1 });
+    });
+
+    describe("with a plugin parameter specification", () => {
+      const functionDefaultValue = () => {};
+      beforeEach(() => {
+        TestPlugin.setParameterInfos({
+          string: { type: ParameterType.STRING, default: null },
+          requiredString: { type: ParameterType.STRING },
+          stringArray: { type: ParameterType.STRING, default: [], array: true },
+          function: { type: ParameterType.FUNCTION, default: functionDefaultValue },
+          complex: {
+            type: ParameterType.COMPLEX,
+            default: { requiredChild: "default" },
+            nested: {
+              requiredChild: { type: ParameterType.STRING },
+            },
+          },
+          requiredComplexNested: {
+            type: ParameterType.COMPLEX,
+            nested: {
+              child: { type: ParameterType.STRING, default: "I'm nested." },
+              requiredChild: { type: ParameterType.STRING },
+            },
+          },
+          requiredComplexNestedArray: {
+            type: ParameterType.COMPLEX,
+            array: true,
+            nested: {
+              child: { type: ParameterType.STRING, default: "I'm nested." },
+              requiredChild: { type: ParameterType.STRING },
+            },
+          },
+        });
+      });
+
+      it("resolves missing parameter values from parent timeline and sets default values", async () => {
+        jest.mocked(timeline).getParameterValue.mockImplementation((parameterPath) => {
+          if (Array.isArray(parameterPath)) {
+            parameterPath = parameterPathArrayToString(parameterPath);
+          }
+
+          if (parameterPath === "requiredString") {
+            return "foo";
+          }
+          if (parameterPath === "requiredComplexNestedArray[0].requiredChild") {
+            return "foo";
+          }
+          return undefined;
+        });
+        const trial = createTrial({
+          type: TestPlugin,
+          requiredComplexNested: { requiredChild: "bar" },
+          requiredComplexNestedArray: [
+            // This empty object is allowed because `requiredComplexNestedArray[0]` is (simulated to
+            // be) set as a parent timeline parameter:
+            {},
+            { requiredChild: "bar" },
+          ],
+        });
+
+        await trial.run();
+
+        // `requiredString` should have been resolved from the parent timeline
+        expect(trial.pluginInstance.trial).toHaveBeenCalledWith(
+          expect.anything(),
+          {
+            type: TestPlugin,
+            string: null,
+            requiredString: "foo",
+            stringArray: [],
+            function: functionDefaultValue,
+            complex: { requiredChild: "default" },
+            requiredComplexNested: { child: "I'm nested.", requiredChild: "bar" },
+            requiredComplexNestedArray: [
+              { child: "I'm nested.", requiredChild: "foo" },
+              { child: "I'm nested.", requiredChild: "bar" },
+            ],
+          },
+          expect.anything()
+        );
+      });
+
+      it("errors when an `array` parameter is not an array", async () => {
+        TestPlugin.setParameterInfos({
+          stringArray: { type: ParameterType.STRING, array: true },
+        });
+
+        // This should work:
+        await createTrial({ type: TestPlugin, stringArray: [] }).run();
+
+        // This shouldn't:
+        await expect(
+          createTrial({ type: TestPlugin, stringArray: {} }).run()
+        ).rejects.toThrowErrorMatchingInlineSnapshot(
+          '"A non-array value (`[object Object]`) was provided for the array parameter "stringArray" in the "test" plugin. Please make sure that "stringArray" is an array."'
+        );
+        await expect(
+          createTrial({ type: TestPlugin, stringArray: 1 }).run()
+        ).rejects.toThrowErrorMatchingInlineSnapshot(
+          '"A non-array value (`1`) was provided for the array parameter "stringArray" in the "test" plugin. Please make sure that "stringArray" is an array."'
+        );
+      });
+
+      it("evaluates parameter functions", async () => {
+        const functionParameter = () => "invalid";
+        const trial = createTrial({
+          type: TestPlugin,
+          function: functionParameter,
+          requiredString: () => "foo",
+          requiredComplexNested: () => ({
+            requiredChild: () => "bar",
+          }),
+          requiredComplexNestedArray: () => [() => ({ requiredChild: () => "bar" })],
+        });
+
+        await trial.run();
+
+        expect(trial.pluginInstance.trial).toHaveBeenCalledWith(
+          expect.anything(),
+          expect.objectContaining({
+            function: functionParameter,
+            requiredString: "foo",
+            requiredComplexNested: expect.objectContaining({ requiredChild: "bar" }),
+            requiredComplexNestedArray: [expect.objectContaining({ requiredChild: "bar" })],
+          }),
+          expect.anything()
+        );
+      });
+
+      it("evaluates timeline variables, including those returned from parameter functions", async () => {
+        jest
+          .mocked(timeline)
+          .evaluateTimelineVariable.mockImplementation((variable: TimelineVariable) => {
+            switch (variable.name) {
+              case "t":
+                return TestPlugin;
+              case "x":
+                return "foo";
+              default:
+                return undefined;
+            }
+          });
+
+        const trial = createTrial({
+          type: new TimelineVariable("t"),
+          requiredString: new TimelineVariable("x"),
+          requiredComplexNested: { requiredChild: () => new TimelineVariable("x") },
+          requiredComplexNestedArray: [{ requiredChild: () => new TimelineVariable("x") }],
+        });
+
+        await trial.run();
+
+        // The `x` timeline variables should have been replaced with `foo`
+        expect(trial.pluginInstance.trial).toHaveBeenCalledWith(
+          expect.anything(),
+          expect.objectContaining({
+            requiredString: "foo",
+            requiredComplexNested: expect.objectContaining({ requiredChild: "foo" }),
+            requiredComplexNestedArray: [expect.objectContaining({ requiredChild: "foo" })],
+          }),
+          expect.anything()
+        );
+      });
+
+      it("allows null values for parameters with a non-null default value", async () => {
+        TestPlugin.setParameterInfos({
+          allowedNullString: { type: ParameterType.STRING, default: "foo" },
+        });
+
+        const trial = createTrial({ type: TestPlugin, allowedNullString: null });
+        await trial.run();
+
+        expect(trial.pluginInstance.trial).toHaveBeenCalledWith(
+          expect.anything(),
+          expect.objectContaining({ allowedNullString: null }),
+          expect.anything()
+        );
+      });
+
+      describe("with missing required parameters", () => {
+        it("errors on missing simple parameters", async () => {
+          TestPlugin.setParameterInfos({ requiredString: { type: ParameterType.STRING } });
+
+          // This should work:
+          await createTrial({ type: TestPlugin, requiredString: "foo" }).run();
+
+          // This shouldn't:
+          await expect(createTrial({ type: TestPlugin }).run()).rejects.toThrow(
+            '"requiredString" parameter'
+          );
+        });
+
+        it("errors on missing parameters nested in `COMPLEX` parameters", async () => {
+          TestPlugin.setParameterInfos({
+            requiredComplexNested: {
+              type: ParameterType.COMPLEX,
+              nested: { requiredChild: { type: ParameterType.STRING } },
+            },
+          });
+
+          // This should work:
+          await createTrial({
+            type: TestPlugin,
+            requiredComplexNested: { requiredChild: "bar" },
+          }).run();
+
+          // This shouldn't:
+          await expect(createTrial({ type: TestPlugin }).run()).rejects.toThrow(
+            '"requiredComplexNested" parameter'
+          );
+          await expect(
+            createTrial({ type: TestPlugin, requiredComplexNested: {} }).run()
+          ).rejects.toThrowError('"requiredComplexNested.requiredChild" parameter');
+        });
+
+        it("errors on missing parameters nested in `COMPLEX` array parameters", async () => {
+          TestPlugin.setParameterInfos({
+            requiredComplexNestedArray: {
+              type: ParameterType.COMPLEX,
+              array: true,
+              nested: { requiredChild: { type: ParameterType.STRING } },
+            },
+          });
+
+          // This should work:
+          await createTrial({ type: TestPlugin, requiredComplexNestedArray: [] }).run();
+          await createTrial({
+            type: TestPlugin,
+            requiredComplexNestedArray: [{ requiredChild: "bar" }],
+          }).run();
+
+          // This shouldn't:
+          await expect(createTrial({ type: TestPlugin }).run()).rejects.toThrow(
+            '"requiredComplexNestedArray" parameter'
+          );
+          await expect(
+            createTrial({ type: TestPlugin, requiredComplexNestedArray: [{}] }).run()
+          ).rejects.toThrow('"requiredComplexNestedArray[0].requiredChild" parameter');
+          await expect(
+            createTrial({
+              type: TestPlugin,
+              requiredComplexNestedArray: [{ requiredChild: "bar" }, {}],
+            }).run()
+          ).rejects.toThrow('"requiredComplexNestedArray[1].requiredChild" parameter');
+        });
+      });
+    });
+
+    it("respects `default_iti` and `post_trial_gap``", async () => {
+      dependencies.getDefaultIti.mockReturnValue(100);
+      TestPlugin.setManualFinishTrialMode();
+
+      const trial1 = createTrial({ type: TestPlugin });
+
+      let hasTrial1Completed = false;
+      trial1.run().then(() => {
+        hasTrial1Completed = true;
+      });
+
+      await TestPlugin.finishTrial();
+      expect(hasTrial1Completed).toBe(false);
+
+      jest.advanceTimersByTime(100);
+      await flushPromises();
+      expect(hasTrial1Completed).toBe(true);
+
+      const trial2 = createTrial({ type: TestPlugin, post_trial_gap: () => 200 });
+
+      let hasTrial2Completed = false;
+      trial2.run().then(() => {
+        hasTrial2Completed = true;
+      });
+
+      await TestPlugin.finishTrial();
+      expect(hasTrial2Completed).toBe(false);
+
+      jest.advanceTimersByTime(100);
+      await flushPromises();
+      expect(hasTrial2Completed).toBe(false);
+
+      jest.advanceTimersByTime(100);
+      await flushPromises();
+      expect(hasTrial2Completed).toBe(true);
+    });
+
+    it("skips inter-trial interval in data-only simulation mode", async () => {
+      dependencies.getSimulationMode.mockReturnValue("data-only");
+      TestPlugin.setManualFinishTrialMode();
+
+      const trial = createTrial({ type: TestPlugin, post_trial_gap: 100 });
+
+      let hasTrialCompleted = false;
+      trial.run().then(() => {
+        hasTrialCompleted = true;
+      });
+
+      await TestPlugin.finishTrial();
+      expect(hasTrialCompleted).toBe(true);
+    });
+
+    it("invokes extension callbacks and includes extension results", async () => {
+      dependencies.runOnFinishExtensionCallbacks.mockResolvedValue({ extension: "result" });
+
+      const extensionsConfig: TrialExtensionsConfiguration = [
+        { type: jest.fn(), params: { my: "option" } },
+      ];
+
+      const trial = createTrial({
+        type: TestPlugin,
+        extensions: extensionsConfig,
+      });
+      await trial.run();
+
+      expect(dependencies.runOnStartExtensionCallbacks).toHaveBeenCalledTimes(1);
+      expect(dependencies.runOnStartExtensionCallbacks).toHaveBeenCalledWith(extensionsConfig);
+
+      expect(dependencies.runOnLoadExtensionCallbacks).toHaveBeenCalledTimes(1);
+      expect(dependencies.runOnLoadExtensionCallbacks).toHaveBeenCalledWith(extensionsConfig);
+
+      expect(dependencies.runOnFinishExtensionCallbacks).toHaveBeenCalledTimes(1);
+      expect(dependencies.runOnFinishExtensionCallbacks).toHaveBeenCalledWith(extensionsConfig);
+      expect(trial.getResult()).toEqual(expect.objectContaining({ extension: "result" }));
+    });
+
+    it("invokes all callbacks in a proper order", async () => {
+      const { createInvocationOrderCallback, invocations } = createInvocationOrderUtils();
+
+      const dependencyCallbacks: Array> = [
+        "onTrialStart",
+        "onTrialResultAvailable",
+        "onTrialFinished",
+        "runOnStartExtensionCallbacks",
+        "runOnLoadExtensionCallbacks",
+        "runOnFinishExtensionCallbacks",
+      ];
+
+      for (const callbackName of dependencyCallbacks) {
+        (dependencies[callbackName] as jest.Mock).mockImplementation(
+          createInvocationOrderCallback(callbackName)
+        );
+      }
+
+      const trial = createTrial({
+        type: TestPlugin,
+        extensions: [{ type: jest.fn(), params: { my: "option" } }],
+        on_start: createInvocationOrderCallback("on_start"),
+        on_load: createInvocationOrderCallback("on_load"),
+        on_finish: createInvocationOrderCallback("on_finish"),
+      });
+
+      await trial.run();
+
+      expect(invocations).toEqual([
+        "onTrialStart",
+        "on_start",
+        "runOnStartExtensionCallbacks",
+
+        "on_load",
+        "runOnLoadExtensionCallbacks",
+
+        "onTrialResultAvailable",
+
+        "runOnFinishExtensionCallbacks",
+        "on_finish",
+        "onTrialFinished",
+      ]);
+    });
+
+    describe("in simulation mode", () => {
+      beforeEach(() => {
+        dependencies.getSimulationMode.mockReturnValue("data-only");
+      });
+
+      it("invokes the plugin's `simulate` method instead of `trial`", async () => {
+        const trial = createTrial({ type: TestPlugin });
+        await trial.run();
+
+        expect(trial.pluginInstance.trial).not.toHaveBeenCalled();
+
+        expect(trial.pluginInstance.simulate).toHaveBeenCalledTimes(1);
+        expect(trial.pluginInstance.simulate).toHaveBeenCalledWith(
+          { type: TestPlugin },
+          "data-only",
+          {},
+          expect.any(Function)
+        );
+      });
+
+      it("doesn't invoke `on_load`, even when `simulate` doesn't return a promise", async () => {
+        TestPlugin.simulate = () => {
+          dependencies.finishTrialPromise.resolve({});
+        };
+
+        const onLoad = jest.fn();
+        await createTrial({ type: TestPlugin, on_load: onLoad }).run();
+
+        expect(onLoad).not.toHaveBeenCalled();
+      });
+
+      it("invokes the plugin's `trial` method if the plugin has no `simulate` method", async () => {
+        const trial = createTrial({
+          type: class implements JsPsychPlugin {
+            static info = { name: "test", parameters: {} };
+            trial = jest.fn(async () => ({}));
+          },
+        });
+        await trial.run();
+
+        expect(trial.pluginInstance.trial).toHaveBeenCalled();
+      });
+
+      it("invokes the plugin's `trial` method if `simulate` is `false` in the trial's simulation options", async () => {
+        const trial = createTrial({ type: TestPlugin, simulation_options: { simulate: false } });
+        await trial.run();
+
+        expect(trial.pluginInstance.trial).toHaveBeenCalled();
+        expect(trial.pluginInstance.simulate).not.toHaveBeenCalled();
+      });
+
+      it("respects the `mode` parameter from the trial's simulation options", async () => {
+        const trial = createTrial({ type: TestPlugin, simulation_options: { mode: "visual" } });
+        await trial.run();
+
+        expect(jest.mocked(trial.pluginInstance.simulate).mock.calls[0][1]).toBe("visual");
+      });
+    });
+  });
+
+  describe("getResult[s]()", () => {
+    it("returns the result once it is available", async () => {
+      TestPlugin.setManualFinishTrialMode();
+      const trial = createTrial({ type: TestPlugin });
+      trial.run();
+
+      expect(trial.getResult()).toBeUndefined();
+      expect(trial.getResults()).toEqual([]);
+
+      await TestPlugin.finishTrial();
+
+      expect(trial.getResult()).toEqual(expect.objectContaining({ my: "result" }));
+      expect(trial.getResults()).toEqual([expect.objectContaining({ my: "result" })]);
+    });
+
+    it("does not return the result when the `record_data` trial parameter is `false`", async () => {
+      TestPlugin.setManualFinishTrialMode();
+      const trial = createTrial({ type: TestPlugin, record_data: false });
+      trial.run();
+
+      expect(trial.getResult()).toBeUndefined();
+      expect(trial.getResults()).toEqual([]);
+
+      await TestPlugin.finishTrial();
+
+      expect(trial.getResult()).toBeUndefined();
+      expect(trial.getResults()).toEqual([]);
+    });
+  });
+
+  describe("evaluateTimelineVariable()", () => {
+    it("defers to the parent node", () => {
+      jest.mocked(timeline).evaluateTimelineVariable.mockReturnValue(1);
+
+      const trial = new Trial(dependencies, { type: TestPlugin }, timeline);
+
+      const variable = new TimelineVariable("x");
+      expect(trial.evaluateTimelineVariable(variable)).toEqual(1);
+      expect(timeline.evaluateTimelineVariable).toHaveBeenCalledWith(variable);
+    });
+  });
+
+  describe("getParameterValue()", () => {
+    it("disables recursive lookups of timeline description keys", async () => {
+      const trial = createTrial({ type: TestPlugin });
+
+      for (const parameter of [
+        "timeline",
+        "timeline_variables",
+        "repetitions",
+        "loop_function",
+        "conditional_function",
+        "randomize_order",
+        "sample",
+        "on_timeline_start",
+        "on_timeline_finish",
+      ]) {
+        expect(trial.getParameterValue(parameter)).toBeUndefined();
+        expect(timeline.getParameterValue).not.toHaveBeenCalled();
+      }
+    });
+  });
+
+  describe("getSimulationOptions()", () => {
+    const createSimulationTrial = (simulationOptions?: SimulationOptionsParameter | string) =>
+      createTrial({
+        type: TestPlugin,
+        simulation_options: simulationOptions,
+      });
+
+    it("merges in global default simulation options", async () => {
+      dependencies.getGlobalSimulationOptions.mockReturnValue({
+        default: { data: { rt: 0, custom: "default" } },
+        foo: { data: { custom: "foo" } },
+      });
+
+      expect(createSimulationTrial({ data: { rt: 1 } }).getSimulationOptions()).toEqual({
+        data: { rt: 1, custom: "default" },
+      });
+
+      expect(createSimulationTrial("foo").getSimulationOptions()).toEqual({
+        data: { rt: 0, custom: "foo" },
+      });
+    });
+
+    describe("if no trial-level simulation options are set", () => {
+      it("falls back to parent timeline simulation options", async () => {
+        jest
+          .mocked(timeline.getParameterValue)
+          .mockImplementation((parameterPath) =>
+            parameterPath.toString() === "simulation_options" ? { data: { rt: 1 } } : undefined
+          );
+
+        expect(createTrial({ type: TestPlugin }).getSimulationOptions()).toEqual({
+          data: { rt: 1 },
+        });
+      });
+
+      it("falls back to global default simulation options ", async () => {
+        expect(createTrial({ type: TestPlugin }).getSimulationOptions()).toEqual({});
+
+        dependencies.getGlobalSimulationOptions.mockReturnValue({ default: { data: { rt: 1 } } });
+        expect(createTrial({ type: TestPlugin }).getSimulationOptions()).toEqual({
+          data: { rt: 1 },
+        });
+      });
+    });
+
+    describe("when trial-level simulation options are a string", () => {
+      beforeEach(() => {
+        dependencies.getGlobalSimulationOptions.mockReturnValue({
+          default: { data: { rt: 1 } },
+          custom: { data: { rt: 2 } },
+        });
+      });
+
+      it("looks up the corresponding global simulation options key", async () => {
+        expect(createSimulationTrial("custom").getSimulationOptions()).toEqual({ data: { rt: 2 } });
+      });
+
+      it("falls back to the global default simulation options ", async () => {
+        expect(createSimulationTrial("nonexistent").getSimulationOptions()).toEqual({
+          data: { rt: 1 },
+        });
+      });
+    });
+
+    describe("when `simulation_options` is a function that returns a string", () => {
+      it("looks up the corresponding global simulation options key", async () => {
+        jest.mocked(dependencies.getGlobalSimulationOptions).mockReturnValue({
+          foo: { data: { rt: 1 } },
+        });
+
+        expect(
+          createTrial({ type: TestPlugin, simulation_options: () => "foo" }).getSimulationOptions()
+        ).toEqual({
+          data: { rt: 1 },
+        });
+      });
+    });
+
+    it("evaluates (nested) functions and timeline variables", async () => {
+      const timelineVariables = { x: "foo" };
+      jest.mocked(dependencies.getGlobalSimulationOptions).mockReturnValue({
+        foo: { data: { rt: 0 } },
+      });
+      jest
+        .mocked(timeline.evaluateTimelineVariable)
+        .mockImplementation((variable) => timelineVariables[variable.name]);
+
+      expect(createSimulationTrial(() => new TimelineVariable("x")).getSimulationOptions()).toEqual(
+        { data: { rt: 0 } }
+      );
+
+      expect(
+        createSimulationTrial(() => ({
+          data: () => ({ rt: () => 1 }),
+          simulate: () => true,
+          mode: () => "visual",
+        })).getSimulationOptions()
+      ).toEqual({ data: { rt: 1 }, simulate: true, mode: "visual" });
+
+      jest.mocked(timeline.evaluateTimelineVariable).mockReturnValue({ data: { rt: 2 } });
+      expect(createSimulationTrial(new TimelineVariable("x")).getSimulationOptions()).toEqual({
+        data: { rt: 2 },
+      });
+    });
+  });
+});
diff --git a/packages/jspsych/src/timeline/Trial.ts b/packages/jspsych/src/timeline/Trial.ts
new file mode 100644
index 0000000000..bfc300ff47
--- /dev/null
+++ b/packages/jspsych/src/timeline/Trial.ts
@@ -0,0 +1,419 @@
+import { Class } from "type-fest";
+
+import { ParameterInfos } from "../modules/plugins";
+import { JsPsychPlugin, ParameterType, PluginInfo } from "../modules/plugins";
+import { deepCopy, deepMerge } from "../modules/utils";
+import { Timeline } from "./Timeline";
+import { GetParameterValueOptions, TimelineNode } from "./TimelineNode";
+import { delay, isPromise, parameterPathArrayToString } from "./util";
+import {
+  SimulationOptions,
+  TimelineNodeDependencies,
+  TimelineNodeStatus,
+  TimelineVariable,
+  TrialDescription,
+  TrialResult,
+  timelineDescriptionKeys,
+} from ".";
+
+export class Trial extends TimelineNode {
+  public readonly pluginClass: Class>;
+  public pluginInstance: JsPsychPlugin;
+  public trialObject?: TrialDescription;
+  public index?: number;
+
+  private result: TrialResult;
+  private readonly pluginInfo: PluginInfo;
+
+  constructor(
+    dependencies: TimelineNodeDependencies,
+    public readonly description: TrialDescription,
+    public readonly parent: Timeline
+  ) {
+    super(dependencies);
+    this.initializeParameterValueCache();
+
+    this.trialObject = deepCopy(description);
+    this.pluginClass = this.getParameterValue("type", { evaluateFunctions: false });
+    this.pluginInfo = this.pluginClass["info"];
+
+    if (!("version" in this.pluginInfo) && !("data" in this.pluginInfo)) {
+      console.warn(
+        this.pluginInfo["name"],
+        "is missing the 'version' and 'data' fields. Please update plugin as 'version' and 'data' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
+      );
+    } else if (!("version" in this.pluginInfo)) {
+      console.warn(
+        this.pluginInfo["name"],
+        "is missing the 'version' field. Please update plugin as 'version' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
+      );
+    } else if (!("data" in this.pluginInfo)) {
+      console.warn(
+        this.pluginInfo["name"],
+        "is missing the 'data' field. Please update plugin as 'data' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
+      );
+    }
+  }
+
+  public async run() {
+    this.status = TimelineNodeStatus.RUNNING;
+    this.processParameters();
+
+    this.onStart();
+    this.addCssClasses();
+
+    this.pluginInstance = this.dependencies.instantiatePlugin(this.pluginClass);
+
+    this.result = this.processResult(await this.executeTrial());
+
+    this.dependencies.onTrialResultAvailable(this);
+
+    this.status = TimelineNodeStatus.COMPLETED;
+
+    await this.onFinish();
+    this.removeCssClasses();
+
+    const gap = this.getParameterValue("post_trial_gap") ?? this.dependencies.getDefaultIti();
+    if (gap !== 0 && this.dependencies.getSimulationMode() !== "data-only") {
+      await delay(gap);
+    }
+
+    this.resetParameterValueCache();
+  }
+
+  private async executeTrial() {
+    const trialPromise = this.dependencies.finishTrialPromise.get();
+
+    /** Used as a way to figure out if `finishTrial()` has ben called without awaiting `trialPromise` */
+    let hasTrialPromiseBeenResolved = false;
+    trialPromise.then(() => {
+      hasTrialPromiseBeenResolved = true;
+    });
+
+    const { trialReturnValue, hasTrialBeenSimulated } = this.invokeTrialMethod();
+
+    // Wait until the trial has completed and grab result data
+    let result: TrialResult | void;
+    if (isPromise(trialReturnValue)) {
+      result = await Promise.race([trialReturnValue, trialPromise]);
+
+      // If `finishTrial()` was called, use the result provided to it. This may happen although
+      // `trialReturnValue` won the race ("run-to-completion").
+      if (hasTrialPromiseBeenResolved) {
+        result = await trialPromise;
+      }
+    } else {
+      // The `simulate` method always invokes `onLoad()`, so we don't call `onLoad()` when the trial
+      // has been simulated
+      if (!hasTrialBeenSimulated) {
+        this.onLoad();
+      }
+
+      result = await trialPromise;
+    }
+
+    // The trial has finished, time to clean up.
+    this.cleanupTrial();
+
+    return result;
+  }
+
+  private invokeTrialMethod(): {
+    trialReturnValue: void | Promise;
+    hasTrialBeenSimulated: boolean;
+  } {
+    const globalSimulationMode = this.dependencies.getSimulationMode();
+
+    if (globalSimulationMode && typeof this.pluginInstance.simulate === "function") {
+      const simulationOptions = this.getSimulationOptions();
+
+      if (simulationOptions.simulate !== false) {
+        return {
+          hasTrialBeenSimulated: true,
+          trialReturnValue: this.pluginInstance.simulate(
+            this.trialObject,
+            simulationOptions.mode ?? globalSimulationMode,
+            simulationOptions,
+            this.onLoad
+          ),
+        };
+      }
+    }
+
+    return {
+      hasTrialBeenSimulated: false,
+      trialReturnValue: this.pluginInstance.trial(
+        this.dependencies.getDisplayElement(),
+        this.trialObject,
+        this.onLoad
+      ),
+    };
+  }
+
+  /**
+   * Cleanup the trial by removing the display element and removing event listeners
+   */
+  private cleanupTrial() {
+    this.dependencies.clearAllTimeouts();
+    this.dependencies.getDisplayElement().innerHTML = "";
+  }
+
+  /**
+   * Add the CSS classes from the `css_classes` parameter to the display element
+   */
+  private addCssClasses() {
+    const classes: string | string[] = this.getParameterValue("css_classes");
+    const classList = this.dependencies.getDisplayElement().classList;
+    if (typeof classes === "string") {
+      classList.add(classes);
+    } else if (Array.isArray(classes)) {
+      classList.add(...classes);
+    }
+  }
+
+  /**
+   * Removes the provided css classes from the display element
+   */
+  private removeCssClasses() {
+    const classes = this.getParameterValue("css_classes");
+    if (classes) {
+      this.dependencies
+        .getDisplayElement()
+        .classList.remove(...(typeof classes === "string" ? [classes] : classes));
+    }
+  }
+
+  private processResult(result: TrialResult | void) {
+    if (!result) {
+      result = {};
+    }
+
+    for (const [parameterName, shouldParameterBeIncluded] of Object.entries(
+      this.getParameterValue("save_trial_parameters") ?? {}
+    )) {
+      if (this.pluginInfo.parameters[parameterName]) {
+        if (shouldParameterBeIncluded && !Object.hasOwn(result, parameterName)) {
+          let parameterValue = this.trialObject[parameterName];
+          if (typeof parameterValue === "function") {
+            parameterValue = parameterValue.toString();
+          }
+          result[parameterName] = parameterValue;
+        } else if (!shouldParameterBeIncluded && Object.hasOwn(result, parameterName)) {
+          delete result[parameterName];
+        }
+      } else {
+        console.warn(
+          `Non-existent parameter "${parameterName}" specified in save_trial_parameters.`
+        );
+      }
+    }
+
+    result = {
+      ...this.getDataParameter(),
+      ...result,
+      trial_type: this.pluginInfo.name,
+      trial_index: this.index,
+      plugin_version: this.pluginInfo["version"] ? this.pluginInfo["version"] : null,
+    };
+
+    // Add timeline variables to the result according to the `save_timeline_variables` parameter
+    const saveTimelineVariables = this.getParameterValue("save_timeline_variables");
+    if (saveTimelineVariables === true) {
+      result.timeline_variables = { ...this.parent.getAllTimelineVariables() };
+    } else if (Array.isArray(saveTimelineVariables)) {
+      result.timeline_variables = Object.fromEntries(
+        Object.entries(this.parent.getAllTimelineVariables()).filter(([key, _]) =>
+          saveTimelineVariables.includes(key)
+        )
+      );
+    }
+
+    return result;
+  }
+
+  /**
+   * Runs a callback function retrieved from a parameter value and returns its result.
+   *
+   * @param parameterName The name of the parameter to retrieve the callback function from.
+   * @param callbackParameters The parameters (if any) to be passed to the callback function
+   */
+  private runParameterCallback(parameterName: string, ...callbackParameters: unknown[]) {
+    const callback = this.getParameterValue(parameterName, { evaluateFunctions: false });
+    if (callback) {
+      return callback(...callbackParameters);
+    }
+  }
+
+  private onStart() {
+    this.dependencies.onTrialStart(this);
+    this.runParameterCallback("on_start", this.trialObject);
+    this.dependencies.runOnStartExtensionCallbacks(this.getParameterValue("extensions"));
+  }
+
+  private onLoad = () => {
+    this.runParameterCallback("on_load");
+    this.dependencies.runOnLoadExtensionCallbacks(this.getParameterValue("extensions"));
+  };
+
+  private async onFinish() {
+    const extensionResults = await this.dependencies.runOnFinishExtensionCallbacks(
+      this.getParameterValue("extensions")
+    );
+    Object.assign(this.result, extensionResults);
+
+    await Promise.resolve(this.runParameterCallback("on_finish", this.getResult()));
+
+    this.dependencies.onTrialFinished(this);
+  }
+
+  public evaluateTimelineVariable(variable: TimelineVariable) {
+    // Timeline variable values are specified at the timeline level, not at the trial level, hence
+    // deferring to the parent timeline here
+    return this.parent?.evaluateTimelineVariable(variable);
+  }
+
+  public getParameterValue(
+    parameterPath: string | string[],
+    options: GetParameterValueOptions = {}
+  ) {
+    // Disable recursion for timeline description keys
+    if (
+      timelineDescriptionKeys.includes(
+        typeof parameterPath === "string" ? parameterPath : parameterPath[0]
+      )
+    ) {
+      options.recursive = false;
+    }
+    return super.getParameterValue(parameterPath, options);
+  }
+
+  /**
+   * Retrieves and evaluates the `simulation_options` parameter, considering nested properties and
+   * global simulation options.
+   */
+  public getSimulationOptions() {
+    const simulationOptions: SimulationOptions = this.getParameterValue("simulation_options", {
+      replaceResult: (result = {}) => {
+        if (typeof result === "string") {
+          // Look up the global simulation options by their key
+          const globalSimulationOptions = this.dependencies.getGlobalSimulationOptions();
+          result = globalSimulationOptions[result] ?? globalSimulationOptions["default"] ?? {};
+        }
+
+        return deepMerge(
+          deepCopy(this.dependencies.getGlobalSimulationOptions().default),
+          deepCopy(result)
+        );
+      },
+    });
+
+    if (typeof simulationOptions === "undefined") {
+      return {};
+    }
+
+    simulationOptions.mode = this.getParameterValue(["simulation_options", "mode"]);
+    simulationOptions.simulate = this.getParameterValue(["simulation_options", "simulate"]);
+    simulationOptions.data = this.getParameterValue(["simulation_options", "data"]);
+
+    if (typeof simulationOptions.data === "object") {
+      simulationOptions.data = Object.fromEntries(
+        Object.keys(simulationOptions.data).map((key) => [
+          key,
+          this.getParameterValue(["simulation_options", "data", key]),
+        ])
+      );
+    }
+
+    return simulationOptions;
+  }
+
+  /**
+   * Returns the result object of this trial or `undefined` if the result is not yet known or the
+   * `record_data` trial parameter is `false`.
+   */
+  public getResult() {
+    return this.getParameterValue("record_data") === false ? undefined : this.result;
+  }
+
+  public getResults() {
+    const result = this.getResult();
+    return result ? [result] : [];
+  }
+
+  /**
+   * Checks that the parameters provided in the trial description align with the plugin's info
+   * object, resolves missing parameter values from the parent timeline, resolves timeline variable
+   * parameters, evaluates parameter functions if the expected parameter type is not `FUNCTION`, and
+   * sets default values for optional parameters.
+   */
+  private processParameters() {
+    const assignParameterValues = (
+      parameterObject: Record,
+      parameterInfos: ParameterInfos,
+      parentParameterPath: string[] = []
+    ) => {
+      for (const [parameterName, parameterConfig] of Object.entries(parameterInfos)) {
+        const parameterPath = [...parentParameterPath, parameterName];
+
+        let parameterValue = this.getParameterValue(parameterPath, {
+          evaluateFunctions: parameterConfig.type !== ParameterType.FUNCTION,
+          replaceResult: (originalResult) => {
+            if (typeof originalResult === "undefined") {
+              if (typeof parameterConfig.default === "undefined") {
+                throw new Error(
+                  `You must specify a value for the "${parameterPathArrayToString(
+                    parameterPath
+                  )}" parameter in the "${this.pluginInfo.name}" plugin.`
+                );
+              } else {
+                return parameterConfig.default;
+              }
+            } else {
+              return originalResult;
+            }
+          },
+        });
+
+        if (parameterConfig.array && !Array.isArray(parameterValue)) {
+          const parameterPathString = parameterPathArrayToString(parameterPath);
+          throw new Error(
+            `A non-array value (\`${parameterValue}\`) was provided for the array parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin. Please make sure that "${parameterPathString}" is an array.`
+          );
+        }
+
+        if (parameterConfig.type === ParameterType.COMPLEX && parameterConfig.nested) {
+          // Assign parameter values according to the `nested` schema
+          if (parameterConfig.array) {
+            // ...for each nested array element
+            parameterValue = parameterValue.map((_, arrayIndex) => {
+              const arrayElementPath = [...parameterPath, arrayIndex.toString()];
+              const arrayElementValue = this.getParameterValue(arrayElementPath);
+              assignParameterValues(arrayElementValue, parameterConfig.nested, arrayElementPath);
+              return arrayElementValue;
+            });
+          } else {
+            // ...for the nested object
+            assignParameterValues(parameterValue, parameterConfig.nested, parameterPath);
+          }
+        }
+
+        parameterObject[parameterName] = parameterValue;
+      }
+    };
+
+    const trialObject = deepCopy(this.description);
+    assignParameterValues(trialObject, this.pluginInfo.parameters);
+    this.trialObject = trialObject;
+  }
+
+  public getLatestNode() {
+    return this;
+  }
+
+  public getActiveTimelineByName(name: string): Timeline | undefined {
+    // This returns undefined because the function is looking
+    // for a timeline. If we get to this point, then none
+    // of the parent nodes match the name.
+    return undefined;
+  }
+}
diff --git a/packages/jspsych/src/timeline/index.ts b/packages/jspsych/src/timeline/index.ts
new file mode 100644
index 0000000000..f929e00ccd
--- /dev/null
+++ b/packages/jspsych/src/timeline/index.ts
@@ -0,0 +1,232 @@
+import { Class } from "type-fest";
+
+import { JsPsychExtension } from "../modules/extensions";
+import { JsPsychPlugin, PluginInfo } from "../modules/plugins";
+import { Trial } from "./Trial";
+import { PromiseWrapper } from "./util";
+
+export class TimelineVariable {
+  constructor(public readonly name: string) {}
+}
+
+export type Parameter = T | (() => T) | TimelineVariable;
+
+export type TrialExtensionsConfiguration = Array<{
+  type: Class;
+  params?: Record;
+}>;
+
+export type SimulationMode = "visual" | "data-only";
+
+export type SimulationOptions = {
+  data?: Record;
+  mode?: SimulationMode;
+  simulate?: boolean;
+};
+
+export type SimulationOptionsParameter = Parameter<{
+  data?: Parameter>>;
+  mode?: Parameter;
+  simulate?: Parameter;
+}>;
+
+export interface TrialDescription extends Record {
+  type: Parameter>>;
+
+  /** https://www.jspsych.org/latest/overview/plugins/#the-post_trial_gap-iti-parameter */
+  post_trial_gap?: Parameter;
+
+  /** https://www.jspsych.org/latest/overview/plugins/#the-save_trial_parameters-parameter */
+  save_trial_parameters?: Parameter>;
+
+  /**
+   * Whether to include the values of timeline variables under a `timeline_variables` key. Can be
+   * `true` to save the values of all timeline variables, or an array of timeline variable names to
+   * only save specific timeline variables. Defaults to `false`.
+   */
+  save_timeline_variables?: Parameter;
+
+  /** https://www.jspsych.org/latest/overview/style/#using-the-css_classes-trial-parameter */
+  css_classes?: Parameter;
+
+  /** https://www.jspsych.org/latest/overview/simulation/#controlling-simulation-mode-with-simulation_options */
+  simulation_options?: SimulationOptionsParameter | string;
+
+  /** https://www.jspsych.org/latest/overview/extensions/ */
+  extensions?: Parameter;
+
+  /**
+   * Whether to record the data of this trial. Defaults to `true`.
+   */
+  record_data?: Parameter;
+
+  // Events
+
+  /** https://www.jspsych.org/latest/overview/events/#on_start-trial */
+  on_start?: (trial: any) => void;
+
+  /** https://www.jspsych.org/latest/overview/events/#on_load */
+  on_load?: () => void;
+
+  /** https://www.jspsych.org/latest/overview/events/#on_finish-trial */
+  on_finish?: (data: any) => void;
+}
+
+/** https://www.jspsych.org/latest/overview/timeline/#sampling-methods */
+export type SampleOptions =
+  | { type: "with-replacement"; size: number; weights?: number[] }
+  | { type: "without-replacement"; size: number }
+  | { type: "fixed-repetitions"; size: number }
+  | { type: "alternate-groups"; groups: number[][]; randomize_group_order?: boolean }
+  | { type: "custom"; fn: (ids: number[]) => number[] };
+
+export type TimelineArray = Array;
+
+export interface TimelineDescription extends Record {
+  timeline: TimelineArray;
+  timeline_variables?: Record[];
+
+  name?: string;
+
+  // Control flow
+
+  /** https://www.jspsych.org/latest/overview/timeline/#repeating-a-set-of-trials */
+  repetitions?: number;
+
+  /** https://www.jspsych.org/latest/overview/timeline/#looping-timelines */
+  loop_function?: (data: any) => boolean;
+
+  /** https://www.jspsych.org/latest/overview/timeline/#conditional-timelines */
+  conditional_function?: () => boolean;
+
+  // Randomization
+
+  /** https://www.jspsych.org/latest/overview/timeline/#random-orders-of-trials */
+  randomize_order?: boolean;
+
+  /** https://www.jspsych.org/latest/overview/timeline/#sampling-methods */
+  sample?: SampleOptions;
+
+  // Events
+
+  /** https://www.jspsych.org/latest/overview/events/#on_timeline_start */
+  on_timeline_start?: () => void;
+
+  /** https://www.jspsych.org/latest/overview/events/#on_timeline_finish */
+  on_timeline_finish?: () => void;
+}
+
+export const timelineDescriptionKeys = [
+  "timeline",
+  "timeline_variables",
+  "name",
+  "repetitions",
+  "loop_function",
+  "conditional_function",
+  "randomize_order",
+  "sample",
+  "on_timeline_start",
+  "on_timeline_finish",
+];
+
+export function isTrialDescription(
+  description: TrialDescription | TimelineDescription
+): description is TrialDescription {
+  return !isTimelineDescription(description);
+}
+
+export function isTimelineDescription(
+  description: TrialDescription | TimelineDescription | TimelineArray
+): description is TimelineDescription | TimelineArray {
+  return Boolean((description as TimelineDescription).timeline) || Array.isArray(description);
+}
+
+export enum TimelineNodeStatus {
+  PENDING,
+  RUNNING,
+  PAUSED,
+  COMPLETED,
+  ABORTED,
+}
+
+/**
+ * Functions and options needed by `TimelineNode`s, provided by the `JsPsych` instance. This
+ * approach allows to keep the public `JsPsych` API slim and decouples the `JsPsych` and timeline
+ * node classes, simplifying unit testing.
+ */
+export interface TimelineNodeDependencies {
+  /**
+   * Called at the start of a trial, prior to invoking the plugin's trial method.
+   */
+  onTrialStart: (trial: Trial) => void;
+
+  /**
+   * Called when a trial's result data is available, before invoking `onTrialFinished()`.
+   */
+  onTrialResultAvailable: (trial: Trial) => void;
+
+  /**
+   * Called after a trial has finished.
+   */
+  onTrialFinished: (trial: Trial) => void;
+
+  /**
+   * Invoke `on_start` extension callbacks according to `extensionsConfiguration`
+   */
+  runOnStartExtensionCallbacks(extensionsConfiguration: TrialExtensionsConfiguration): void;
+
+  /**
+   * Invoke `on_load` extension callbacks according to `extensionsConfiguration`
+   */
+  runOnLoadExtensionCallbacks(extensionsConfiguration: TrialExtensionsConfiguration): void;
+
+  /**
+   * Invoke `on_finish` extension callbacks according to `extensionsConfiguration` and return a
+   * joint extensions result object
+   */
+  runOnFinishExtensionCallbacks(
+    extensionsConfiguration: TrialExtensionsConfiguration
+  ): Promise>;
+
+  /**
+   * Returns the simulation mode or `undefined`, if the experiment is not running in simulation
+   * mode.
+   */
+  getSimulationMode(): SimulationMode | undefined;
+
+  /**
+   * Returns the global simulation options as passed to `jsPsych.simulate()`
+   */
+  getGlobalSimulationOptions(): Record;
+
+  /**
+   * Given a plugin class, create a new instance of it and return it.
+   */
+  instantiatePlugin: (
+    pluginClass: Class>
+  ) => JsPsychPlugin;
+
+  /**
+   * Return JsPsych's display element so it can be provided to plugins
+   */
+  getDisplayElement: () => HTMLElement;
+
+  /**
+   * Return the default inter-trial interval as provided to `initJsPsych()`
+   */
+  getDefaultIti: () => number;
+
+  /**
+   * A `PromiseWrapper` whose promise is resolved with result data whenever `jsPsych.finishTrial()`
+   * is called.
+   */
+  finishTrialPromise: PromiseWrapper;
+
+  /**
+   * Clear all of the timeouts
+   */
+  clearAllTimeouts: () => void;
+}
+
+export type TrialResult = Record;
+export type TrialResults = Array>;
diff --git a/packages/jspsych/src/timeline/util.spec.ts b/packages/jspsych/src/timeline/util.spec.ts
new file mode 100644
index 0000000000..711e2f5285
--- /dev/null
+++ b/packages/jspsych/src/timeline/util.spec.ts
@@ -0,0 +1,124 @@
+import { ParameterObjectPathCache, parameterPathArrayToString } from "./util";
+
+describe("parameterPathArrayToString()", () => {
+  it("works with flat paths", () => {
+    expect(parameterPathArrayToString(["flat"])).toEqual("flat");
+  });
+
+  it("works with nested object paths", () => {
+    expect(parameterPathArrayToString(["nested", "object", "path"])).toEqual("nested.object.path");
+  });
+
+  it("works with array indices", () => {
+    expect(parameterPathArrayToString(["arrayElement", "10"])).toEqual("arrayElement[10]");
+  });
+
+  it("works with nested object paths and array indices", () => {
+    expect(parameterPathArrayToString(["nested", "arrayElement", "10", "property"])).toEqual(
+      "nested.arrayElement[10].property"
+    );
+  });
+});
+
+describe("ParameterObjectPathCache", () => {
+  const rootObject = {
+    object: {
+      id: 1,
+      nestedObject: { id: 2 },
+    },
+    array: [{ id: 3 }, { id: 4, nestedObject: { id: 5 } }],
+    explicitlyUndefined: undefined,
+  };
+
+  let cache: ParameterObjectPathCache;
+
+  beforeEach(() => {
+    cache = new ParameterObjectPathCache();
+    cache.initialize(rootObject);
+  });
+
+  describe("lookup()", () => {
+    it("works with object properties", () => {
+      expect(cache.lookup(["nonExistent"])).toEqual({ doesPathExist: false, value: undefined });
+
+      expect(cache.lookup(["explicitlyUndefined"])).toEqual({
+        doesPathExist: true,
+        value: undefined,
+      });
+
+      expect(cache.lookup(["object"])).toEqual({
+        doesPathExist: true,
+        value: rootObject.object,
+      });
+    });
+
+    it("works with nested object properties", () => {
+      expect(cache.lookup(["object", "nestedObject"])).toEqual({
+        doesPathExist: true,
+        value: rootObject.object.nestedObject,
+      });
+
+      expect(cache.lookup(["nonExistent", "nonExistent"])).toEqual({
+        doesPathExist: false,
+        value: undefined,
+      });
+    });
+
+    it("works with nested array indices", () => {
+      expect(cache.lookup(["array", "0"])).toEqual({
+        doesPathExist: true,
+        value: rootObject.array[0],
+      });
+
+      expect(cache.lookup(["array", "1"])).toEqual({
+        doesPathExist: true,
+        value: rootObject.array[1],
+      });
+
+      expect(cache.lookup(["array", "2"])).toEqual({
+        doesPathExist: false,
+        value: undefined,
+      });
+
+      expect(cache.lookup(["array", "1", "nestedObject"])).toEqual({
+        doesPathExist: true,
+        value: rootObject.array[1].nestedObject,
+      });
+    });
+  });
+
+  describe("set()", () => {
+    it("overrides paths of the root object", () => {
+      cache.set(["object"], { nested: 1 });
+      expect(cache.lookup(["object", "nested"])).toEqual({
+        doesPathExist: true,
+        value: 1,
+      });
+
+      cache.set(["object", "nested"], 2);
+      expect(cache.lookup(["object", "nested"])).toEqual({
+        doesPathExist: true,
+        value: 2,
+      });
+
+      cache.set(["array"], [1]);
+      expect(cache.lookup(["array", "0"])).toEqual({ doesPathExist: true, value: 1 });
+    });
+  });
+
+  describe("reset()", () => {
+    it("deletes all set entries", () => {
+      cache.set(["object"], { nested: 1 });
+      expect(cache.lookup(["object", "nested"])).toEqual({
+        doesPathExist: true,
+        value: 1,
+      });
+
+      cache.reset();
+      expect(cache.lookup(["object", "nested"])).toEqual({
+        doesPathExist: false,
+        value: undefined,
+      });
+    });
+  });
+});
diff --git a/packages/jspsych/src/timeline/util.ts b/packages/jspsych/src/timeline/util.ts
new file mode 100644
index 0000000000..4f30ba2fa4
--- /dev/null
+++ b/packages/jspsych/src/timeline/util.ts
@@ -0,0 +1,146 @@
+/**
+ * Maintains a promise and offers a function to resolve it. Whenever the promise is resolved, it is
+ * replaced with a new one.
+ */
+export class PromiseWrapper {
+  constructor() {
+    this.reset();
+  }
+
+  private promise: Promise;
+  private resolvePromise: (resolveValue: ResolveType) => void;
+
+  reset() {
+    this.promise = new Promise((resolve) => {
+      this.resolvePromise = resolve;
+    });
+  }
+  get() {
+    return this.promise;
+  }
+  resolve(value: ResolveType) {
+    this.resolvePromise(value);
+    this.reset();
+  }
+}
+
+export function isPromise(value: any): value is Promise {
+  return value && typeof value["then"] === "function";
+}
+
+export function delay(ms: number) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+/**
+ * Returns the string representation of a `path` array like accepted by lodash's `get` and `set`
+ * functions.
+ */
+export function parameterPathArrayToString([firstPathElement, ...remainingPathElements]: string[]) {
+  let pathString = firstPathElement ?? "";
+
+  for (const pathElement of remainingPathElements) {
+    pathString += Number.isNaN(Number.parseInt(pathElement))
+      ? `.${pathElement}`
+      : `[${pathElement}]`;
+  }
+
+  return pathString;
+}
+
+function isObjectOrArray(value: any): value is Record | any[] {
+  return typeof value === "object" && value !== null;
+}
+
+type LookupResult = { doesPathExist: boolean; value?: any };
+
+/**
+ * Initialized with an object, provides a `lookup` method to look up nested object and array paths
+ * and a `set` method to override the element that `lookup` uses at a given path. The original
+ * object remains unmodified. All looked up values are cached, including those at intermediate
+ * paths. This means, `set`ting the element at a path only affects nested path lookups if the paths
+ * have not been looked up and cached before.
+ */
+export class ParameterObjectPathCache {
+  private static lookupChild(
+    objectOrArray: Record | any[],
+    childName: string
+  ): LookupResult {
+    let doesPathExist: boolean = false;
+    let childValue: any;
+
+    if (Number.isNaN(Number.parseInt(childName))) {
+      // `childName` refers to an object property
+      if (Object.hasOwn(objectOrArray, childName)) {
+        doesPathExist = true;
+        childValue = objectOrArray[childName];
+      }
+    } else {
+      // `childName` refers to an array index
+      if (Number.parseInt(childName) < objectOrArray.length) {
+        doesPathExist = true;
+        childValue = objectOrArray[childName];
+      }
+    }
+
+    return { doesPathExist, value: childValue };
+  }
+
+  private cache = new Map();
+  private rootObject: any;
+
+  private get(path: string[]) {
+    return this.cache.get(path.join("."));
+  }
+
+  private has(path: string[]) {
+    return this.cache.has(path.join("."));
+  }
+
+  constructor() {}
+
+  public initialize(rootObject: any) {
+    this.rootObject = rootObject;
+    this.cache.set("", rootObject);
+  }
+
+  public reset() {
+    this.cache.clear();
+    this.cache.set("", this.rootObject);
+  }
+
+  public set(path: string[], value: any) {
+    this.cache.set(path.join("."), value);
+  }
+
+  public lookup(path: string[]): LookupResult {
+    if (this.has(path)) {
+      return { doesPathExist: true, value: this.get(path) };
+    }
+
+    // Recursively find the closest ancestor path that has already been cached and start looking up
+    // the path from there, caching intermediate elements along the way
+    const lookupPath = (path: string[]): LookupResult => {
+      const parentPath = path.slice(0, -1);
+      const childName = path[path.length - 1];
+      if (!this.has(parentPath) && parentPath.length > 0) {
+        if (!lookupPath(parentPath).doesPathExist) {
+          return { doesPathExist: false };
+        }
+      }
+
+      const parentValue = this.get(parentPath);
+      if (!isObjectOrArray(parentValue)) {
+        return { doesPathExist: false };
+      }
+
+      const lookupResult = ParameterObjectPathCache.lookupChild(parentValue, childName);
+      if (lookupResult.doesPathExist) {
+        this.set(path, lookupResult.value);
+      }
+      return lookupResult;
+    };
+
+    return lookupPath(path);
+  }
+}
diff --git a/packages/jspsych/tests/TestPlugin.ts b/packages/jspsych/tests/TestPlugin.ts
new file mode 100644
index 0000000000..10f400340e
--- /dev/null
+++ b/packages/jspsych/tests/TestPlugin.ts
@@ -0,0 +1,97 @@
+import { flushPromises } from "@jspsych/test-utils";
+import { JsPsych, JsPsychPlugin, TrialType } from "jspsych";
+
+import { ParameterInfos } from "../src/modules/plugins";
+import { SimulationMode, SimulationOptions, TrialResult } from "../src/timeline";
+import { PromiseWrapper } from "../src/timeline/util";
+
+export const testPluginInfo = {
+  name: "test",
+  version: "0.0.1",
+  parameters: {},
+  data: {},
+};
+
+class TestPlugin implements JsPsychPlugin {
+  static info = testPluginInfo;
+
+  static setParameterInfos(parameters: ParameterInfos) {
+    TestPlugin.info = { ...testPluginInfo, parameters };
+  }
+
+  static resetPluginInfo() {
+    TestPlugin.info = testPluginInfo;
+  }
+
+  static defaultTrialResult: Record = { my: "result" };
+
+  private static finishTrialMode: "immediate" | "manual" = "immediate";
+
+  /**
+   * Disables immediate finishing of the `trial` method of all `TestPlugin` instances. Instead, any
+   * running trial can be finished by invoking `TestPlugin.finishTrial()`.
+   */
+  static setManualFinishTrialMode() {
+    TestPlugin.finishTrialMode = "manual";
+  }
+
+  /**
+   * Makes the `trial` method of all instances of `TestPlugin` finish immediately and allows to
+   * manually finish the trial by invoking `TestPlugin.finishTrial()` instead.
+   */
+  static setImmediateFinishTrialMode() {
+    TestPlugin.finishTrialMode = "immediate";
+  }
+
+  private static trialPromise = new PromiseWrapper>();
+
+  /**
+   * Resolves the promise returned by `trial()` with the provided `result` or
+   * `TestPlugin.defaultTrialResult` if no `result` object was passed.
+   **/
+  static async finishTrial(result?: Record) {
+    TestPlugin.trialPromise.resolve(result ?? TestPlugin.defaultTrialResult);
+    await flushPromises();
+  }
+
+  static defaultTrialImplementation(
+    display_element: HTMLElement,
+    trial: TrialType,
+    on_load: () => void
+  ): void | Promise {
+    on_load();
+    if (TestPlugin.finishTrialMode === "immediate") {
+      return Promise.resolve(TestPlugin.defaultTrialResult);
+    }
+    return TestPlugin.trialPromise.get();
+  }
+
+  public static trial = TestPlugin.defaultTrialImplementation;
+
+  static defaultSimulateImplementation(
+    trial: TrialType,
+    simulation_mode: SimulationMode,
+    simulation_options: SimulationOptions,
+    on_load?: () => void
+  ): void | Promise {
+    return TestPlugin.defaultTrialImplementation(document.createElement("div"), trial, on_load);
+  }
+
+  public static simulate = TestPlugin.defaultSimulateImplementation;
+
+  /** Resets all static properties including function implementations */
+  static reset() {
+    TestPlugin.defaultTrialResult = { my: "result" };
+    TestPlugin.trial = TestPlugin.defaultTrialImplementation;
+    TestPlugin.simulate = TestPlugin.defaultSimulateImplementation;
+    TestPlugin.resetPluginInfo();
+    TestPlugin.setImmediateFinishTrialMode();
+  }
+
+  constructor(private jsPsych: JsPsych) {}
+
+  trial = jest.fn(TestPlugin.trial);
+  simulate = jest.fn(TestPlugin.simulate);
+}
+
+export default TestPlugin;
diff --git a/packages/jspsych/tests/core/endexperiment.test.ts b/packages/jspsych/tests/core/abortexperiment.test.ts
similarity index 90%
rename from packages/jspsych/tests/core/endexperiment.test.ts
rename to packages/jspsych/tests/core/abortexperiment.test.ts
index ab0f0770a2..775601595b 100644
--- a/packages/jspsych/tests/core/endexperiment.test.ts
+++ b/packages/jspsych/tests/core/abortexperiment.test.ts
@@ -11,7 +11,7 @@ test("works on basic timeline", async () => {
         type: htmlKeyboardResponse,
         stimulus: "trial 1",
         on_finish: () => {
-          jsPsych.endExperiment("the end");
+          jsPsych.abortExperiment("the end");
         },
       },
       {
@@ -23,7 +23,7 @@ test("works on basic timeline", async () => {
   );
 
   expect(getHTML()).toMatch("trial 1");
-  pressKey("a");
+  await pressKey("a");
   expect(getHTML()).toMatch("the end");
   await expectFinished();
 });
@@ -35,7 +35,7 @@ test("works with looping timeline (#541)", async () => {
       {
         timeline: [{ type: htmlKeyboardResponse, stimulus: "trial 1" }],
         loop_function: () => {
-          jsPsych.endExperiment("the end");
+          jsPsych.abortExperiment("the end");
         },
       },
     ],
@@ -43,7 +43,7 @@ test("works with looping timeline (#541)", async () => {
   );
 
   expect(getHTML()).toMatch("trial 1");
-  pressKey("a");
+  await pressKey("a");
   expect(getHTML()).toMatch("the end");
   await expectFinished();
 });
@@ -64,7 +64,7 @@ test("if on_finish returns a Promise, wait for resolve before showing end messag
       type: htmlKeyboardResponse,
       stimulus: "foo",
       on_finish: () => {
-        jsPsych.endExperiment("done");
+        jsPsych.abortExperiment("done");
       },
     },
     {
@@ -76,7 +76,7 @@ test("if on_finish returns a Promise, wait for resolve before showing end messag
   const { getHTML, expectFinished, expectRunning } = await startTimeline(timeline, jsPsych);
 
   expect(getHTML()).toMatch("foo");
-  pressKey("a");
+  await pressKey("a");
   expect(getHTML()).not.toMatch("foo");
   expect(getHTML()).not.toMatch("bar");
 
diff --git a/packages/jspsych/tests/core/case-sensitive-responses.test.ts b/packages/jspsych/tests/core/case-sensitive-responses.test.ts
index cf3288c3e2..a287348a82 100644
--- a/packages/jspsych/tests/core/case-sensitive-responses.test.ts
+++ b/packages/jspsych/tests/core/case-sensitive-responses.test.ts
@@ -12,7 +12,7 @@ describe("case_sensitive_responses parameter", () => {
     ]);
 
     expect(getHTML()).toMatch("foo");
-    pressKey("A");
+    await pressKey("A");
     await expectFinished();
   });
 
@@ -29,7 +29,7 @@ describe("case_sensitive_responses parameter", () => {
     );
 
     expect(getHTML()).toMatch("foo");
-    pressKey("A");
+    await pressKey("A");
     await expectFinished();
   });
 
@@ -46,9 +46,9 @@ describe("case_sensitive_responses parameter", () => {
     );
 
     expect(getHTML()).toMatch("foo");
-    pressKey("A");
+    await pressKey("A");
     expect(getHTML()).toMatch("foo");
-    pressKey("a");
+    await pressKey("a");
     await expectFinished();
   });
 });
diff --git a/packages/jspsych/tests/core/css-classes-parameter.test.ts b/packages/jspsych/tests/core/css-classes-parameter.test.ts
index 61dabd2285..6d46a28d06 100644
--- a/packages/jspsych/tests/core/css-classes-parameter.test.ts
+++ b/packages/jspsych/tests/core/css-classes-parameter.test.ts
@@ -26,7 +26,7 @@ describe("The css_classes parameter for trials", () => {
     ]);
 
     expect(displayElement.classList).toContain("foo");
-    pressKey("a");
+    await pressKey("a");
     expect(displayElement.classList).not.toContain("foo");
   });
 
@@ -44,7 +44,7 @@ describe("The css_classes parameter for trials", () => {
     ]);
 
     expect(displayElement.classList).toContain("foo");
-    pressKey("a");
+    await pressKey("a");
     expect(displayElement.classList).not.toContain("foo");
   });
 
@@ -58,7 +58,7 @@ describe("The css_classes parameter for trials", () => {
     ]);
 
     expect(displayElement.classList).toContain("foo");
-    pressKey("a");
+    await pressKey("a");
     expect(displayElement.classList).not.toContain("foo");
   });
 
@@ -81,7 +81,7 @@ describe("The css_classes parameter for trials", () => {
     );
 
     expect(displayElement.classList).toContain("foo");
-    pressKey("a");
+    await pressKey("a");
     expect(displayElement.classList).not.toContain("foo");
   });
 });
diff --git a/packages/jspsych/tests/core/default-iti.test.ts b/packages/jspsych/tests/core/default-iti.test.ts
index ff8a3a1c21..a6db7cfc29 100644
--- a/packages/jspsych/tests/core/default-iti.test.ts
+++ b/packages/jspsych/tests/core/default-iti.test.ts
@@ -1,6 +1,5 @@
-import { jest } from "@jest/globals";
 import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
-import { pressKey, startTimeline } from "@jspsych/test-utils";
+import { flushPromises, pressKey, startTimeline } from "@jspsych/test-utils";
 
 jest.useFakeTimers();
 
@@ -18,7 +17,7 @@ describe("default iti parameter", () => {
     ]);
 
     expect(getHTML()).toMatch("foo");
-    pressKey("a");
+    await pressKey("a");
     expect(getHTML()).toMatch("bar");
   });
 
@@ -38,9 +37,10 @@ describe("default iti parameter", () => {
     );
 
     expect(getHTML()).toMatch("foo");
+    await pressKey("a");
     expect(getHTML()).not.toMatch("bar");
-    pressKey("a");
     jest.advanceTimersByTime(100);
+    await flushPromises();
     expect(getHTML()).toMatch("bar");
   });
 });
diff --git a/packages/jspsych/tests/core/default-parameters.test.ts b/packages/jspsych/tests/core/default-parameters.test.ts
index 6c5290e51c..132e5b1c16 100644
--- a/packages/jspsych/tests/core/default-parameters.test.ts
+++ b/packages/jspsych/tests/core/default-parameters.test.ts
@@ -8,14 +8,7 @@ describe("nested defaults", () => {
     const { displayElement } = await startTimeline([
       {
         type: surveyText,
-        questions: [
-          {
-            prompt: "Question 1.",
-          },
-          {
-            prompt: "Question 2.",
-          },
-        ],
+        questions: [{ prompt: "Question 1." }, { prompt: "Question 2." }],
       },
     ]);
 
@@ -31,14 +24,7 @@ describe("nested defaults", () => {
     const { displayElement } = await startTimeline([
       {
         type: surveyText,
-        questions: [
-          {
-            prompt: "Question 1.",
-          },
-          {
-            prompt: "Question 2.",
-          },
-        ],
+        questions: [{ prompt: "Question 1." }, { prompt: "Question 2." }],
       },
     ]);
 
diff --git a/packages/jspsych/tests/core/events.test.ts b/packages/jspsych/tests/core/events.test.ts
index 8feac710df..891159505a 100644
--- a/packages/jspsych/tests/core/events.test.ts
+++ b/packages/jspsych/tests/core/events.test.ts
@@ -1,6 +1,6 @@
 import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
 import htmlSliderResponse from "@jspsych/plugin-html-slider-response";
-import { pressKey, startTimeline } from "@jspsych/test-utils";
+import { flushPromises, pressKey, startTimeline } from "@jspsych/test-utils";
 
 import { initJsPsych } from "../../src";
 
@@ -20,7 +20,7 @@ describe("on_finish (trial)", () => {
       },
     ]);
 
-    pressKey("a");
+    await pressKey("a");
     expect(key_data).toBe("a");
   });
 
@@ -35,7 +35,7 @@ describe("on_finish (trial)", () => {
       },
     ]);
 
-    pressKey("a");
+    await pressKey("a");
     expect(getData().values()[0].response).toBe(1);
   });
 });
@@ -54,7 +54,7 @@ describe("on_start (trial)", () => {
       },
     ]);
 
-    pressKey("a");
+    await pressKey("a");
     expect(stimulus).toBe("hello");
   });
 
@@ -80,7 +80,7 @@ describe("on_start (trial)", () => {
       jsPsych
     );
 
-    pressKey("a");
+    await pressKey("a");
     expect(d).toBe("hello");
   });
 });
@@ -104,7 +104,7 @@ describe("on_trial_finish (experiment level)", () => {
       jsPsych
     );
 
-    pressKey("a");
+    await pressKey("a");
     expect(key).toBe("a");
   });
 
@@ -124,7 +124,7 @@ describe("on_trial_finish (experiment level)", () => {
       jsPsych
     );
 
-    pressKey("a");
+    await pressKey("a");
     expect(getData().values()[0].write).toBe(true);
   });
 });
@@ -148,7 +148,7 @@ describe("on_data_update", () => {
       jsPsych
     );
 
-    pressKey("a");
+    await pressKey("a");
     expect(key).toBe("a");
   });
 
@@ -174,7 +174,10 @@ describe("on_data_update", () => {
       jsPsych
     );
 
-    jest.advanceTimersByTime(20);
+    jest.advanceTimersByTime(10);
+    await flushPromises();
+    jest.advanceTimersByTime(10);
+    await flushPromises();
 
     expect(onDataUpdateFn).toHaveBeenNthCalledWith(1, expect.objectContaining({ response: null }));
     expect(onDataUpdateFn).toHaveBeenNthCalledWith(
@@ -204,7 +207,7 @@ describe("on_data_update", () => {
       jsPsych
     );
 
-    pressKey("a");
+    await pressKey("a");
     expect(trialLevel).toBe(true);
   });
 
@@ -229,7 +232,7 @@ describe("on_data_update", () => {
       jsPsych
     );
 
-    pressKey("a");
+    await pressKey("a");
     expect(experimentLevel).toBe(true);
   });
 });
@@ -253,7 +256,7 @@ describe("on_trial_start", () => {
       jsPsych
     );
 
-    pressKey("a");
+    await pressKey("a");
     expect(text).toBe("hello");
   });
 
@@ -274,7 +277,7 @@ describe("on_trial_start", () => {
     );
 
     expect(getHTML()).toMatch("goodbye");
-    pressKey("a");
+    await pressKey("a");
   });
 });
 
@@ -302,11 +305,11 @@ describe("on_timeline_finish", () => {
       },
     ]);
 
-    pressKey("a");
+    await pressKey("a");
     expect(onFinishFunction).not.toHaveBeenCalled();
-    pressKey("a");
+    await pressKey("a");
     expect(onFinishFunction).not.toHaveBeenCalled();
-    pressKey("a");
+    await pressKey("a");
     expect(onFinishFunction).toHaveBeenCalledTimes(1);
   });
 
@@ -326,67 +329,10 @@ describe("on_timeline_finish", () => {
       },
     ]);
 
-    pressKey("a");
-    pressKey("a");
+    await pressKey("a");
+    await pressKey("a");
     expect(onFinishFunction).toHaveBeenCalledTimes(1);
   });
-
-  test("should fire on every repetition", async () => {
-    const onFinishFunction = jest.fn();
-
-    await startTimeline([
-      {
-        timeline: [
-          {
-            type: htmlKeyboardResponse,
-            stimulus: "foo",
-          },
-        ],
-        on_timeline_finish: onFinishFunction,
-        repetitions: 2,
-      },
-    ]);
-
-    pressKey("a");
-    pressKey("a");
-    expect(onFinishFunction).toHaveBeenCalledTimes(2);
-  });
-
-  test("should fire before a loop function", async () => {
-    const callback = jest.fn().mockImplementation((str) => str);
-    let count = 0;
-
-    await startTimeline([
-      {
-        timeline: [
-          {
-            type: htmlKeyboardResponse,
-            stimulus: "foo",
-          },
-        ],
-        on_timeline_finish: () => {
-          callback("finish");
-        },
-        loop_function: () => {
-          callback("loop");
-          count++;
-          if (count == 2) {
-            return false;
-          } else {
-            return true;
-          }
-        },
-      },
-    ]);
-
-    pressKey("a");
-    pressKey("a");
-    expect(callback).toHaveBeenCalledTimes(4);
-    expect(callback.mock.calls[0][0]).toBe("finish");
-    expect(callback.mock.calls[1][0]).toBe("loop");
-    expect(callback.mock.calls[2][0]).toBe("finish");
-    expect(callback.mock.calls[3][0]).toBe("loop");
-  });
 });
 
 describe("on_timeline_start", () => {
@@ -414,9 +360,9 @@ describe("on_timeline_start", () => {
     ]);
 
     expect(onStartFunction).toHaveBeenCalledTimes(1);
-    pressKey("a");
-    pressKey("a");
-    pressKey("a");
+    await pressKey("a");
+    await pressKey("a");
+    await pressKey("a");
     expect(onStartFunction).toHaveBeenCalledTimes(1);
   });
 
@@ -437,31 +383,9 @@ describe("on_timeline_start", () => {
     ]);
 
     expect(onStartFunction).toHaveBeenCalledTimes(1);
-    pressKey("a");
-    pressKey("a");
-    expect(onStartFunction).toHaveBeenCalledTimes(1);
-  });
-
-  test("should fire on every repetition", async () => {
-    const onStartFunction = jest.fn();
-
-    await startTimeline([
-      {
-        timeline: [
-          {
-            type: htmlKeyboardResponse,
-            stimulus: "foo",
-          },
-        ],
-        on_timeline_start: onStartFunction,
-        repetitions: 2,
-      },
-    ]);
-
+    await pressKey("a");
+    await pressKey("a");
     expect(onStartFunction).toHaveBeenCalledTimes(1);
-    pressKey("a");
-    pressKey("a");
-    expect(onStartFunction).toHaveBeenCalledTimes(2);
   });
 
   test("should fire after a conditional function", async () => {
@@ -488,6 +412,6 @@ describe("on_timeline_start", () => {
     expect(callback).toHaveBeenCalledTimes(2);
     expect(callback.mock.calls[0][0]).toBe("conditional");
     expect(callback.mock.calls[1][0]).toBe("start");
-    pressKey("a");
+    await pressKey("a");
   });
 });
diff --git a/packages/jspsych/tests/core/functions-as-parameters.test.ts b/packages/jspsych/tests/core/functions-as-parameters.test.ts
index 3fd1ffe17e..472866457d 100644
--- a/packages/jspsych/tests/core/functions-as-parameters.test.ts
+++ b/packages/jspsych/tests/core/functions-as-parameters.test.ts
@@ -16,7 +16,6 @@ describe("standard use of function as parameter", () => {
     ]);
 
     expect(getHTML()).toMatch("foo");
-    pressKey("a");
   });
 
   test("parameters can be protected from early evaluation using ParameterType.FUNCTION", async () => {
@@ -32,7 +31,7 @@ describe("standard use of function as parameter", () => {
     ]);
 
     expect(mock).not.toHaveBeenCalled();
-    clickTarget(document.querySelector("#finish_cloze_button"));
+    await clickTarget(document.querySelector("#finish_cloze_button"));
     expect(mock).toHaveBeenCalledTimes(1);
   });
 });
@@ -47,7 +46,7 @@ describe("data as function", () => {
       },
     ]);
 
-    pressKey("a");
+    await pressKey("a");
     expect(getData().values()[0].x).toBe(1);
   });
 
@@ -62,7 +61,7 @@ describe("data as function", () => {
       },
     ]);
 
-    pressKey("a");
+    await pressKey("a");
     expect(getData().values()[0].x).toBe(1);
   });
 });
@@ -77,7 +76,7 @@ describe("nested parameters as functions", () => {
     ]);
 
     expect(displayElement.querySelectorAll("p.jspsych-survey-text").length).toBe(2);
-    clickTarget(document.querySelector("#jspsych-survey-text-next"));
+    await clickTarget(document.querySelector("#jspsych-survey-text-next"));
     await expectFinished();
   });
 
@@ -102,7 +101,7 @@ describe("nested parameters as functions", () => {
     expect(document.querySelector("#jspsych-survey-text-1 p.jspsych-survey-text").innerHTML).toBe(
       "bar"
     );
-    clickTarget(document.querySelector("#jspsych-survey-text-next"));
+    await clickTarget(document.querySelector("#jspsych-survey-text-next"));
     await expectFinished();
   });
 
@@ -133,7 +132,7 @@ describe("nested parameters as functions", () => {
     expect(document.querySelector("#jspsych-survey-multi-choice-0").innerHTML).toMatch("buzz");
     expect(document.querySelector("#jspsych-survey-multi-choice-1").innerHTML).toMatch("bar");
     expect(document.querySelector("#jspsych-survey-multi-choice-1").innerHTML).toMatch("one");
-    clickTarget(document.querySelector("#jspsych-survey-multi-choice-next"));
+    await clickTarget(document.querySelector("#jspsych-survey-multi-choice-next"));
     await expectFinished();
   });
 
@@ -142,6 +141,7 @@ describe("nested parameters as functions", () => {
 
     const info = {
       name: "function-test-plugin",
+      version: "0.0.1",
       parameters: {
         foo: {
           type: ParameterType.COMPLEX,
@@ -158,6 +158,7 @@ describe("nested parameters as functions", () => {
           },
         },
       },
+      data: {},
     };
 
     class FunctionTestPlugin implements JsPsychPlugin {
@@ -166,26 +167,17 @@ describe("nested parameters as functions", () => {
       constructor(private jsPsych: JsPsych) {}
 
       trial(display_element: HTMLElement, trial: TrialType) {
-        this.jsPsych.finishTrial({
-          not_protected: trial.foo[0].not_protected,
-          protected: trial.foo[0].protected,
-        });
+        this.jsPsych.finishTrial(trial.foo);
       }
     }
 
     const { getData } = await startTimeline([
       {
         type: FunctionTestPlugin,
-        foo: [
-          {
-            not_protected: () => {
-              return "x";
-            },
-            protected: () => {
-              return "y";
-            },
-          },
-        ],
+        foo: {
+          not_protected: () => "x",
+          protected: () => "y",
+        },
       },
     ]);
 
diff --git a/packages/jspsych/tests/core/min-rt.test.ts b/packages/jspsych/tests/core/min-rt.test.ts
index aa0e8db241..c8251fff58 100644
--- a/packages/jspsych/tests/core/min-rt.test.ts
+++ b/packages/jspsych/tests/core/min-rt.test.ts
@@ -17,7 +17,7 @@ describe("minimum_valid_rt parameter", () => {
     ]);
 
     expect(getHTML()).toMatch("foo");
-    pressKey("a");
+    await pressKey("a");
     expect(getHTML()).toMatch("bar");
   });
 
@@ -37,12 +37,12 @@ describe("minimum_valid_rt parameter", () => {
     );
 
     expect(getHTML()).toMatch("foo");
-    pressKey("a");
+    await pressKey("a");
     expect(getHTML()).toMatch("foo");
 
     jest.advanceTimersByTime(100);
 
-    pressKey("a");
+    await pressKey("a");
     expect(getHTML()).toMatch("bar");
   });
 });
diff --git a/packages/jspsych/tests/core/progressbar.test.ts b/packages/jspsych/tests/core/progressbar.test.ts
index 32a1b3941d..39d1c78a9c 100644
--- a/packages/jspsych/tests/core/progressbar.test.ts
+++ b/packages/jspsych/tests/core/progressbar.test.ts
@@ -12,8 +12,8 @@ describe("automatic progress bar", () => {
       },
     ]);
 
-    expect(document.querySelector("#jspsych-progressbar-container")).toBe(null);
-    pressKey("a");
+    expect(document.querySelector("#jspsych-progressbar-container")).toBeNull();
+    await pressKey("a");
   });
 
   test("progress bar displays when show_progress_bar is true", async () => {
@@ -28,7 +28,7 @@ describe("automatic progress bar", () => {
     );
 
     expect(document.querySelector("#jspsych-progressbar-container").innerHTML).toMatch(
-      'Completion Progress
' + 'Completion Progress
' ); }); @@ -42,15 +42,15 @@ describe("automatic progress bar", () => { const progressbarElement = document.querySelector("#jspsych-progressbar-inner"); - expect(progressbarElement.style.width).toBe(""); - pressKey("a"); - expect(progressbarElement.style.width).toBe("25%"); - pressKey("a"); - expect(progressbarElement.style.width).toBe("50%"); - pressKey("a"); - expect(progressbarElement.style.width).toBe("75%"); - pressKey("a"); - expect(progressbarElement.style.width).toBe("100%"); + expect(progressbarElement.style.width).toEqual("0%"); + await pressKey("a"); + expect(progressbarElement.style.width).toEqual("25%"); + await pressKey("a"); + expect(progressbarElement.style.width).toEqual("50%"); + await pressKey("a"); + expect(progressbarElement.style.width).toEqual("75%"); + await pressKey("a"); + expect(progressbarElement.style.width).toEqual("100%"); }); test("progress bar does not automatically update when auto_update_progress_bar is false", async () => { @@ -67,13 +67,13 @@ describe("automatic progress bar", () => { const progressbarElement = document.querySelector("#jspsych-progressbar-inner"); for (let i = 0; i < 4; i++) { - expect(progressbarElement.style.width).toBe(""); - pressKey("a"); + expect(progressbarElement.style.width).toEqual("0%"); + await pressKey("a"); } - expect(progressbarElement.style.width).toBe(""); + expect(progressbarElement.style.width).toEqual("0%"); }); - test("setProgressBar() manually", async () => { + test("set `progressBar.progress` manually", async () => { const jsPsych = initJsPsych({ show_progress_bar: true, auto_update_progress_bar: false, @@ -84,14 +84,14 @@ describe("automatic progress bar", () => { type: htmlKeyboardResponse, stimulus: "foo", on_finish: () => { - jsPsych.setProgressBar(0.2); + jsPsych.progressBar.progress = 0.2; }, }, { type: htmlKeyboardResponse, stimulus: "foo", on_finish: () => { - jsPsych.setProgressBar(0.8); + jsPsych.progressBar.progress = 0.8; }, }, ]; @@ -100,16 +100,16 @@ describe("automatic progress bar", () => { const progressbarElement = document.querySelector("#jspsych-progressbar-inner"); - expect(progressbarElement.style.width).toBe(""); - pressKey("a"); - expect(jsPsych.getProgressBarCompleted()).toBe(0.2); - expect(progressbarElement.style.width).toBe("20%"); - pressKey("a"); - expect(progressbarElement.style.width).toBe("80%"); - expect(jsPsych.getProgressBarCompleted()).toBe(0.8); + expect(progressbarElement.style.width).toEqual("0%"); + await pressKey("a"); + expect(jsPsych.progressBar.progress).toEqual(0.2); + expect(progressbarElement.style.width).toEqual("20%"); + await pressKey("a"); + expect(progressbarElement.style.width).toEqual("80%"); + expect(jsPsych.progressBar.progress).toEqual(0.8); }); - test("getProgressBarCompleted() -- automatic updates", async () => { + test("`progressBar.progress` -- automatic updates", async () => { const trial = { type: htmlKeyboardResponse, stimulus: "foo", @@ -119,13 +119,13 @@ describe("automatic progress bar", () => { show_progress_bar: true, }); - pressKey("a"); - expect(jsPsych.getProgressBarCompleted()).toBe(0.25); - pressKey("a"); - expect(jsPsych.getProgressBarCompleted()).toBe(0.5); - pressKey("a"); - expect(jsPsych.getProgressBarCompleted()).toBe(0.75); - pressKey("a"); - expect(jsPsych.getProgressBarCompleted()).toBe(1); + await pressKey("a"); + expect(jsPsych.progressBar.progress).toEqual(0.25); + await pressKey("a"); + expect(jsPsych.progressBar.progress).toEqual(0.5); + await pressKey("a"); + expect(jsPsych.progressBar.progress).toEqual(0.75); + await pressKey("a"); + expect(jsPsych.progressBar.progress).toEqual(1); }); }); diff --git a/packages/jspsych/tests/core/simulation-mode.test.ts b/packages/jspsych/tests/core/simulation-mode.test.ts index da1a3b0364..94e9e8fcfb 100644 --- a/packages/jspsych/tests/core/simulation-mode.test.ts +++ b/packages/jspsych/tests/core/simulation-mode.test.ts @@ -1,7 +1,7 @@ import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response"; -import { clickTarget, pressKey, simulateTimeline } from "@jspsych/test-utils"; +import { clickTarget, flushPromises, pressKey, simulateTimeline } from "@jspsych/test-utils"; -import { JsPsych, JsPsychPlugin, ParameterType, TrialType, initJsPsych } from "../../src"; +import { JsPsych, ParameterType, initJsPsych } from "../../src"; jest.useFakeTimers(); @@ -246,12 +246,14 @@ describe("data simulation mode", () => { class FakePlugin { static info = { name: "fake-plugin", + version: "0.0.1", parameters: { foo: { type: ParameterType.BOOL, default: true, }, }, + data: {}, }; constructor(private jsPsych: JsPsych) {} @@ -306,7 +308,7 @@ describe("data simulation mode", () => { expect(getData().values()[2].rt).toBe(200); }); - test("endExperiment() works in simulation mode", async () => { + test("abortExperiment() works in simulation mode", async () => { const jsPsych = initJsPsych(); const timeline = [ @@ -314,7 +316,7 @@ describe("data simulation mode", () => { type: htmlKeyboardResponse, stimulus: "foo", on_finish: () => { - jsPsych.endExperiment("done"); + jsPsych.abortExperiment("done"); }, }, { @@ -468,10 +470,17 @@ describe("data simulation mode", () => { }, ]; - const { expectRunning, expectFinished, getData } = await simulateTimeline(timeline, "visual", { - default: { data: { rt: 200 } }, - }); + const { jsPsych, expectRunning, expectFinished, getData } = await simulateTimeline( + timeline, + "visual", + { + default: { data: { rt: 200 } }, + } + ); + // Make the event loop tick for each simulated keyboard response + jest.runAllTimers(); + await flushPromises(); jest.runAllTimers(); await expectFinished(); @@ -523,7 +532,7 @@ describe("data simulation mode", () => { expect(getHTML()).toContain("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toContain("bar"); @@ -571,7 +580,7 @@ describe("data simulation mode", () => { expect(getHTML()).toContain("foo"); - pressKey("a"); // this is the user responding instead of letting the simulation handle it. + await pressKey("a"); // this is the user responding instead of letting the simulation handle it. expect(getHTML()).toContain("bar"); @@ -606,7 +615,7 @@ describe("data simulation mode", () => { expect(on_load).toHaveBeenCalled(); - pressKey("a"); + await pressKey("a"); await expectFinished(); }); diff --git a/packages/jspsych/tests/core/test-complex-plugin.ts b/packages/jspsych/tests/core/test-complex-plugin.ts index 4cc53a7a7a..bf93a0c0b9 100644 --- a/packages/jspsych/tests/core/test-complex-plugin.ts +++ b/packages/jspsych/tests/core/test-complex-plugin.ts @@ -2,6 +2,7 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; const info = { name: "test-complex-plugin", + version: "0.0.1", parameters: { blocks: { type: ParameterType.COMPLEX, @@ -23,6 +24,7 @@ const info = { }, }, }, + data: {}, }; type Info = typeof info; diff --git a/packages/jspsych/tests/core/timeline-variables.test.ts b/packages/jspsych/tests/core/timeline-variables.test.ts index 73b65f79d3..7bd1e8cdad 100644 --- a/packages/jspsych/tests/core/timeline-variables.test.ts +++ b/packages/jspsych/tests/core/timeline-variables.test.ts @@ -1,5 +1,4 @@ import callFunction from "@jspsych/plugin-call-function"; -import htmlButtonResponse from "@jspsych/plugin-html-button-response"; import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response"; import { pressKey, startTimeline } from "@jspsych/test-utils"; @@ -39,12 +38,12 @@ describe("sampling", () => { let last = getHTML(); for (let i = 0; i < 23; i++) { - pressKey("a"); + await pressKey("a"); let curr = getHTML(); expect(last).not.toMatch(curr); last = curr; } - pressKey("a"); + await pressKey("a"); }); test("sampling functions run when timeline loops", async () => { @@ -80,9 +79,9 @@ describe("sampling", () => { const result2 = []; for (let i = 0; i < reps / 2; i++) { result1.push(getHTML()); - pressKey("a"); + await pressKey("a"); result2.push(getHTML()); - pressKey("a"); + await pressKey("a"); } expect(result1).not.toEqual(result2); @@ -115,7 +114,7 @@ describe("sampling", () => { ); for (let i = 0; i < 10; i++) { - pressKey("a"); + await pressKey("a"); } await expectFinished(); @@ -140,18 +139,16 @@ describe("timeline variables are correctly evaluated", () => { { type: jsPsych.timelineVariable("type"), stimulus: "hello", - choices: ["a", "b"], }, ], - timeline_variables: [{ type: htmlKeyboardResponse }, { type: htmlButtonResponse }], + timeline_variables: [{ type: htmlKeyboardResponse }], }, ], jsPsych ); - expect(getHTML()).not.toMatch("button"); - pressKey("a"); - expect(getHTML()).toMatch("button"); + expect(getHTML()).toMatch("hello"); + await pressKey("a"); }); test("when used with a plugin that has a FUNCTION parameter type", async () => { @@ -202,8 +199,8 @@ describe("timeline variables are correctly evaluated", () => { jsPsych ); - pressKey("a"); - pressKey("a"); + await pressKey("a"); + await pressKey("a"); expect(jsPsych.data.get().select("id").values).toEqual([2, 0]); }); @@ -243,10 +240,10 @@ describe("timeline variables are correctly evaluated", () => { jsPsych ); - pressKey("a"); - pressKey("a"); - pressKey("a"); - pressKey("a"); + await pressKey("a"); + await pressKey("a"); + await pressKey("a"); + await pressKey("a"); expect(jsPsych.data.get().select("id").values).toEqual([3, 2, 1, 0]); }); @@ -268,7 +265,7 @@ describe("timeline variables are correctly evaluated", () => { ); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("bar"); }); @@ -287,7 +284,7 @@ describe("timeline variables are correctly evaluated", () => { ], timeline_variables: [{ x: "foo" }], conditional_function: () => { - x = jsPsych.timelineVariable("x"); + x = jsPsych.evaluateTimelineVariable("x"); return true; }, }, @@ -295,7 +292,7 @@ describe("timeline variables are correctly evaluated", () => { jsPsych ); - pressKey("a"); + await pressKey("a"); expect(x).toBe("foo"); }); @@ -314,7 +311,7 @@ describe("timeline variables are correctly evaluated", () => { ], timeline_variables: [{ x: "foo" }], loop_function: () => { - x = jsPsych.timelineVariable("x"); + x = jsPsych.evaluateTimelineVariable("x"); return false; }, }, @@ -322,7 +319,7 @@ describe("timeline variables are correctly evaluated", () => { jsPsych ); - pressKey("a"); + await pressKey("a"); expect(x).toBe("foo"); }); @@ -336,7 +333,7 @@ describe("timeline variables are correctly evaluated", () => { type: htmlKeyboardResponse, stimulus: "hello world", on_finish: (data) => { - data.x = jsPsych.timelineVariable("x"); + data.x = jsPsych.evaluateTimelineVariable("x"); }, }, ], @@ -346,7 +343,7 @@ describe("timeline variables are correctly evaluated", () => { jsPsych ); - pressKey("a"); + await pressKey("a"); expect(jsPsych.data.get().values()[0].x).toBe("foo"); }); @@ -362,7 +359,7 @@ describe("timeline variables are correctly evaluated", () => { type: htmlKeyboardResponse, stimulus: "hello world", on_start: () => { - x = jsPsych.timelineVariable("x"); + x = jsPsych.evaluateTimelineVariable("x"); }, }, ], @@ -372,7 +369,7 @@ describe("timeline variables are correctly evaluated", () => { jsPsych ); - pressKey("a"); + await pressKey("a"); expect(x).toBe("foo"); }); @@ -388,7 +385,7 @@ describe("timeline variables are correctly evaluated", () => { type: htmlKeyboardResponse, stimulus: "hello world", on_load: () => { - x = jsPsych.timelineVariable("x"); + x = jsPsych.evaluateTimelineVariable("x"); }, }, ], @@ -398,126 +395,15 @@ describe("timeline variables are correctly evaluated", () => { jsPsych ); - pressKey("a"); + await pressKey("a"); expect(x).toBe("foo"); }); }); -describe("jsPsych.getAllTimelineVariables()", () => { - test("gets all timeline variables for a simple timeline", async () => { - const jsPsych = initJsPsych(); - await startTimeline( - [ - { - timeline: [ - { - type: htmlKeyboardResponse, - stimulus: "foo", - on_finish: (data) => { - var all_tvs = jsPsych.getAllTimelineVariables(); - Object.assign(data, all_tvs); - }, - }, - ], - timeline_variables: [ - { a: 1, b: 2 }, - { a: 2, b: 3 }, - ], - }, - ], - jsPsych - ); - - pressKey("a"); - pressKey("a"); - - expect(jsPsych.data.get().values()).toEqual([ - expect.objectContaining({ a: 1, b: 2 }), - expect.objectContaining({ a: 2, b: 3 }), - ]); - }); - - test("gets all timeline variables for a nested timeline", async () => { - const jsPsych = initJsPsych(); - await startTimeline( - [ - { - timeline: [ - { - timeline: [ - { - type: htmlKeyboardResponse, - stimulus: "foo", - on_finish: (data) => { - var all_tvs = jsPsych.getAllTimelineVariables(); - Object.assign(data, all_tvs); - }, - }, - ], - timeline_variables: [ - { a: 1, b: 2 }, - { a: 2, b: 3 }, - ], - }, - ], - timeline_variables: [{ c: 1 }, { c: 2 }], - }, - ], - jsPsych - ); - - for (let i = 0; i < 4; i++) { - pressKey("a"); - } - - expect(jsPsych.data.get().values()).toEqual([ - expect.objectContaining({ a: 1, b: 2, c: 1 }), - expect.objectContaining({ a: 2, b: 3, c: 1 }), - expect.objectContaining({ a: 1, b: 2, c: 2 }), - expect.objectContaining({ a: 2, b: 3, c: 2 }), - ]); - }); - - test("gets the right values in a conditional_function", async () => { - let a: number, b: number; - - const jsPsych = initJsPsych(); - await startTimeline( - [ - { - timeline: [ - { - type: htmlKeyboardResponse, - stimulus: "foo", - }, - ], - timeline_variables: [ - { a: 1, b: 2 }, - { a: 2, b: 3 }, - ], - conditional_function: () => { - var all_tvs = jsPsych.getAllTimelineVariables(); - a = all_tvs.a; - b = all_tvs.b; - return true; - }, - }, - ], - jsPsych - ); - - pressKey("a"); - pressKey("a"); - - expect(a).toBe(1); - expect(b).toBe(2); - }); -}); - // using console.warn instead of error for now. plan is to enable this test with version 8. test.skip("timelineVariable() throws an error when variable doesn't exist", async () => { const jsPsych = initJsPsych(); - await startTimeline( + const { expectFinished } = await startTimeline( [ { timeline: [ @@ -525,7 +411,7 @@ test.skip("timelineVariable() throws an error when variable doesn't exist", asyn type: htmlKeyboardResponse, stimulus: "foo", on_start: () => { - expect(() => jsPsych.timelineVariable("c")).toThrowError(); + expect(() => jsPsych.evaluateTimelineVariable("c")).toThrowError(); }, }, ], @@ -538,8 +424,10 @@ test.skip("timelineVariable() throws an error when variable doesn't exist", asyn jsPsych ); - pressKey("a"); - pressKey("a"); + await pressKey("a"); + await pressKey("a"); + + await expectFinished(); }); test("timelineVariable() can fetch a variable called 'data'", async () => { @@ -552,7 +440,7 @@ test("timelineVariable() can fetch a variable called 'data'", async () => { type: htmlKeyboardResponse, stimulus: "foo", on_start: () => { - expect(() => jsPsych.timelineVariable("data")).not.toThrowError(); + expect(() => jsPsych.evaluateTimelineVariable("data")).not.toThrowError(); }, }, ], @@ -565,8 +453,8 @@ test("timelineVariable() can fetch a variable called 'data'", async () => { jsPsych ); - pressKey("a"); - pressKey("a"); + await pressKey("a"); + await pressKey("a"); await expectFinished(); }); diff --git a/packages/jspsych/tests/core/timelines.test.ts b/packages/jspsych/tests/core/timelines.test.ts index dd8a8f0baf..0a98299089 100644 --- a/packages/jspsych/tests/core/timelines.test.ts +++ b/packages/jspsych/tests/core/timelines.test.ts @@ -22,11 +22,11 @@ describe("loop function", () => { const { jsPsych } = await startTimeline([trial]); // first trial - pressKey("a"); + await pressKey("a"); expect(jsPsych.data.get().count()).toBe(1); // second trial - pressKey("a"); + await pressKey("a"); expect(jsPsych.data.get().count()).toBe(2); }); @@ -44,11 +44,11 @@ describe("loop function", () => { ]); // first trial - pressKey("a"); + await pressKey("a"); expect(jsPsych.data.get().count()).toBe(1); // second trial - pressKey("a"); + await pressKey("a"); expect(jsPsych.data.get().count()).toBe(1); }); @@ -77,13 +77,13 @@ describe("loop function", () => { ]); // first trial - pressKey("a"); + await pressKey("a"); // second trial - pressKey("a"); + await pressKey("a"); // third trial - pressKey("a"); + await pressKey("a"); expect(data_count).toEqual([1, 1, 1]); expect(jsPsych.data.get().count()).toBe(3); @@ -109,7 +109,7 @@ describe("loop function", () => { }, ], loop_function: () => { - if (jsPsych.timelineVariable("word") == "b" && counter < 2) { + if (jsPsych.evaluateTimelineVariable("word") === "b" && counter < 2) { counter++; return true; } else { @@ -126,21 +126,21 @@ describe("loop function", () => { ); expect(getHTML()).toMatch("a"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("b"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("c"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); }); test("only runs once when timeline variables are used", async () => { @@ -163,11 +163,11 @@ describe("loop function", () => { ]); // first trial - pressKey("a"); + await pressKey("a"); expect(count).toBe(0); // second trial - pressKey("a"); + await pressKey("a"); expect(count).toBe(1); }); }); @@ -194,7 +194,7 @@ describe("conditional function", () => { expect(getHTML()).toMatch("bar"); // clear - pressKey("a"); + await pressKey("a"); }); test("completes the timeline when returns true", async () => { @@ -220,52 +220,12 @@ describe("conditional function", () => { expect(getHTML()).toMatch("foo"); // next - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("bar"); // clear - pressKey("a"); - }); - - test("executes on every loop of the timeline", async () => { - let count = 0; - let conditional_count = 0; - - await startTimeline([ - { - timeline: [ - { - type: htmlKeyboardResponse, - stimulus: "foo", - }, - ], - loop_function: () => { - if (count < 1) { - count++; - return true; - } else { - return false; - } - }, - conditional_function: () => { - conditional_count++; - return true; - }, - }, - ]); - - expect(conditional_count).toBe(1); - - // first trial - pressKey("a"); - - expect(conditional_count).toBe(2); - - // second trial - pressKey("a"); - - expect(conditional_count).toBe(2); + await pressKey("a"); }); test("executes only once even when repetitions is > 1", async () => { @@ -290,12 +250,12 @@ describe("conditional function", () => { expect(conditional_count).toBe(1); // first trial - pressKey("a"); + await pressKey("a"); expect(conditional_count).toBe(1); // second trial - pressKey("a"); + await pressKey("a"); expect(conditional_count).toBe(1); }); @@ -322,12 +282,12 @@ describe("conditional function", () => { expect(conditional_count).toBe(1); // first trial - pressKey("a"); + await pressKey("a"); expect(conditional_count).toBe(1); // second trial - pressKey("a"); + await pressKey("a"); expect(conditional_count).toBe(1); }); @@ -350,7 +310,7 @@ describe("conditional function", () => { }, ], conditional_function: () => { - if (jsPsych.timelineVariable("word") == "b") { + if (jsPsych.evaluateTimelineVariable("word") === "b") { return false; } else { return true; @@ -365,19 +325,19 @@ describe("conditional function", () => { ); expect(getHTML()).toMatch("a"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("b"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("c"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); }); }); -describe("endCurrentTimeline", () => { +describe("abortCurrentTimeline", () => { test("stops the current timeline, skipping to the end after the trial completes", async () => { const jsPsych = initJsPsych(); const { getHTML } = await startTimeline( @@ -388,7 +348,7 @@ describe("endCurrentTimeline", () => { type: htmlKeyboardResponse, stimulus: "foo", on_finish: () => { - jsPsych.endCurrentTimeline(); + jsPsych.abortCurrentTimeline(); }, }, { @@ -406,9 +366,9 @@ describe("endCurrentTimeline", () => { ); expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("woo"); - pressKey("a"); + await pressKey("a"); }); test("works inside nested timelines", async () => { @@ -423,7 +383,7 @@ describe("endCurrentTimeline", () => { type: htmlKeyboardResponse, stimulus: "foo", on_finish: () => { - jsPsych.endCurrentTimeline(); + jsPsych.abortCurrentTimeline(); }, }, { @@ -448,15 +408,93 @@ describe("endCurrentTimeline", () => { expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("bar"); - pressKey("a"); + await pressKey("a"); + + expect(getHTML()).toMatch("woo"); + + await pressKey("a"); + }); +}); + +describe("abortTimelineByName", () => { + test("stops the timeline with the given name, skipping to the end after the trial completes", async () => { + const jsPsych = initJsPsych(); + const { getHTML } = await startTimeline( + [ + { + timeline: [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + on_finish: () => { + jsPsych.abortTimelineByName("timeline"); + }, + }, + { + type: htmlKeyboardResponse, + stimulus: "bar", + }, + ], + name: "timeline", + }, + { + type: htmlKeyboardResponse, + stimulus: "woo", + }, + ], + jsPsych + ); + expect(getHTML()).toMatch("foo"); + await pressKey("a"); expect(getHTML()).toMatch("woo"); + await pressKey("a"); + }); - pressKey("a"); + test("works inside nested timelines", async () => { + const jsPsych = initJsPsych(); + const { getHTML } = await startTimeline( + [ + { + timeline: [ + { + timeline: [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + on_finish: () => { + jsPsych.abortTimelineByName("timeline"); + }, + }, + { + type: htmlKeyboardResponse, + stimulus: "skip me!", + }, + ], + }, + { + type: htmlKeyboardResponse, + stimulus: "skip me too!", + }, + ], + name: "timeline", + }, + { + type: htmlKeyboardResponse, + stimulus: "woo", + }, + ], + jsPsych + ); + + expect(getHTML()).toMatch("foo"); + await pressKey("a"); + expect(getHTML()).toMatch("woo"); + await pressKey("a"); }); }); @@ -478,33 +516,8 @@ describe("nested timelines", () => { ]); expect(getHTML()).toMatch("foo"); - pressKey("a"); - expect(getHTML()).toMatch("bar"); - pressKey("a"); - }); -}); - -describe("add node to end of timeline", () => { - test("adds node to end of timeline", async () => { - const jsPsych = initJsPsych(); - const { getHTML } = await startTimeline( - [ - { - type: htmlKeyboardResponse, - stimulus: "foo", - on_start: () => { - jsPsych.addNodeToEndOfTimeline({ - timeline: [{ type: htmlKeyboardResponse, stimulus: "bar" }], - }); - }, - }, - ], - jsPsych - ); - - expect(getHTML()).toMatch("foo"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("bar"); - pressKey("a"); + await pressKey("a"); }); }); diff --git a/packages/jspsych/tests/data/data-csv-conversion.test.ts b/packages/jspsych/tests/data/data-csv-conversion.test.ts index a7d9ef0c4f..133c5f4168 100644 --- a/packages/jspsych/tests/data/data-csv-conversion.test.ts +++ b/packages/jspsych/tests/data/data-csv-conversion.test.ts @@ -17,11 +17,13 @@ describe("data conversion to csv", () => { document.querySelector("#input-0").value = "Response 1"; document.querySelector("#input-1").value = "Response 2"; - clickTarget(document.querySelector("#jspsych-survey-text-next")); + await clickTarget(document.querySelector("#jspsych-survey-text-next")); - expect(getData().ignore(["rt", "internal_node_id", "time_elapsed", "trial_type"]).csv()).toBe( - '"response","trial_index"\r\n"{""Q0"":""Response 1"",""Q1"":""Response 2""}","0"\r\n' - ); + expect( + getData() + .ignore(["rt", "internal_node_id", "time_elapsed", "trial_type", "plugin_version"]) + .csv() + ).toBe('"response","trial_index"\r\n"{""Q0"":""Response 1"",""Q1"":""Response 2""}","0"\r\n'); }); test("same-different-html stimulus array is correctly converted", async () => { @@ -36,10 +38,10 @@ describe("data conversion to csv", () => { ]); expect(getHTML()).toMatch("

Climbing

"); - pressKey("q"); + await pressKey("q"); jest.runAllTimers(); expect(getHTML()).toMatch("

Walking

"); - pressKey("q"); + await pressKey("q"); expect(getHTML()).toBe(""); expect( @@ -51,6 +53,7 @@ describe("data conversion to csv", () => { "trial_type", "rt_stim1", "response_stim1", + "plugin_version", ]) .csv() ).toBe( @@ -67,14 +70,21 @@ describe("data conversion to csv", () => { ]); expect(getHTML()).toMatch("foo"); - clickTarget(document.querySelector("#jspsych-survey-multi-select-response-0-0")); - clickTarget(document.querySelector("#jspsych-survey-multi-select-response-0-1")); - clickTarget(document.querySelector("#jspsych-survey-multi-select-next")); + await clickTarget(document.querySelector("#jspsych-survey-multi-select-response-0-0")); + await clickTarget(document.querySelector("#jspsych-survey-multi-select-response-0-1")); + await clickTarget(document.querySelector("#jspsych-survey-multi-select-next")); expect(getHTML()).toBe(""); expect( getData() - .ignore(["rt", "internal_node_id", "time_elapsed", "trial_type", "question_order"]) + .ignore([ + "rt", + "internal_node_id", + "time_elapsed", + "trial_type", + "question_order", + "plugin_version", + ]) .csv() ).toBe('"response","trial_index"\r\n"{""q"":[""fuzz"",""bizz""]}","0"\r\n'); }); diff --git a/packages/jspsych/tests/data/data-json-conversion.test.ts b/packages/jspsych/tests/data/data-json-conversion.test.ts index 0862f6a573..5d58d902e7 100644 --- a/packages/jspsych/tests/data/data-json-conversion.test.ts +++ b/packages/jspsych/tests/data/data-json-conversion.test.ts @@ -18,11 +18,13 @@ describe("data conversion to json", () => { document.querySelector("#input-0").value = "Response 1"; document.querySelector("#input-1").value = "Response 2"; - clickTarget(document.querySelector("#jspsych-survey-text-next")); + await clickTarget(document.querySelector("#jspsych-survey-text-next")); - expect(getData().ignore(["rt", "internal_node_id", "time_elapsed", "trial_type"]).json()).toBe( - JSON.stringify([{ response: { Q0: "Response 1", Q1: "Response 2" }, trial_index: 0 }]) - ); + expect( + getData() + .ignore(["rt", "internal_node_id", "time_elapsed", "trial_type", "plugin_version"]) + .json() + ).toBe(JSON.stringify([{ response: { Q0: "Response 1", Q1: "Response 2" }, trial_index: 0 }])); }); test("same-different-html stimulus array is correctly converted", async () => { @@ -37,10 +39,10 @@ describe("data conversion to json", () => { ]); expect(getHTML()).toMatch("

Climbing

"); - pressKey("q"); + await pressKey("q"); jest.runAllTimers(); expect(getHTML()).toMatch("

Walking

"); - pressKey("q"); + await pressKey("q"); expect(getHTML()).toBe(""); expect( @@ -52,6 +54,7 @@ describe("data conversion to json", () => { "trial_type", "rt_stim1", "response_stim1", + "plugin_version", ]) .json() ).toBe( @@ -76,14 +79,21 @@ describe("data conversion to json", () => { ]); expect(getHTML()).toMatch("foo"); - clickTarget(document.querySelector("#jspsych-survey-multi-select-response-0-0")); - clickTarget(document.querySelector("#jspsych-survey-multi-select-response-0-1")); - clickTarget(document.querySelector("#jspsych-survey-multi-select-next")); + await clickTarget(document.querySelector("#jspsych-survey-multi-select-response-0-0")); + await clickTarget(document.querySelector("#jspsych-survey-multi-select-response-0-1")); + await clickTarget(document.querySelector("#jspsych-survey-multi-select-next")); expect(getHTML()).toBe(""); expect( getData() - .ignore(["rt", "internal_node_id", "time_elapsed", "trial_type", "question_order"]) + .ignore([ + "rt", + "internal_node_id", + "time_elapsed", + "trial_type", + "question_order", + "plugin_version", + ]) .json() ).toBe( JSON.stringify([ @@ -108,9 +118,9 @@ describe("data conversion to json", () => { ]); expect(getHTML()).toMatch("page 1"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toMatch("page 2"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toBe(""); const jsonData = getData().ignore(["rt", "internal_node_id", "time_elapsed"]).json(); diff --git a/packages/jspsych/tests/data/datamodule.test.ts b/packages/jspsych/tests/data/datamodule.test.ts index 5fb3dfb981..85bba21b2d 100644 --- a/packages/jspsych/tests/data/datamodule.test.ts +++ b/packages/jspsych/tests/data/datamodule.test.ts @@ -8,7 +8,7 @@ describe("Basic data recording", () => { const { getData } = await startTimeline([{ type: htmlKeyboardResponse, stimulus: "hello" }]); // click through first trial - pressKey("a"); + await pressKey("a"); // check if data contains rt expect(getData().select("rt").count()).toBe(1); }); @@ -21,11 +21,11 @@ describe("#addProperties", () => { ]); // click through first trial - pressKey("a"); + await pressKey("a"); // check if data contains testprop - expect(getData().select("testprop").count()).toBe(0); + expect(getData().values()[0]).not.toHaveProperty("testprop"); jsPsych.data.addProperties({ testprop: 1 }); - expect(getData().select("testprop").count()).toBe(1); + expect(getData().values()[0]).toHaveProperty("testprop"); }); }); @@ -46,10 +46,9 @@ describe("#addDataToLastTrial", () => { ); // click through first trial - pressKey("a"); + await pressKey("a"); // check data structure - expect(getData().select("testA").values[0]).toBe(1); - expect(getData().select("testB").values[0]).toBe(2); + expect(getData().values()[0]).toEqual(expect.objectContaining({ testA: 1, testB: 2 })); }); }); @@ -65,9 +64,9 @@ describe("#getLastTrialData", () => { ); // click through first trial - pressKey("a"); + await pressKey("a"); // click through second trial - pressKey("a"); + await pressKey("a"); // check data structure expect(jsPsych.data.getLastTrialData().select("trial_index").values[0]).toBe(1); }); @@ -92,7 +91,7 @@ describe("#getLastTimelineData", () => { // click through all four trials for (let i = 0; i < 4; i++) { - pressKey("a"); + await pressKey("a"); } // check data structure expect(jsPsych.data.getLastTimelineData().count()).toBe(2); @@ -103,23 +102,17 @@ describe("#getLastTimelineData", () => { describe("#displayData", () => { test("should display in json format", async () => { - const { jsPsych, getHTML } = await startTimeline([ + const { jsPsych, getHTML, getData } = await startTimeline([ { type: htmlKeyboardResponse, stimulus: "hello" }, ]); // click through first trial - pressKey("a"); - // overwrite data with custom data - const data = [ - { col1: 1, col2: 2 }, - { col1: 3, col2: 4 }, - ]; - jsPsych.data._customInsert(data); + await pressKey("a"); // display data in json format jsPsych.data.displayData("json"); // check display element HTML expect(getHTML()).toBe( - '
' + JSON.stringify(data, null, "\t") + "
" + '
' + JSON.stringify(getData().values(), null, "\t") + "
" ); }); test("should display in csv format", async () => { @@ -128,18 +121,17 @@ describe("#displayData", () => { ]); // click through first trial - pressKey("a"); + await pressKey("a"); // overwrite data with custom data const data = [ { col1: 1, col2: 2 }, { col1: 3, col2: 4 }, ]; - jsPsych.data._customInsert(data); // display data in json format jsPsych.data.displayData("csv"); // check display element HTML - expect(getHTML()).toBe( - '
"col1","col2"\r\n"1","2"\r\n"3","4"\r\n
' + expect(getHTML()).toMatch( + /
"rt","stimulus","response","trial_type","trial_index","plugin_version","time_elapsed"\r\n"[\d]+","hello","a","html-keyboard-response","0","[\d.]+","[\d]+"\r\n<\/pre>/
     );
   });
 });
diff --git a/packages/jspsych/tests/data/dataparameter.test.ts b/packages/jspsych/tests/data/dataparameter.test.ts
index 7de1fa9f87..5898839029 100644
--- a/packages/jspsych/tests/data/dataparameter.test.ts
+++ b/packages/jspsych/tests/data/dataparameter.test.ts
@@ -13,7 +13,7 @@ describe("The data parameter", () => {
       },
     ]);
 
-    pressKey("a");
+    await pressKey("a");
     await finished;
 
     expect(getData().values()[0].added).toBe(true);
@@ -28,8 +28,8 @@ describe("The data parameter", () => {
       },
     ]);
 
-    pressKey("a");
-    pressKey("a");
+    await pressKey("a");
+    await pressKey("a");
     await finished;
 
     expect(getData().filter({ added: true }).count()).toBe(2);
@@ -50,8 +50,8 @@ describe("The data parameter", () => {
       jsPsych
     );
 
-    pressKey("a");
-    pressKey("a");
+    await pressKey("a");
+    await pressKey("a");
     await finished;
 
     expect(getData().filter({ added: true }).count()).toBe(2);
@@ -71,8 +71,8 @@ describe("The data parameter", () => {
       jsPsych
     );
 
-    pressKey("a"); // trial 1
-    pressKey("a"); // trial 2
+    await pressKey("a"); // trial 1
+    await pressKey("a"); // trial 2
 
     expect(getData().filter({ added: true }).count()).toBe(1);
     expect(getData().filter({ added: false }).count()).toBe(1);
@@ -96,8 +96,8 @@ describe("The data parameter", () => {
       jsPsych
     );
 
-    pressKey("a"); // trial 1
-    pressKey("a"); // trial 2
+    await pressKey("a"); // trial 1
+    await pressKey("a"); // trial 2
 
     expect(getData().filter({ added: true }).count()).toBe(1);
     expect(getData().filter({ added: false }).count()).toBe(1);
@@ -124,8 +124,8 @@ describe("The data parameter", () => {
       jsPsych
     );
 
-    pressKey("a"); // trial 1
-    pressKey("a"); // trial 2
+    await pressKey("a"); // trial 1
+    await pressKey("a"); // trial 2
 
     expect(getData().filter({ added_copy: true }).count()).toBe(1);
     expect(getData().filter({ added_copy: false }).count()).toBe(1);
@@ -150,8 +150,8 @@ describe("The data parameter", () => {
       jsPsych
     );
 
-    pressKey("a");
-    pressKey("a");
+    await pressKey("a");
+    await pressKey("a");
     await finished;
 
     expect(getData().filter({ added: true, foo: 1 }).count()).toBe(2);
@@ -168,7 +168,7 @@ describe("The data parameter", () => {
       },
     ]);
 
-    pressKey("a");
+    await pressKey("a");
     await finished;
 
     expect(getData().values()[0].a).toBe(1);
diff --git a/packages/jspsych/tests/data/interactions.test.ts b/packages/jspsych/tests/data/interactions.test.ts
index 23dffec7fb..db1074bb37 100644
--- a/packages/jspsych/tests/data/interactions.test.ts
+++ b/packages/jspsych/tests/data/interactions.test.ts
@@ -1,77 +1,74 @@
 import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
 import { pressKey, startTimeline } from "@jspsych/test-utils";
 
-import { initJsPsych } from "../../src";
+import { JsPsych, initJsPsych } from "../../src";
+
+function setIsFullScreen(isFullscreen: boolean) {
+  // @ts-expect-error
+  window.document.isFullScreen = isFullscreen;
+  document.dispatchEvent(new Event("fullscreenchange"));
+}
+
+afterEach(async () => {
+  // Finish the experiment so its interaction listeners are removed
+  await pressKey("a");
+});
 
 describe("Data recording", () => {
+  let jsPsych: JsPsych;
+
+  beforeEach(async () => {
+    jsPsych = (await startTimeline([{ type: htmlKeyboardResponse, stimulus: "hello" }])).jsPsych;
+  });
+
   test("record focus events", async () => {
-    const { jsPsych } = await startTimeline([{ type: htmlKeyboardResponse, stimulus: "hello" }]);
     window.dispatchEvent(new Event("focus"));
-    // click through first trial
-    pressKey("a");
-    // check data
     expect(jsPsych.data.getInteractionData().filter({ event: "focus" }).count()).toBe(1);
   });
 
   test("record blur events", async () => {
-    const { jsPsych } = await startTimeline([{ type: htmlKeyboardResponse, stimulus: "hello" }]);
     window.dispatchEvent(new Event("blur"));
-    // click through first trial
-    pressKey("a");
-    // check data
     expect(jsPsych.data.getInteractionData().filter({ event: "blur" }).count()).toBe(1);
   });
 
-  /* not sure yet how to test fullscreen events with jsdom engine */
-
-  test.skip("record fullscreenenter events", async () => {
-    const { jsPsych } = await startTimeline([{ type: htmlKeyboardResponse, stimulus: "hello" }]);
-    // click through first trial
-    pressKey("a");
-    // check if data contains rt
+  test("record fullscreenenter events", async () => {
+    setIsFullScreen(true);
+    expect(jsPsych.data.getInteractionData().filter({ event: "fullscreenenter" }).count()).toBe(1);
   });
 
-  test.skip("record fullscreenexit events", async () => {
-    const { jsPsych } = await startTimeline([{ type: htmlKeyboardResponse, stimulus: "hello" }]);
-    // click through first trial
-    pressKey("a");
-    // check if data contains rt
+  test("record fullscreenexit events", async () => {
+    setIsFullScreen(false);
+    expect(jsPsych.data.getInteractionData().filter({ event: "fullscreenexit" }).count()).toBe(1);
   });
 });
 
 describe("on_interaction_data_update", () => {
-  test("fires for blur", async () => {
-    const updateFunction = jest.fn();
-    const jsPsych = initJsPsych({
-      on_interaction_data_update: updateFunction,
-    });
+  const updateFunction = jest.fn();
+  let jsPsych: JsPsych;
 
+  beforeEach(async () => {
+    updateFunction.mockClear();
+    jsPsych = initJsPsych({ on_interaction_data_update: updateFunction });
     await startTimeline([{ type: htmlKeyboardResponse, stimulus: "hello" }], jsPsych);
+  });
 
+  test("fires for blur", async () => {
     window.dispatchEvent(new Event("blur"));
     expect(updateFunction).toHaveBeenCalledTimes(1);
-
-    // click through first trial
-    pressKey("a");
   });
 
   test("fires for focus", async () => {
-    const updateFunction = jest.fn();
-
-    const jsPsych = initJsPsych({
-      on_interaction_data_update: updateFunction,
-    });
-    await startTimeline([{ type: htmlKeyboardResponse, stimulus: "hello" }], jsPsych);
-
     window.dispatchEvent(new Event("focus"));
     expect(updateFunction).toHaveBeenCalledTimes(1);
-    // click through first trial
-    pressKey("a");
   });
 
-  /* not sure yet how to test fullscreen events with jsdom engine */
-
-  test.skip("fires for fullscreenexit", () => {});
+  test("fires for fullscreenenter", async () => {
+    setIsFullScreen(true);
+    expect(updateFunction).toHaveBeenCalledTimes(1);
+  });
 
-  test.skip("fires for fullscreenenter", () => {});
+  test("fires for fullscreenexit", async () => {
+    setIsFullScreen(false);
+    expect(updateFunction).toHaveBeenCalledTimes(1);
+  });
 });
diff --git a/packages/jspsych/tests/data/recorddataparameter.test.ts b/packages/jspsych/tests/data/recorddataparameter.test.ts
new file mode 100644
index 0000000000..cf08561f6c
--- /dev/null
+++ b/packages/jspsych/tests/data/recorddataparameter.test.ts
@@ -0,0 +1,86 @@
+import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
+import { pressKey, startTimeline } from "@jspsych/test-utils";
+
+import { initJsPsych } from "../../src";
+
+describe("The record_data parameter", () => {
+  it("Defaults to true", async () => {
+    const { getData } = await startTimeline([
+      {
+        type: htmlKeyboardResponse,
+        stimulus: "

foo

", + }, + ]); + + await pressKey(" "); + + expect(getData().count()).toBe(1); + }); + + it("Can be set to false to prevent the data from being recorded", async () => { + const onFinish = jest.fn(); + const onTrialFinish = jest.fn(); + const onDataUpdate = jest.fn(); + + const { getData } = await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "

foo

", + record_data: false, + on_finish: onFinish, + }, + ], + { on_trial_finish: onTrialFinish, on_data_update: onDataUpdate } + ); + + await pressKey(" "); + + expect(getData().count()).toBe(0); + expect(onFinish).toHaveBeenCalledWith(undefined); + expect(onTrialFinish).toHaveBeenCalledWith(undefined); + expect(onDataUpdate).not.toHaveBeenCalled(); + }); + + it("Can be set as a timeline variable", async () => { + const jsPsych = initJsPsych(); + const { getData } = await startTimeline( + [ + { + timeline: [ + { + type: htmlKeyboardResponse, + stimulus: "

foo

", + record_data: jsPsych.timelineVariable("record_data"), + }, + ], + timeline_variables: [{ record_data: true }, { record_data: false }], + }, + ], + jsPsych + ); + + await pressKey(" "); + await pressKey(" "); + + expect(getData().count()).toBe(1); + }); + + it("Can be set as a function", async () => { + const jsPsych = initJsPsych(); + const { getData } = await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "

foo

", + record_data: () => false, + }, + ], + jsPsych + ); + + await pressKey(" "); + + expect(getData().count()).toBe(0); + }); +}); diff --git a/packages/jspsych/tests/data/trialparameters.test.ts b/packages/jspsych/tests/data/trialparameters.test.ts index a239e830f6..3627742ebc 100644 --- a/packages/jspsych/tests/data/trialparameters.test.ts +++ b/packages/jspsych/tests/data/trialparameters.test.ts @@ -16,7 +16,7 @@ describe("Trial parameters in the data", () => { }, ]); - pressKey(" "); + await pressKey(" "); const data = getData().values()[0]; expect(data.choices).not.toBeUndefined(); @@ -34,31 +34,12 @@ describe("Trial parameters in the data", () => { }, ]); - pressKey(" "); + await pressKey(" "); const data = getData().values()[0]; expect(data.stimulus).toBeUndefined(); }); - test("For compatibility with data access functions, internal_node_id and trial_index cannot be removed", async () => { - const { getData } = await startTimeline([ - { - type: htmlKeyboardResponse, - stimulus: "

foo

", - save_trial_parameters: { - internal_node_id: false, - trial_index: false, - }, - }, - ]); - - pressKey(" "); - - const data = getData().values()[0]; - expect(data.internal_node_id).not.toBeUndefined(); - expect(data.trial_index).not.toBeUndefined(); - }); - test("Invalid parameter names throw a warning in the console", async () => { const spy = jest.spyOn(console, "warn").mockImplementation(); @@ -67,15 +48,17 @@ describe("Trial parameters in the data", () => { type: htmlKeyboardResponse, stimulus: "

foo

", save_trial_parameters: { + trial_type: false, + trial_index: false, foo: true, bar: false, }, }, ]); - pressKey(" "); + await pressKey(" "); - expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledTimes(4); spy.mockRestore(); }); @@ -92,7 +75,7 @@ describe("Trial parameters in the data", () => { }, ]); - clickTarget(document.querySelector("#jspsych-survey-text-next")); + await clickTarget(document.querySelector("#jspsych-survey-text-next")); const data = getData().values()[0]; expect(data.questions[0].prompt).toBe(questions[0].prompt); @@ -124,7 +107,7 @@ describe("Trial parameters in the data", () => { }, ]); - clickTarget(document.querySelector("button")); + await clickTarget(document.querySelector("button")); expect(getData().values()[0].stim_function).toBe(sample_function.toString()); }); @@ -141,7 +124,7 @@ describe("Trial parameters in the data", () => { }, ]); - pressKey(" "); + await pressKey(" "); expect(getData().values()[0].trial_duration).toBe(1000); }); diff --git a/packages/jspsych/tests/extensions/extensions.test.ts b/packages/jspsych/tests/extensions/extensions.test.ts index cbf9a02d20..c98f532bc6 100644 --- a/packages/jspsych/tests/extensions/extensions.test.ts +++ b/packages/jspsych/tests/extensions/extensions.test.ts @@ -1,168 +1,183 @@ import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response"; -import { pressKey } from "@jspsych/test-utils"; +import { pressKey, startTimeline } from "@jspsych/test-utils"; import { JsPsych, initJsPsych } from "../../src"; -import testExtension from "./test-extension"; +import { TestExtension } from "./test-extension"; jest.useFakeTimers(); describe("jsPsych.extensions", () => { let jsPsych: JsPsych; + let extension: TestExtension; beforeEach(() => { - jsPsych = initJsPsych({ - extensions: [{ type: testExtension }], - }); + jsPsych = initJsPsych({ extensions: [{ type: TestExtension }] }); + extension = jsPsych.extensions.test as TestExtension; }); test("initialize is called at start of experiment", async () => { - expect(typeof jsPsych.extensions.test.initialize).toBe("function"); - - const initFunc = jest.spyOn(jsPsych.extensions.test, "initialize"); - - const timeline = [ - { - type: htmlKeyboardResponse, - stimulus: "foo", - on_load: () => { - pressKey("a"); + expect.assertions(2); + + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + on_start: () => { + expect(extension.initialize).toHaveBeenCalled(); + }, }, - on_start: () => { - expect(initFunc).toHaveBeenCalled(); - }, - }, - ]; + ], + jsPsych + ); - await jsPsych.run(timeline); + expect(typeof extension.initialize).toBe("function"); + await pressKey("a"); }); test("initialize gets params", async () => { - const jsPsych = initJsPsych({ - extensions: [{ type: testExtension, params: { foo: 1 } }], - }); - - expect(typeof jsPsych.extensions.test.initialize).toBe("function"); - - const initFunc = jest.spyOn(jsPsych.extensions.test, "initialize"); + expect.assertions(2); - const timeline = [ - { - type: htmlKeyboardResponse, - stimulus: "foo", - on_load: () => { - pressKey("a"); - }, - on_start: () => { - expect(initFunc).toHaveBeenCalledWith({ foo: 1 }); + jsPsych = initJsPsych({ + extensions: [{ type: TestExtension, params: { foo: 1 } }], + }); + extension = jsPsych.extensions.test as TestExtension; + + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + on_start: () => { + expect(extension.initialize).toHaveBeenCalledWith({ foo: 1 }); + }, }, - }, - ]; + ], + jsPsych + ); + + expect(typeof extension.initialize).toBe("function"); - await jsPsych.run(timeline); + await pressKey("a"); }); test("on_start is called before trial", async () => { - const onStartFunc = jest.spyOn(jsPsych.extensions.test, "on_start"); - - const trial = { - type: htmlKeyboardResponse, - stimulus: "foo", - extensions: [{ type: testExtension }], - on_load: () => { - expect(onStartFunc).toHaveBeenCalled(); - pressKey("a"); - }, - }; - - await jsPsych.run([trial]); + expect.assertions(1); + + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + extensions: [{ type: TestExtension }], + on_load: () => { + expect(extension.on_start).toHaveBeenCalled(); + }, + }, + ], + jsPsych + ); + + await pressKey("a"); }); test("on_start gets params", async () => { - const onStartFunc = jest.spyOn(jsPsych.extensions.test, "on_start"); - - const trial = { - type: htmlKeyboardResponse, - stimulus: "foo", - extensions: [{ type: testExtension, params: { foo: 1 } }], - on_load: () => { - expect(onStartFunc).toHaveBeenCalledWith({ foo: 1 }); - pressKey("a"); - }, - }; - - await jsPsych.run([trial]); + expect.assertions(1); + + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + extensions: [{ type: TestExtension, params: { foo: 1 } }], + on_load: () => { + expect(extension.on_start).toHaveBeenCalledWith({ foo: 1 }); + }, + }, + ], + jsPsych + ); + + await pressKey("a"); }); test("on_load is called after load", async () => { - const onLoadFunc = jest.spyOn(jsPsych.extensions.test, "on_load"); - - const trial = { - type: htmlKeyboardResponse, - stimulus: "foo", - extensions: [{ type: testExtension }], - on_load: () => { - // trial load happens before extension load - expect(onLoadFunc).not.toHaveBeenCalled(); - pressKey("a"); - }, - }; - - await jsPsych.run([trial]); - - expect(onLoadFunc).toHaveBeenCalled(); + expect.assertions(2); + + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + extensions: [{ type: TestExtension }], + on_load: () => { + // trial load happens before extension load + expect(extension.on_load).not.toHaveBeenCalled(); + }, + }, + ], + jsPsych + ); + + expect(extension.on_load).toHaveBeenCalled(); + + await pressKey("a"); }); test("on_load gets params", async () => { - const onLoadFunc = jest.spyOn(jsPsych.extensions.test, "on_load"); - - const trial = { - type: htmlKeyboardResponse, - stimulus: "foo", - extensions: [{ type: testExtension, params: { foo: 1 } }], - on_load: () => { - pressKey("a"); - }, - }; + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + extensions: [{ type: TestExtension, params: { foo: 1 } }], + }, + ], + jsPsych + ); - await jsPsych.run([trial]); + expect(extension.on_load).toHaveBeenCalledWith({ foo: 1 }); - expect(onLoadFunc).toHaveBeenCalledWith({ foo: 1 }); + await pressKey("a"); }); test("on_finish called after trial", async () => { - const onFinishFunc = jest.spyOn(jsPsych.extensions.test, "on_finish"); - - const trial = { - type: htmlKeyboardResponse, - stimulus: "foo", - extensions: [{ type: testExtension }], - on_load: () => { - expect(onFinishFunc).not.toHaveBeenCalled(); - pressKey("a"); - }, - }; + expect.assertions(2); + + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + extensions: [{ type: TestExtension }], + on_load: () => { + expect(extension.on_finish).not.toHaveBeenCalled(); + }, + }, + ], + jsPsych + ); - await jsPsych.run([trial]); + await pressKey("a"); - expect(onFinishFunc).toHaveBeenCalled(); + expect(extension.on_finish).toHaveBeenCalled(); }); test("on_finish gets params", async () => { - const onFinishFunc = jest.spyOn(jsPsych.extensions.test, "on_finish"); - - const trial = { - type: htmlKeyboardResponse, - stimulus: "foo", - extensions: [{ type: testExtension, params: { foo: 1 } }], - on_load: () => { - expect(onFinishFunc).not.toHaveBeenCalled(); - pressKey("a"); - }, - }; + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + extensions: [{ type: TestExtension, params: { foo: 1 } }], + }, + ], + jsPsych + ); - await jsPsych.run([trial]); + await pressKey("a"); - expect(onFinishFunc).toHaveBeenCalledWith({ foo: 1 }); + expect(extension.on_finish).toHaveBeenCalledWith({ foo: 1 }); }); test.each` @@ -170,35 +185,41 @@ describe("jsPsych.extensions", () => { ${"on_finish"} | ${{ extension_data: true }} ${"async on_finish"} | ${Promise.resolve({ extension_data: true })} `("$name adds trial data", async ({ onFinishReturnValue }) => { - jest.spyOn(jsPsych.extensions.test, "on_finish").mockReturnValueOnce(onFinishReturnValue); - - const trial = { - type: htmlKeyboardResponse, - stimulus: "foo", - extensions: [{ type: testExtension }], - on_load: () => { - pressKey("a"); - }, - }; + extension.on_finish.mockReturnValueOnce(onFinishReturnValue); + + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + extensions: [{ type: TestExtension }], + }, + ], + jsPsych + ); - await jsPsych.run([trial]); + await pressKey("a"); expect(jsPsych.data.get().values()[0].extension_data).toBe(true); }); test("on_finish data is available in trial on_finish", async () => { - const trial = { - type: htmlKeyboardResponse, - stimulus: "foo", - extensions: [{ type: testExtension }], - on_load: () => { - pressKey("a"); - }, - on_finish: (data) => { - expect(data.extension_data).toBe(true); - }, - }; - - await jsPsych.run([trial]); + expect.assertions(1); + + await startTimeline( + [ + { + type: htmlKeyboardResponse, + stimulus: "foo", + extensions: [{ type: TestExtension }], + on_finish: (data) => { + expect(data.extension_data).toBe(true); + }, + }, + ], + jsPsych + ); + + await pressKey("a"); }); }); diff --git a/packages/jspsych/tests/extensions/test-extension.ts b/packages/jspsych/tests/extensions/test-extension.ts index f0fb90abfd..d7391daada 100644 --- a/packages/jspsych/tests/extensions/test-extension.ts +++ b/packages/jspsych/tests/extensions/test-extension.ts @@ -1,6 +1,6 @@ import { JsPsych, JsPsychExtension } from "../../src"; -class TestExtension implements JsPsychExtension { +export class TestExtension implements JsPsychExtension { static info = { name: "test", }; @@ -9,26 +9,15 @@ class TestExtension implements JsPsychExtension { // required, will be called at initJsPsych // should return a Promise - initialize(params) { - return new Promise((resolve, reject) => { - resolve(); - }); - } + initialize = jest.fn().mockResolvedValue(undefined); // required, will be called when the trial starts (before trial loads) - on_start(params) {} + on_start = jest.fn(); // required will be called when the trial loads - on_load(params) {} + on_load = jest.fn(); // required, will be called when jsPsych.finishTrial() is called // must return data object to be merged into data. - on_finish(params) { - // send back data - return { - extension_data: true, - }; - } + on_finish = jest.fn().mockReturnValue({ extension_data: true }); } - -export default TestExtension; diff --git a/packages/jspsych/tests/pluginAPI/pluginapi.test.ts b/packages/jspsych/tests/pluginAPI/pluginapi.test.ts index c55e5c76c5..77ce59faa0 100644 --- a/packages/jspsych/tests/pluginAPI/pluginapi.test.ts +++ b/packages/jspsych/tests/pluginAPI/pluginapi.test.ts @@ -19,39 +19,39 @@ describe("#getKeyboardResponse", () => { callback_function: callback, }); - keyDown("a"); + await keyDown("a"); expect(callback).toHaveBeenCalledTimes(1); - keyUp("a"); + await keyUp("a"); expect(callback).toHaveBeenCalledTimes(1); }); - test("should execute only valid keys", () => { + test("should execute only valid keys", async () => { new KeyboardListenerAPI(getRootElement).getKeyboardResponse({ callback_function: callback, valid_responses: ["a"], }); - pressKey("b"); + await pressKey("b"); expect(callback).toHaveBeenCalledTimes(0); - pressKey("a"); + await pressKey("a"); expect(callback).toHaveBeenCalledTimes(1); }); - test('should not respond when "NO_KEYS" is used', () => { + test('should not respond when "NO_KEYS" is used', async () => { new KeyboardListenerAPI(getRootElement).getKeyboardResponse({ callback_function: callback, valid_responses: "NO_KEYS", }); - pressKey("a"); + await pressKey("a"); expect(callback).toHaveBeenCalledTimes(0); - pressKey("a"); + await pressKey("a"); expect(callback).toHaveBeenCalledTimes(0); }); - test("should not respond to held keys when allow_held_key is false", () => { + test("should not respond to held keys when allow_held_key is false", async () => { const api = new KeyboardListenerAPI(getRootElement); - keyDown("a"); + await keyDown("a"); api.getKeyboardResponse({ callback_function: callback, @@ -59,16 +59,16 @@ describe("#getKeyboardResponse", () => { allow_held_key: false, }); - keyDown("a"); + await keyDown("a"); expect(callback).toHaveBeenCalledTimes(0); - keyUp("a"); - pressKey("a"); + await keyUp("a"); + await pressKey("a"); expect(callback).toHaveBeenCalledTimes(1); }); - test("should respond to held keys when allow_held_key is true", () => { + test("should respond to held keys when allow_held_key is true", async () => { const api = new KeyboardListenerAPI(getRootElement); - keyDown("a"); + await keyDown("a"); api.getKeyboardResponse({ callback_function: callback, @@ -76,9 +76,21 @@ describe("#getKeyboardResponse", () => { allow_held_key: true, }); - keyDown("a"); + await keyDown("a"); expect(callback).toHaveBeenCalledTimes(1); - keyUp("a"); + await keyUp("a"); + }); + + test("should return the key in standard capitalization (issue #3325)", async () => { + const api = new KeyboardListenerAPI(getRootElement); + + api.getKeyboardResponse({ + callback_function: callback, + valid_responses: ["enter"], + }); + + await pressKey("Enter"); + expect(callback).toHaveBeenCalledWith({ key: "Enter", rt: expect.any(Number) }); }); describe("when case_sensitive_responses is false", () => { @@ -88,43 +100,43 @@ describe("#getKeyboardResponse", () => { api = new KeyboardListenerAPI(getRootElement); }); - test("should convert response key to lowercase before determining validity", () => { + test("should convert response key to lowercase before determining validity", async () => { // case_sensitive_responses is false by default api.getKeyboardResponse({ callback_function: callback, valid_responses: ["a"], }); - pressKey("A"); + await pressKey("A"); expect(callback).toHaveBeenCalledTimes(1); }); - test("should not respond to held key when response/valid key case differs and allow_held_key is false", () => { - keyDown("A"); + test("should not respond to held key when response/valid key case differs and allow_held_key is false", async () => { + await keyDown("A"); api.getKeyboardResponse({ callback_function: callback, valid_responses: ["a"], allow_held_key: false, }); - keyDown("A"); - expect(callback).toHaveBeenCalledTimes(0); - keyUp("A"); - pressKey("A"); + await keyDown("A"); + expect(callback).not.toHaveBeenCalled(); + await keyUp("A"); + await pressKey("A"); expect(callback).toHaveBeenCalledTimes(1); }); - test("should respond to held keys when response/valid case differs and allow_held_key is true", () => { - keyDown("A"); + test("should respond to held keys when response/valid case differs and allow_held_key is true", async () => { + await keyDown("A"); api.getKeyboardResponse({ callback_function: callback, valid_responses: ["a"], allow_held_key: true, }); - keyDown("A"); + await keyDown("A"); expect(callback).toHaveBeenCalledTimes(1); - keyUp("A"); + await keyUp("A"); }); }); @@ -135,18 +147,18 @@ describe("#getKeyboardResponse", () => { api = new KeyboardListenerAPI(getRootElement, true); }); - test("should not convert response key to lowercase before determining validity", () => { + test("should not convert response key to lowercase before determining validity", async () => { api.getKeyboardResponse({ callback_function: callback, valid_responses: ["a"], }); - pressKey("A"); + await pressKey("A"); expect(callback).toHaveBeenCalledTimes(0); }); - test("should not respond to a held key when response/valid case differs and allow_held_key is true", () => { - keyDown("A"); + test("should not respond to a held key when response/valid case differs and allow_held_key is true", async () => { + await keyDown("A"); api.getKeyboardResponse({ callback_function: callback, @@ -154,13 +166,13 @@ describe("#getKeyboardResponse", () => { allow_held_key: true, }); - keyDown("A"); + await keyDown("A"); expect(callback).toHaveBeenCalledTimes(0); - keyUp("A"); + await keyUp("A"); }); - test("should not respond to a held key when response/valid case differs and allow_held_key is false", () => { - keyDown("A"); + test("should not respond to a held key when response/valid case differs and allow_held_key is false", async () => { + await keyDown("A"); api.getKeyboardResponse({ callback_function: callback, @@ -168,13 +180,13 @@ describe("#getKeyboardResponse", () => { allow_held_key: false, }); - keyDown("A"); + await keyDown("A"); expect(callback).toHaveBeenCalledTimes(0); - keyUp("A"); + await keyUp("A"); }); }); - test("handles two listeners on the same key correctly #2104/#2105", () => { + test("handles two listeners on the same key correctly #2104/#2105", async () => { const callback_1 = jest.fn(); const callback_2 = jest.fn(); const api = new KeyboardListenerAPI(getRootElement); @@ -188,19 +200,19 @@ describe("#getKeyboardResponse", () => { persist: false, }); - keyDown("a"); + await keyDown("a"); expect(callback_1).toHaveBeenCalledTimes(1); expect(callback_2).toHaveBeenCalledTimes(1); - keyUp("a"); + await keyUp("a"); - keyDown("a"); + await keyDown("a"); expect(callback_1).toHaveBeenCalledTimes(2); expect(callback_2).toHaveBeenCalledTimes(1); - keyUp("a"); + await keyUp("a"); }); }); @@ -213,7 +225,7 @@ describe("#cancelKeyboardResponse", () => { const listener = api.getKeyboardResponse({ callback_function: callback }); api.cancelKeyboardResponse(listener); - pressKey("q"); + await pressKey("q"); expect(callback).toHaveBeenCalledTimes(1); }); }); @@ -227,7 +239,7 @@ describe("#cancelAllKeyboardResponses", () => { api.getKeyboardResponse({ callback_function: callback }); api.cancelAllKeyboardResponses(); - pressKey("q"); + await pressKey("q"); expect(callback).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/jspsych/tests/randomization/setseed.test.ts b/packages/jspsych/tests/randomization/setseed.test.ts index 4aaf528bfb..36d68fbe54 100644 --- a/packages/jspsych/tests/randomization/setseed.test.ts +++ b/packages/jspsych/tests/randomization/setseed.test.ts @@ -41,7 +41,7 @@ describe("setSeed generates predictable randomization", () => { ); for (let i = 0; i < 9; i++) { - pressKey(" "); + await pressKey(" "); } const data_run_1 = getData().readOnly(); @@ -80,7 +80,7 @@ describe("setSeed generates predictable randomization", () => { ); for (let i = 0; i < 9; i++) { - pressKey(" "); + await pressKey(" "); } const data_run_2 = getData2().readOnly(); diff --git a/packages/jspsych/tests/test-utils.ts b/packages/jspsych/tests/test-utils.ts new file mode 100644 index 0000000000..3b0cbcbcad --- /dev/null +++ b/packages/jspsych/tests/test-utils.ts @@ -0,0 +1,68 @@ +import { Class } from "type-fest"; + +import { JsPsych, JsPsychPlugin } from "../src"; +import { TimelineNodeDependencies, TrialResult } from "../src/timeline"; +import { PromiseWrapper } from "../src/timeline/util"; + +jest.mock("../src/JsPsych"); + +/** + * A class to instantiate mock `TimelineNodeDependencies` objects + */ +export class TimelineNodeDependenciesMock implements TimelineNodeDependencies { + private jsPsych = new JsPsych(); // So we have something for plugins in `instantiatePlugin` + private displayElement = document.createElement("div"); + + onTrialStart = jest.fn(); + onTrialResultAvailable = jest.fn(); + onTrialFinished = jest.fn(); + + runOnStartExtensionCallbacks = jest.fn(); + runOnLoadExtensionCallbacks = jest.fn(); + runOnFinishExtensionCallbacks = jest.fn< + ReturnType, + any + >(async () => ({})); + + getSimulationMode = jest.fn, any>(); + getGlobalSimulationOptions = jest.fn< + ReturnType, + any + >(() => ({})); + + instantiatePlugin = jest.fn( + (pluginClass: Class>) => new pluginClass(this.jsPsych) + ); + + getDisplayElement = jest.fn(() => this.displayElement); + getDefaultIti = jest.fn(() => 0); + + finishTrialPromise = new PromiseWrapper(); + + clearAllTimeouts = jest.fn(); +} + +/** + * Returns utilities for capturing the result of a provided `snapshotFunction` with a callback + * function and store its result in a `snapshots` object, keyed by an arbitrary name. + */ +export function createSnapshotUtils(snapshotFunction: () => SnapshotValueType) { + const snapshots: Record = {}; + const createSnapshotCallback = (snapshotName: string) => () => { + snapshots[snapshotName] = snapshotFunction(); + }; + + return { snapshots, createSnapshotCallback }; +} + +/** + * Returns utilities for saving the invocation order of callback functions. + */ +export function createInvocationOrderUtils() { + const invocations: string[] = []; + const createInvocationOrderCallback = (callbackName: string) => () => { + invocations.push(callbackName); + }; + + return { invocations, createInvocationOrderCallback }; +} diff --git a/packages/jspsych/tsconfig.json b/packages/jspsych/tsconfig.json index 87269ce9b7..d4c096a7f9 100644 --- a/packages/jspsych/tsconfig.json +++ b/packages/jspsych/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "@jspsych/config/tsconfig.core.json", "compilerOptions": { - "baseUrl": ".", - "resolveJsonModule": false // using https://stackoverflow.com/a/61426303 instead + "baseUrl": "." }, - "include": ["src", "tests", "global.d.ts"] + "include": ["src", "tests", "package.json"] } diff --git a/packages/plugin-animation/src/index.ts b/packages/plugin-animation/src/index.ts index 9f37a38b25..405f4eb382 100644 --- a/packages/plugin-animation/src/index.ts +++ b/packages/plugin-animation/src/index.ts @@ -1,66 +1,111 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "animation", + version: version, parameters: { - /** Array containing the image(s) to be displayed. */ + /** Each element of the array is a path to an image file. */ stimuli: { type: ParameterType.IMAGE, - pretty_name: "Stimuli", default: undefined, array: true, }, - /** Duration to display each image. */ + /** How long to display each image in milliseconds. */ frame_time: { type: ParameterType.INT, - pretty_name: "Frame time", default: 250, }, - /** Length of gap to be shown between each image. */ + /** If greater than 0, then a gap will be shown between each image in the sequence. This parameter + * specifies the length of the gap in milliseconds. + */ frame_isi: { type: ParameterType.INT, - pretty_name: "Frame gap", default: 0, }, - /** Number of times to show entire sequence */ + /** How many times to show the entire sequence. There will be no gap (other than the gap specified by `frame_isi`) + * between repetitions. */ sequence_reps: { type: ParameterType.INT, - pretty_name: "Sequence repetitions", default: 1, }, - /** Array containing the key(s) the subject is allowed to press to respond to the stimuli. */ + /** This array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. + * Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see + * [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) and + * [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/) + * for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"ALL_KEYS"` + * means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean that no responses are allowed. */ choices: { type: ParameterType.KEYS, - pretty_name: "Choices", default: "ALL_KEYS", }, - /** Any content here will be displayed below stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that + * it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key(s) to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, /** - * If true, the images will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers). - * If false, the image will be shown via an img element. + * If true, the images will be drawn onto a canvas element. This prevents a blank screen (white flash) between consecutive + * images in some browsers, like Firefox and Edge. If false, the image will be shown via an img element, as in previous + * versions of jsPsych. */ render_on_canvas: { type: ParameterType.BOOL, - pretty_name: "Render on canvas", default: true, }, }, + data: { + /** An array, where each element is an object that represents a stimulus in the animation sequence. Each object has + * a `stimulus` property, which is the image that was displayed, and a `time` property, which is the time in ms, + * measured from when the sequence began, that the stimulus was displayed. The array will be encoded in JSON format + * when data is saved using either the `.json()` or `.csv()` functions. + */ + animation_sequence: { + type: ParameterType.COMPLEX, + array: true, + parameters: { + stimulus: { + type: ParameterType.STRING, + }, + time: { + type: ParameterType.INT, + }, + }, + }, + /** An array, where each element is an object representing a response given by the participant. Each object has a + * `stimulus` property, indicating which image was displayed when the key was pressed, an `rt` property, indicating + * the time of the key press relative to the start of the animation, and a `key_press` property, indicating which + * key was pressed. The array will be encoded in JSON format when data is saved using either the `.json()` or `.csv()` + * functions. + */ + response: { + type: ParameterType.COMPLEX, + array: true, + parameters: { + stimulus: { + type: ParameterType.STRING, + }, + rt: { + type: ParameterType.INT, + }, + key_press: { + type: ParameterType.STRING, + }, + }, + }, + }, }; type Info = typeof info; /** - * **animation** - * - * jsPsych plugin for showing animations and recording keyboard responses + * This plugin displays a sequence of images at a fixed frame rate. The sequence can be looped a specified number of times. + * The participant is free to respond at any point during the animation, and the time of the response is recorded. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-animation/ animation plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/animation/ animation plugin documentation on jspsych.org} */ class AnimationPlugin implements JsPsychPlugin { static info = info; diff --git a/packages/plugin-audio-button-response/src/index.spec.ts b/packages/plugin-audio-button-response/src/index.spec.ts index 281e2011f3..8a3ff5196a 100644 --- a/packages/plugin-audio-button-response/src/index.spec.ts +++ b/packages/plugin-audio-button-response/src/index.spec.ts @@ -1,13 +1,60 @@ -import { clickTarget, simulateTimeline, startTimeline } from "@jspsych/test-utils"; +jest.mock("../../jspsych/src/modules/plugin-api/AudioPlayer"); + +import { clickTarget, flushPromises, simulateTimeline, startTimeline } from "@jspsych/test-utils"; import { initJsPsych } from "jspsych"; +//@ts-expect-error mock +import { mockStop } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; import audioButtonResponse from "."; jest.useFakeTimers(); +beforeEach(() => { + jest.clearAllMocks(); +}); + // skip this until we figure out how to mock the audio loading -describe.skip("audio-button-response", () => { +describe("audio-button-response", () => { + it.skip("works with all defaults", async () => { + const { expectFinished, expectRunning, displayElement, getHTML } = await startTimeline([ + { + type: audioButtonResponse, + choices: ["choice1"], + stimulus: "foo.mp3", + }, + ]); + + expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + expectFinished(); + + await flushPromises(); + }); + it("works with use_webaudio:false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioButtonResponse, + choices: ["choice1"], + stimulus: "foo.mp3", + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); test("on_load event triggered after page setup complete", async () => { + const onLoadCallback = jest.fn(); + const timeline = [ { type: audioButtonResponse, @@ -15,9 +62,7 @@ describe.skip("audio-button-response", () => { prompt: "foo", choices: ["choice1"], on_load: () => { - expect(getHTML()).toContain("foo"); - - clickTarget(displayElement.querySelector("button")); + onLoadCallback(); }, }, ]; @@ -26,11 +71,190 @@ describe.skip("audio-button-response", () => { use_webaudio: false, }); - const { getHTML, finished, displayElement } = await startTimeline(timeline, jsPsych); + await startTimeline(timeline, jsPsych); + + expect(onLoadCallback).toHaveBeenCalled(); + }); + it("trial ends when button is clicked", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + prompt: "foo", + choices: ["choice1"], + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + + it("ends when trial_ends_after_audio is true and audio finishes", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + trial_duration: 30000, + trial_ends_after_audio: true, + }, + ], + jsPsych + ); + + await expectRunning(); + + jest.runAllTimers(); + + await expectFinished(); + }); + it("ends when trial_duration is shorter than the audio duration, stopping the audio", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + trial_duration: 500, + }, + ], + jsPsych + ); + + await expectRunning(); + + expect(mockStop).not.toHaveBeenCalled(); - expect(getHTML()).not.toContain("foo"); + jest.advanceTimersByTime(500); + + expect(mockStop).toHaveBeenCalled(); - await finished; + await expectFinished(); + }); + it("prevents responses when response_allowed_while_playing is false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement, getHTML } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + response_allowed_while_playing: false, + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectRunning(); + + jest.runAllTimers(); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + it("works when response_allowed_while_playing is true", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + response_allowed_while_playing: true, + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + it("does not allow reponses when response_allowed_while_playing is false and enable_button_after is set, until after set time", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + response_allowed_while_playing: false, + enable_button_after: 1000, + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectRunning(); + + jest.advanceTimersByTime(1000); + + clickTarget(displayElement.querySelector("button")); + + await expectRunning(); + + jest.advanceTimersByTime(1000); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + it("does not allow reponses when response_allowed_while_playing is true and enable_button_after is set, until after set time", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + response_allowed_while_playing: true, + enable_button_after: 500, + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectRunning(); + + jest.advanceTimersByTime(500); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); }); test("enable buttons during audio playback", async () => { @@ -49,7 +273,7 @@ describe.skip("audio-button-response", () => { use_webaudio: false, }); - const { getHTML, finished } = await startTimeline(timeline, jsPsych); + await startTimeline(timeline, jsPsych); const btns = document.querySelectorAll(".jspsych-html-button-response-button button"); diff --git a/packages/plugin-audio-button-response/src/index.ts b/packages/plugin-audio-button-response/src/index.ts index 628fddb0fe..954c7b6437 100644 --- a/packages/plugin-audio-button-response/src/index.ts +++ b/packages/plugin-audio-button-response/src/index.ts @@ -1,312 +1,306 @@ +import autoBind from "auto-bind"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { AudioPlayerInterface } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; +import { version } from "../package.json"; + const info = { name: "audio-button-response", + version: version, parameters: { - /** The audio to be played. */ + /** Path to audio file to be played. */ stimulus: { type: ParameterType.AUDIO, - pretty_name: "Stimulus", default: undefined, }, - /** Array containing the label(s) for the button(s). */ + /** Labels for the buttons. Each different string in the array will generate a different button. */ choices: { type: ParameterType.STRING, - pretty_name: "Choices", default: undefined, array: true, }, - /** The HTML for creating button. Can create own style. Use the "%choice%" string to indicate where the label from the choices parameter should be inserted. */ + /** + * A function that generates the HTML for each button in the `choices` array. The function gets the string + * and index of the item in the `choices` array and should return valid HTML. If you want to use different + * markup for each button, you can do that by using a conditional on either parameter. The default parameter + * returns a button element with the text label of the choice. + */ button_html: { - type: ParameterType.HTML_STRING, - pretty_name: "Button HTML", - default: '', - array: true, + type: ParameterType.FUNCTION, + default: function (choice: string, choice_index: number) { + return ``; + }, }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention + * is that it can be used to provide a reminder about the action the participant is supposed to take + * (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** The maximum duration to wait for a response. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the + * participant fails to make a response before this timer is reached, the participant's response will be + * recorded as null for the trial and the trial will end. If the value of this parameter is null, the trial + * will wait for a response indefinitely */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** Vertical margin of button. */ - margin_vertical: { + /** Setting to `'grid'` will make the container element have the CSS property `display: grid` and enable the + * use of `grid_rows` and `grid_columns`. Setting to `'flex'` will make the container element have the CSS + * property `display: flex`. You can customize how the buttons are laid out by adding inline CSS in the `button_html` parameter. + */ + button_layout: { type: ParameterType.STRING, - pretty_name: "Margin vertical", - default: "0px", + default: "grid", }, - /** Horizontal margin of button. */ - margin_horizontal: { - type: ParameterType.STRING, - pretty_name: "Margin horizontal", - default: "8px", + /** The number of rows in the button grid. Only applicable when `button_layout` is set to `'grid'`. If null, the + * number of rows will be determined automatically based on the number of buttons and the number of columns. + */ + grid_rows: { + type: ParameterType.INT, + default: 1, }, - /** If true, the trial will end when user makes a response. */ + /** The number of columns in the button grid. Only applicable when `button_layout` is set to `'grid'`. + * If null, the number of columns will be determined automatically based on the number of buttons and the + * number of rows. + */ + grid_columns: { + type: ParameterType.INT, + default: null, + }, + /** If true, then the trial will end whenever the participant makes a response (assuming they make their + * response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will + * continue until the value for `trial_duration` is reached. You can set this parameter to `false` to force + * the participant to listen to the stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** If true, then the trial will end as soon as the audio file finishes playing. */ + /** If true, then the trial will end as soon as the audio file finishes playing. */ trial_ends_after_audio: { type: ParameterType.BOOL, - pretty_name: "Trial ends after audio", default: false, }, /** - * If true, then responses are allowed while the audio is playing. - * If false, then the audio must finish playing before a response is accepted. + * If true, then responses are allowed while the audio is playing. If false, then the audio must finish + * playing before the button choices are enabled and a response is accepted. Once the audio has played + * all the way through, the buttons are enabled and a response is allowed (including while the audio is + * being re-played via on-screen playback controls). */ response_allowed_while_playing: { type: ParameterType.BOOL, - pretty_name: "Response allowed while playing", default: true, }, - /** The delay of enabling button */ + /** How long the button will delay enabling in milliseconds. If `response_allowed_while_playing` is `true`, + * the timer will start immediately. If it is `false`, the timer will start at the end of the audio. */ enable_button_after: { type: ParameterType.INT, - pretty_name: "Enable button after", default: 0, }, }, + data: { + /** The response time in milliseconds for the participant to make a response. The time is measured from + * when the stimulus first began playing until the participant's response.*/ + rt: { + type: ParameterType.INT, + }, + /** Indicates which button the participant pressed. The first button in the `choices` array is 0, the second is 1, and so on. */ + response: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **audio-button-response** - * - * jsPsych plugin for playing an audio file and getting a button response - * + * If the browser supports it, audio files are played using the WebAudio API. This allows for reasonably precise + * timing of the playback. The timing of responses generated is measured against the WebAudio specific clock, + * improving the measurement of response times. If the browser does not support the WebAudio API, then the audio file is + * played with HTML5 audio. + + * Audio files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if + * you are using timeline variables or another dynamic method to specify the audio stimulus, you will need + * to [manually preload](../overview/media-preloading.md#manual-preloading) the audio. + + * The trial can end when the participant responds, when the audio file has finished playing, or if the participant + * has failed to respond within a fixed length of time. You can also prevent a button response from being made before the + * audio has finished playing. + * * @author Kristin Diep - * @see {@link https://www.jspsych.org/plugins/jspsych-audio-button-response/ audio-button-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/audio-button-response/ audio-button-response plugin documentation on jspsych.org} */ class AudioButtonResponsePlugin implements JsPsychPlugin { static info = info; - private audio; - - constructor(private jsPsych: JsPsych) {} + private audio: AudioPlayerInterface; + private params: TrialType; + private buttonElements: HTMLElement[] = []; + private display: HTMLElement; + private response: { rt: number; button: number } = { rt: null, button: null }; + private context: AudioContext; + private startTime: number; + private trial_complete: (trial_data: { rt: number; stimulus: string; response: number }) => void; + + constructor(private jsPsych: JsPsych) { + autoBind(this); + } - trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { + async trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { // hold the .resolve() function from the Promise that ends the trial - let trial_complete; - + this.trial_complete; + this.params = trial; + this.display = display_element; // setup stimulus - var context = this.jsPsych.pluginAPI.audioContext(); - - // store response - var response = { - rt: null, - button: null, - }; - - // record webaudio context start time - var startTime; + this.context = this.jsPsych.pluginAPI.audioContext(); // load audio file - this.jsPsych.pluginAPI - .getAudioBuffer(trial.stimulus) - .then((buffer) => { - if (context !== null) { - this.audio = context.createBufferSource(); - this.audio.buffer = buffer; - this.audio.connect(context.destination); - } else { - this.audio = buffer; - this.audio.currentTime = 0; - } - setupTrial(); - }) - .catch((err) => { - console.error( - `Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.` - ); - console.error(err); - }); - - const setupTrial = () => { - // set up end event if trial needs it - if (trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", end_trial); - } - - // enable buttons after audio ends if necessary - if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", enable_buttons); - } - - //display buttons - var buttons = []; - if (Array.isArray(trial.button_html)) { - if (trial.button_html.length == trial.choices.length) { - buttons = trial.button_html; - } else { - console.error( - "Error in audio-button-response plugin. The length of the button_html array does not equal the length of the choices array" - ); - } - } else { - for (var i = 0; i < trial.choices.length; i++) { - buttons.push(trial.button_html); - } - } - - var html = '
'; - for (var i = 0; i < trial.choices.length; i++) { - var str = buttons[i].replace(/%choice%/g, trial.choices[i]); - html += - '
' + - str + - "
"; - } - html += "
"; - - //show prompt if there is one - if (trial.prompt !== null) { - html += trial.prompt; - } + this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus); - display_element.innerHTML = html; - - if (trial.response_allowed_while_playing) { - disable_buttons(); - enable_buttons(); - } else { - disable_buttons(); - } - - // start time - startTime = performance.now(); + // set up end event if trial needs it + if (trial.trial_ends_after_audio) { + this.audio.addEventListener("ended", this.end_trial); + } - // start audio - if (context !== null) { - startTime = context.currentTime; - this.audio.start(startTime); - } else { - this.audio.play(); - } + // enable buttons after audio ends if necessary + if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) { + this.audio.addEventListener("ended", this.enable_buttons); + } - // end trial if time limit is set - if (trial.trial_duration !== null) { - this.jsPsych.pluginAPI.setTimeout(() => { - end_trial(); - }, trial.trial_duration); + // Display buttons + const buttonGroupElement = document.createElement("div"); + buttonGroupElement.id = "jspsych-audio-button-response-btngroup"; + if (trial.button_layout === "grid") { + buttonGroupElement.classList.add("jspsych-btn-group-grid"); + if (trial.grid_rows === null && trial.grid_columns === null) { + throw new Error( + "You cannot set `grid_rows` to `null` without providing a value for `grid_columns`." + ); } + const n_cols = + trial.grid_columns === null + ? Math.ceil(trial.choices.length / trial.grid_rows) + : trial.grid_columns; + const n_rows = + trial.grid_rows === null + ? Math.ceil(trial.choices.length / trial.grid_columns) + : trial.grid_rows; + buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`; + buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`; + } else if (trial.button_layout === "flex") { + buttonGroupElement.classList.add("jspsych-btn-group-flex"); + } - on_load(); - }; + for (const [choiceIndex, choice] of trial.choices.entries()) { + buttonGroupElement.insertAdjacentHTML("beforeend", trial.button_html(choice, choiceIndex)); + const buttonElement = buttonGroupElement.lastChild as HTMLElement; + buttonElement.dataset.choice = choiceIndex.toString(); + buttonElement.addEventListener("click", () => { + this.after_response(choiceIndex); + }); + this.buttonElements.push(buttonElement); + } - // function to handle responses by the subject - function after_response(choice) { - // measure rt - var endTime = performance.now(); - var rt = Math.round(endTime - startTime); - if (context !== null) { - endTime = context.currentTime; - rt = Math.round((endTime - startTime) * 1000); - } - response.button = parseInt(choice); - response.rt = rt; + display_element.appendChild(buttonGroupElement); - // disable all the buttons after a response - disable_buttons(); + // Show prompt if there is one + if (trial.prompt !== null) { + display_element.insertAdjacentHTML("beforeend", trial.prompt); + } - if (trial.response_ends_trial) { - end_trial(); + if (trial.response_allowed_while_playing) { + if (trial.enable_button_after > 0) { + this.disable_buttons(); + this.enable_buttons(); } + } else { + this.disable_buttons(); } - // function to end trial when it is time - const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); + // start time + this.startTime = performance.now(); - // stop the audio file if it is playing - // remove end event listeners if they exist - if (context !== null) { - this.audio.stop(); - } else { - this.audio.pause(); - } + // end trial if time limit is set + if (trial.trial_duration !== null) { + this.jsPsych.pluginAPI.setTimeout(() => { + this.end_trial(); + }, trial.trial_duration); + } - this.audio.removeEventListener("ended", end_trial); - this.audio.removeEventListener("ended", enable_buttons); + on_load(); - // gather the data to store for the trial - var trial_data = { - rt: response.rt, - stimulus: trial.stimulus, - response: response.button, - }; + this.audio.play(); - // clear the display - display_element.innerHTML = ""; + return new Promise((resolve) => { + this.trial_complete = resolve; + }); + } - // move on to the next trial - this.jsPsych.finishTrial(trial_data); + private disable_buttons = () => { + for (const button of this.buttonElements) { + button.setAttribute("disabled", "disabled"); + } + }; - trial_complete(); - }; + private enable_buttons_without_delay = () => { + for (const button of this.buttonElements) { + button.removeAttribute("disabled"); + } + }; - const enable_buttons_with_delay = (delay: number) => { - this.jsPsych.pluginAPI.setTimeout(enable_buttons_without_delay, delay); - }; + private enable_buttons_with_delay = (delay: number) => { + this.jsPsych.pluginAPI.setTimeout(this.enable_buttons_without_delay, delay); + }; - function button_response(e) { - var choice = e.currentTarget.getAttribute("data-choice"); // don't use dataset for jsdom compatibility - after_response(choice); + private enable_buttons() { + if (this.params.enable_button_after > 0) { + this.enable_buttons_with_delay(this.params.enable_button_after); + } else { + this.enable_buttons_without_delay(); } + } - function disable_buttons() { - var btns = document.querySelectorAll(".jspsych-audio-button-response-button"); - for (var i = 0; i < btns.length; i++) { - var btn_el = btns[i].querySelector("button"); - if (btn_el) { - btn_el.disabled = true; - } - btns[i].removeEventListener("click", button_response); - } + // function to handle responses by the subject + private after_response = (choice) => { + // measure rt + var endTime = performance.now(); + var rt = Math.round(endTime - this.startTime); + if (this.context !== null) { + endTime = this.context.currentTime; + rt = Math.round((endTime - this.startTime) * 1000); } + this.response.button = parseInt(choice); + this.response.rt = rt; - function enable_buttons_without_delay() { - var btns = document.querySelectorAll(".jspsych-audio-button-response-button"); - for (var i = 0; i < btns.length; i++) { - var btn_el = btns[i].querySelector("button"); - if (btn_el) { - btn_el.disabled = false; - } - btns[i].addEventListener("click", button_response); - } - } + // disable all the buttons after a response + this.disable_buttons(); - function enable_buttons() { - if (trial.enable_button_after > 0) { - enable_buttons_with_delay(trial.enable_button_after); - } else { - enable_buttons_without_delay(); - } + if (this.params.response_ends_trial) { + this.end_trial(); } + }; + + // method to end trial when it is time + private end_trial = () => { + // stop the audio file if it is playing + this.audio.stop(); + + // remove end event listeners if they exist + this.audio.removeEventListener("ended", this.end_trial); + this.audio.removeEventListener("ended", this.enable_buttons); + + // gather the data to store for the trial + var trial_data = { + rt: this.response.rt, + stimulus: this.params.stimulus, + response: this.response.button, + }; - return new Promise((resolve) => { - trial_complete = resolve; - }); - } + // move on to the next trial + this.trial_complete(trial_data); + }; - simulate( + async simulate( trial: TrialType, simulation_mode, simulation_options: any, @@ -351,7 +345,9 @@ class AudioButtonResponsePlugin implements JsPsychPlugin { const respond = () => { if (data.rt !== null) { this.jsPsych.pluginAPI.clickTarget( - display_element.querySelector(`div[data-choice="${data.response}"] button`), + display_element.querySelector( + `#jspsych-audio-button-response-btngroup [data-choice="${data.response}"]` + ), data.rt ); } diff --git a/packages/plugin-audio-keyboard-response/src/index.spec.ts b/packages/plugin-audio-keyboard-response/src/index.spec.ts index cd5e073745..1ffa62fede 100644 --- a/packages/plugin-audio-keyboard-response/src/index.spec.ts +++ b/packages/plugin-audio-keyboard-response/src/index.spec.ts @@ -1,10 +1,131 @@ -import { pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils"; +jest.mock("../../jspsych/src/modules/plugin-api/AudioPlayer"); + +import { flushPromises, pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils"; import { initJsPsych } from "jspsych"; +//@ts-expect-error mock +import { mockStop } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; import audioKeyboardResponse from "."; jest.useFakeTimers(); +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe("audio-keyboard-response", () => { + // this relies on AudioContext, which we haven't mocked yet + it.skip("works with all defaults", async () => { + const { expectFinished, expectRunning } = await startTimeline([ + { + type: audioKeyboardResponse, + stimulus: "foo.mp3", + }, + ]); + + expectRunning(); + + pressKey("a"); + + expectFinished(); + + await flushPromises(); + }); + + it("works with use_webaudio:false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioKeyboardResponse, + stimulus: "foo.mp3", + }, + ], + jsPsych + ); + + await expectRunning(); + pressKey("a"); + await expectFinished(); + }); + + it("ends when trial_ends_after_audio is true and audio finishes", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioKeyboardResponse, + stimulus: "foo.mp3", + trial_ends_after_audio: true, + }, + ], + jsPsych + ); + + await expectRunning(); + + jest.runAllTimers(); + + await expectFinished(); + }); + + it("prevents responses when response_allowed_while_playing is false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioKeyboardResponse, + stimulus: "foo.mp3", + response_allowed_while_playing: false, + }, + ], + jsPsych + ); + + await expectRunning(); + + pressKey("a"); + + await expectRunning(); + + jest.runAllTimers(); + + await expectRunning(); + + pressKey("a"); + + await expectFinished(); + }); + + it("ends when trial_duration is shorter than the audio duration, stopping the audio", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioKeyboardResponse, + stimulus: "foo.mp3", + trial_duration: 500, + }, + ], + jsPsych + ); + + await expectRunning(); + + expect(mockStop).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(500); + + expect(mockStop).toHaveBeenCalled(); + + await expectFinished(); + }); +}); + describe("audio-keyboard-response simulation", () => { test("data mode works", async () => { const timeline = [ @@ -22,8 +143,7 @@ describe("audio-keyboard-response simulation", () => { expect(typeof getData().values()[0].response).toBe("string"); }); - // can't run this until we mock Audio elements. - test.skip("visual mode works", async () => { + test("visual mode works", async () => { const jsPsych = initJsPsych({ use_webaudio: false }); const timeline = [ diff --git a/packages/plugin-audio-keyboard-response/src/index.ts b/packages/plugin-audio-keyboard-response/src/index.ts index aaae336b27..50544500bd 100644 --- a/packages/plugin-audio-keyboard-response/src/index.ts +++ b/packages/plugin-audio-keyboard-response/src/index.ts @@ -1,36 +1,53 @@ +import autoBind from "auto-bind"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { AudioPlayerInterface } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; +import { version } from "../package.json"; + const info = { name: "audio-keyboard-response", + version: version, parameters: { /** The audio file to be played. */ stimulus: { type: ParameterType.AUDIO, - pretty_name: "Stimulus", default: undefined, }, - /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */ + /** This array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. + * Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - + * see [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) + * and [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/) + * for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"ALL_KEYS"` + * means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean that no responses are allowed. + */ choices: { type: ParameterType.KEYS, - pretty_name: "Choices", default: "ALL_KEYS", }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that + * it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). + */ prompt: { type: ParameterType.HTML_STRING, pretty_name: "Prompt", default: null, }, - /** The maximum duration to wait for a response. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the + * participant fails to make a response before this timer is reached, the participant's response will be + * recorded as null for the trial and the trial will end. If the value of this parameter is null, then the + * trial will wait for a response indefinitely. + */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** If true, the trial will end when user makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their + * response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will + * continue until the value for `trial_duration` is reached. You can use set this parameter to `false` to + * force the participant to listen to the stimulus for a fixed amount of time, even if they respond before the time is complete + */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, /** If true, then the trial will end as soon as the audio file finishes playing. */ @@ -39,72 +56,77 @@ const info = { pretty_name: "Trial ends after audio", default: false, }, - /** If true, then responses are allowed while the audio is playing. If false, then the audio must finish playing before a response is accepted. */ + /** If true, then responses are allowed while the audio is playing. If false, then the audio must finish + * playing before a keyboard response is accepted. Once the audio has played all the way through, a valid + * keyboard response is allowed (including while the audio is being re-played via on-screen playback controls). + */ response_allowed_while_playing: { type: ParameterType.BOOL, - pretty_name: "Response allowed while playing", default: true, }, }, + data: { + /** Indicates which key the participant pressed. If no key was pressed before the trial ended, then the value will be `null`. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus + * first began playing until the participant made a key response. If no key was pressed before the trial ended, then the + * value will be `null`. + */ + rt: { + type: ParameterType.INT, + }, + /** Path to the audio file that played during the trial. */ + stimulus: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * **audio-keyboard-response** + * This plugin plays audio files and records responses generated with the keyboard. + * + * If the browser supports it, audio files are played using the WebAudio API. This allows for reasonably precise timing of the + * playback. The timing of responses generated is measured against the WebAudio specific clock, improving the measurement of + * response times. If the browser does not support the WebAudio API, then the audio file is played with HTML5 audio. + * + * Audio files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using + * timeline variables or another dynamic method to specify the audio stimulus, then you will need to [manually preload](../overview/media-preloading.md#manual-preloading) the audio. * - * jsPsych plugin for playing an audio file and getting a keyboard response + * The trial can end when the participant responds, when the audio file has finished playing, or if the participant has + * failed to respond within a fixed length of time. You can also prevent a keyboard response from being recorded before + * the audio has finished playing. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-audio-keyboard-response/ audio-keyboard-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/audio-keyboard-response/ audio-keyboard-response plugin documentation on jspsych.org} */ class AudioKeyboardResponsePlugin implements JsPsychPlugin { static info = info; - private audio; - - constructor(private jsPsych: JsPsych) {} + private audio: AudioPlayerInterface; + private params: TrialType; + private display: HTMLElement; + private response: { rt: number; key: string } = { rt: null, key: null }; + private startTime: number; + private finish: ({}: { rt: number; response: string; stimulus: string }) => void; + + constructor(private jsPsych: JsPsych) { + autoBind(this); + } trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { - // hold the .resolve() function from the Promise that ends the trial - let trial_complete; + return new Promise(async (resolve) => { + this.finish = resolve; + this.params = trial; + this.display = display_element; + // load audio file + this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus); - // setup stimulus - var context = this.jsPsych.pluginAPI.audioContext(); - - // store response - var response = { - rt: null, - key: null, - }; - - // record webaudio context start time - var startTime; - - // load audio file - this.jsPsych.pluginAPI - .getAudioBuffer(trial.stimulus) - .then((buffer) => { - if (context !== null) { - this.audio = context.createBufferSource(); - this.audio.buffer = buffer; - this.audio.connect(context.destination); - } else { - this.audio = buffer; - this.audio.currentTime = 0; - } - setupTrial(); - }) - .catch((err) => { - console.error( - `Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.` - ); - console.error(err); - }); - - const setupTrial = () => { // set up end event if trial needs it if (trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", end_trial); + this.audio.addEventListener("ended", this.end_trial); } // show prompt if there is one @@ -112,107 +134,91 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin { display_element.innerHTML = trial.prompt; } - // start audio - if (context !== null) { - startTime = context.currentTime; - this.audio.start(startTime); - } else { - this.audio.play(); - } + // start playing audio here to record time + // use this for offsetting RT measurement in + // setup_keyboard_listener + this.startTime = this.jsPsych.pluginAPI.audioContext()?.currentTime; // start keyboard listener when trial starts or sound ends if (trial.response_allowed_while_playing) { - setup_keyboard_listener(); + this.setup_keyboard_listener(); } else if (!trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", setup_keyboard_listener); + this.audio.addEventListener("ended", this.setup_keyboard_listener); } // end trial if time limit is set if (trial.trial_duration !== null) { this.jsPsych.pluginAPI.setTimeout(() => { - end_trial(); + this.end_trial(); }, trial.trial_duration); } + // call trial on_load method because we are done with all loading setup on_load(); - }; - - // function to end trial when it is time - const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // stop the audio file if it is playing - // remove end event listeners if they exist - if (context !== null) { - this.audio.stop(); - } else { - this.audio.pause(); - } - - this.audio.removeEventListener("ended", end_trial); - this.audio.removeEventListener("ended", setup_keyboard_listener); + this.audio.play(); + }); + } - // kill keyboard listeners - this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); + private end_trial() { + // kill any remaining setTimeout handlers + this.jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial - var trial_data = { - rt: response.rt, - stimulus: trial.stimulus, - response: response.key, - }; + // stop the audio file if it is playing + this.audio.stop(); - // clear the display - display_element.innerHTML = ""; + // remove end event listeners if they exist + this.audio.removeEventListener("ended", this.end_trial); + this.audio.removeEventListener("ended", this.setup_keyboard_listener); - // move on to the next trial - this.jsPsych.finishTrial(trial_data); + // kill keyboard listeners + this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); - trial_complete(); + // gather the data to store for the trial + var trial_data = { + rt: this.response.rt, + response: this.response.key, + stimulus: this.params.stimulus, }; - // function to handle responses by the subject - function after_response(info) { - // only record the first response - if (response.key == null) { - response = info; - } + // clear the display + this.display.innerHTML = ""; - if (trial.response_ends_trial) { - end_trial(); - } - } + // move on to the next trial + this.finish(trial_data); + } - const setup_keyboard_listener = () => { - // start the response listener - if (context !== null) { - this.jsPsych.pluginAPI.getKeyboardResponse({ - callback_function: after_response, - valid_responses: trial.choices, - rt_method: "audio", - persist: false, - allow_held_key: false, - audio_context: context, - audio_context_start_time: startTime, - }); - } else { - this.jsPsych.pluginAPI.getKeyboardResponse({ - callback_function: after_response, - valid_responses: trial.choices, - rt_method: "performance", - persist: false, - allow_held_key: false, - }); - } - }; + private after_response(info: { key: string; rt: number }) { + this.response = info; + if (this.params.response_ends_trial) { + this.end_trial(); + } + } - return new Promise((resolve) => { - trial_complete = resolve; - }); + private setup_keyboard_listener() { + // start the response listener + if (this.jsPsych.pluginAPI.useWebaudio) { + this.jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: this.after_response, + valid_responses: this.params.choices, + rt_method: "audio", + persist: false, + allow_held_key: false, + audio_context: this.jsPsych.pluginAPI.audioContext(), + audio_context_start_time: this.startTime, + }); + } else { + this.jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: this.after_response, + valid_responses: this.params.choices, + rt_method: "performance", + persist: false, + allow_held_key: false, + }); + } } - simulate( + async simulate( trial: TrialType, simulation_mode, simulation_options: any, @@ -220,20 +226,24 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin { ) { if (simulation_mode == "data-only") { load_callback(); - this.simulate_data_only(trial, simulation_options); + return this.simulate_data_only(trial, simulation_options); } if (simulation_mode == "visual") { - this.simulate_visual(trial, simulation_options, load_callback); + return 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); + return data; } - private simulate_visual(trial: TrialType, simulation_options, load_callback: () => void) { + private async simulate_visual( + trial: TrialType, + simulation_options, + load_callback: () => void + ) { const data = this.create_simulation_data(trial, simulation_options); const display_element = this.jsPsych.getDisplayElement(); @@ -244,7 +254,7 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin { } }; - this.trial(display_element, trial, () => { + const result = await this.trial(display_element, trial, () => { load_callback(); if (!trial.response_allowed_while_playing) { this.audio.addEventListener("ended", respond); @@ -252,6 +262,8 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin { respond(); } }); + + return result; } private create_simulation_data(trial: TrialType, simulation_options) { diff --git a/packages/plugin-audio-slider-response/src/index.spec.ts b/packages/plugin-audio-slider-response/src/index.spec.ts index 180214d9cb..c9d8b75827 100644 --- a/packages/plugin-audio-slider-response/src/index.spec.ts +++ b/packages/plugin-audio-slider-response/src/index.spec.ts @@ -1,10 +1,140 @@ -import { pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils"; +jest.mock("../../jspsych/src/modules/plugin-api/AudioPlayer"); + +import { + clickTarget, + flushPromises, + pressKey, + simulateTimeline, + startTimeline, +} from "@jspsych/test-utils"; import { initJsPsych } from "jspsych"; +//@ts-expect-error mock +import { mockStop } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; import audioSliderResponse from "."; jest.useFakeTimers(); +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe("audio-slider-response", () => { + // this relies on AudioContext, which we haven't mocked yet + it.skip("works with all defaults", async () => { + const { expectFinished, expectRunning } = await startTimeline([ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + }, + ]); + + expectRunning(); + + pressKey("a"); + + expectFinished(); + + await flushPromises(); + }); + + it("works with use_webaudio:false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement, getHTML } = await startTimeline( + [ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + }, + ], + jsPsych + ); + + await expectRunning(); + + //jest.runAllTimers(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + + it("ends when trial_ends_after_audio is true and audio finishes", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + trial_ends_after_audio: true, + }, + ], + jsPsych + ); + + await expectRunning(); + + jest.runAllTimers(); + + await expectFinished(); + }); + + it("prevents responses when response_allowed_while_playing is false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + response_allowed_while_playing: false, + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectRunning(); + + jest.runAllTimers(); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + + it("ends when trial_duration is shorter than the audio duration, stopping the audio", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + trial_duration: 500, + }, + ], + jsPsych + ); + + await expectRunning(); + + expect(mockStop).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(500); + + expect(mockStop).toHaveBeenCalled(); + + await expectFinished(); + }); +}); describe("audio-slider-response simulation", () => { test("data mode works", async () => { const timeline = [ diff --git a/packages/plugin-audio-slider-response/src/index.ts b/packages/plugin-audio-slider-response/src/index.ts index 08e400897b..2a803a74c2 100644 --- a/packages/plugin-audio-slider-response/src/index.ts +++ b/packages/plugin-audio-slider-response/src/index.ts @@ -1,347 +1,367 @@ +import autoBind from "auto-bind"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { AudioPlayerInterface } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; +import { version } from "../package.json"; + const info = { name: "audio-slider-response", + version: version, parameters: { - /** The audio file to be played. */ + /** Audio file to be played. */ stimulus: { type: ParameterType.AUDIO, - pretty_name: "Stimulus", default: undefined, }, /** Sets the minimum value of the slider. */ min: { type: ParameterType.INT, - pretty_name: "Min slider", default: 0, }, /** Sets the maximum value of the slider */ max: { type: ParameterType.INT, - pretty_name: "Max slider", default: 100, }, /** Sets the starting value of the slider */ slider_start: { type: ParameterType.INT, - pretty_name: "Slider starting value", default: 50, }, - /** Sets the step of the slider */ + /** Sets the step of the slider. This is the smallest amount by which the slider can change. */ step: { type: ParameterType.INT, - pretty_name: "Step", default: 1, }, - /** Array containing the labels for the slider. Labels will be displayed at equidistant locations along the slider. */ + /** Labels displayed at equidistant locations on the slider. For example, two labels will be placed at the ends of the + * slider. Three labels would place two at the ends and one in the middle. Four will place two at the ends, and the + * other two will be at 33% and 67% of the slider width. + */ labels: { type: ParameterType.HTML_STRING, - pretty_name: "Labels", default: [], array: true, }, - /** Width of the slider in pixels. */ + /** Set the width of the slider in pixels. If left null, then the width will be equal to the widest element in the display. */ slider_width: { type: ParameterType.INT, - pretty_name: "Slider width", default: null, }, - /** Label of the button to advance. */ + /** Label of the button to end the trial. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", array: false, }, - /** If true, the participant will have to move the slider before continuing. */ + /** If true, the participant must move the slider before clicking the continue button. */ require_movement: { type: ParameterType.BOOL, - pretty_name: "Require movement", default: false, }, - /** Any content here will be displayed below the slider. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is + * that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). + */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to show the trial. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If + * the participant fails to make a response before this timer is reached, the participant's response will be + * recorded as null for the trial and the trial will end. If the value of this parameter is null, then the trial + * will wait for a response indefinitely. + */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** If true, trial will end when user makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response + * before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the + * value for `trial_duration` is reached. You can set this parameter to `false` to force the participant to listen to + * the stimulus for a fixed amount of time, even if they respond before the time is complete. + */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, /** If true, then the trial will end as soon as the audio file finishes playing. */ trial_ends_after_audio: { type: ParameterType.BOOL, - pretty_name: "Trial ends after audio", default: false, }, - /** If true, then responses are allowed while the audio is playing. If false, then the audio must finish playing before a response is accepted. */ + /** If true, then responses are allowed while the audio is playing. If false, then the audio must finish playing before + * the slider is enabled and the trial can end via the next button click. Once the audio has played all the way through, + * the slider is enabled and a response is allowed (including while the audio is being re-played via on-screen playback controls). + */ response_allowed_while_playing: { type: ParameterType.BOOL, - pretty_name: "Response allowed while playing", default: true, }, }, + data: { + /** The numeric value of the slider. */ + response: { + type: ParameterType.INT, + }, + /** The time in milliseconds for the participant to make a response. The time is measured from when the stimulus first + * began playing until the participant's response. + */ + rt: { + type: ParameterType.INT, + }, + /** The path of the audio file that was played. */ + stimulus: { + type: ParameterType.STRING, + }, + /** The starting value of the slider. */ + slider_start: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **audio-slider-response** + * This plugin plays an audio file and allows the participant to respond by dragging a slider. + * + * If the browser supports it, audio files are played using the WebAudio API. This allows for reasonably precise timing of the + * playback. The timing of responses generated is measured against the WebAudio specific clock, improving the measurement of + * response times. If the browser does not support the WebAudio API, then the audio file is played with HTML5 audio. * - * jsPsych plugin for playing audio and getting a slider response + * Audio files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using + * timeline variables or another dynamic method to specify the audio stimulus, then you will need + * to [manually preload](../overview/media-preloading.md#manual-preloading) the audio. * + * The trial can end when the participant responds, or if the participant has failed to respond within a fixed length of time. You can also prevent the slider response from being made before the audio has finished playing. * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-audio-slider-response/ audio-slider-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/audio-slider-response/ audio-slider-response plugin documentation on jspsych.org} */ class AudioSliderResponsePlugin implements JsPsychPlugin { static info = info; - private audio; - - constructor(private jsPsych: JsPsych) {} - - trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { - // hold the .resolve() function from the Promise that ends the trial - let trial_complete; + private audio: AudioPlayerInterface; + private context: AudioContext; + private params: TrialType; + private display: HTMLElement; + private response: { rt: number; response: number } = { rt: null, response: null }; + private startTime: number; + private half_thumb_width: number; + private trial_complete: (trial_data: { + rt: number; + slider_start: number; + response: number; + }) => void; + + constructor(private jsPsych: JsPsych) { + autoBind(this); + } + async trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { + // record webaudio context start time + this.startTime; + this.params = trial; + this.display = display_element; + // for storing data related to response + this.response; // half of the thumb width value from jspsych.css, used to adjust the label positions - var half_thumb_width = 7.5; + this.half_thumb_width = 7.5; + // hold the .resolve() function from the Promise that ends the trial + this.trial_complete; // setup stimulus - var context = this.jsPsych.pluginAPI.audioContext(); + this.context = this.jsPsych.pluginAPI.audioContext(); - // record webaudio context start time - var startTime; + // load audio file + this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus); - // for storing data related to response - var response; + this.setupTrial(); - // load audio file - this.jsPsych.pluginAPI - .getAudioBuffer(trial.stimulus) - .then((buffer) => { - if (context !== null) { - this.audio = context.createBufferSource(); - this.audio.buffer = buffer; - this.audio.connect(context.destination); - } else { - this.audio = buffer; - this.audio.currentTime = 0; - } - setupTrial(); - }) - .catch((err) => { - console.error( - `Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.` - ); - console.error(err); - }); + on_load(); - const setupTrial = () => { - // set up end event if trial needs it - if (trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", end_trial); - } + return new Promise((resolve) => { + this.trial_complete = resolve; + }); + } - // enable slider after audio ends if necessary - if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", enable_slider); - } + // to enable slider after audio ends + private enable_slider() { + document.querySelector("#jspsych-audio-slider-response-response").disabled = + false; + if (!this.params.require_movement) { + document.querySelector("#jspsych-audio-slider-response-next").disabled = + false; + } + } + + private setupTrial = () => { + // set up end event if trial needs it + if (this.params.trial_ends_after_audio) { + this.audio.addEventListener("ended", this.end_trial); + } + + // enable slider after audio ends if necessary + if (!this.params.response_allowed_while_playing && !this.params.trial_ends_after_audio) { + this.audio.addEventListener("ended", this.enable_slider); + } - var html = '
'; + var html = '
'; + html += + '
'; html += - ''; - html += '' + trial.labels[j] + ""; - html += "
"; - } - html += "
"; + '' + this.params.labels[j] + ""; html += "
"; - html += "
"; - - if (trial.prompt !== null) { - html += trial.prompt; - } + } + html += ""; + html += ""; + html += ""; - // add submit button - var next_disabled_attribute = ""; - if (trial.require_movement || !trial.response_allowed_while_playing) { - next_disabled_attribute = "disabled"; - } - html += - '"; + if (this.params.prompt !== null) { + html += this.params.prompt; + } - display_element.innerHTML = html; + // add submit button + var next_disabled_attribute = ""; + if (this.params.require_movement || !this.params.response_allowed_while_playing) { + next_disabled_attribute = "disabled"; + } + html += + '"; + + this.display.innerHTML = html; + + this.response = { + rt: null, + response: null, + }; - response = { - rt: null, - response: null, - }; + if (!this.params.response_allowed_while_playing) { + this.display.querySelector( + "#jspsych-audio-slider-response-response" + ).disabled = true; + this.display.querySelector("#jspsych-audio-slider-response-next").disabled = + true; + } - if (!trial.response_allowed_while_playing) { - display_element.querySelector( - "#jspsych-audio-slider-response-response" - ).disabled = true; - display_element.querySelector( + if (this.params.require_movement) { + const enable_button = () => { + this.display.querySelector( "#jspsych-audio-slider-response-next" - ).disabled = true; - } + ).disabled = false; + }; - if (trial.require_movement) { - const enable_button = () => { - display_element.querySelector( - "#jspsych-audio-slider-response-next" - ).disabled = false; - }; + this.display + .querySelector("#jspsych-audio-slider-response-response") + .addEventListener("mousedown", enable_button); - display_element - .querySelector("#jspsych-audio-slider-response-response") - .addEventListener("mousedown", enable_button); + this.display + .querySelector("#jspsych-audio-slider-response-response") + .addEventListener("touchstart", enable_button); - display_element - .querySelector("#jspsych-audio-slider-response-response") - .addEventListener("touchstart", enable_button); + this.display + .querySelector("#jspsych-audio-slider-response-response") + .addEventListener("change", enable_button); + } - display_element - .querySelector("#jspsych-audio-slider-response-response") - .addEventListener("change", enable_button); - } + this.display + .querySelector("#jspsych-audio-slider-response-next") + .addEventListener("click", () => { + // measure response time + var endTime = performance.now(); + var rt = Math.round(endTime - this.startTime); + if (this.context !== null) { + endTime = this.context.currentTime; + rt = Math.round((endTime - this.startTime) * 1000); + } + this.response.rt = rt; + this.response.response = this.display.querySelector( + "#jspsych-audio-slider-response-response" + ).valueAsNumber; - display_element - .querySelector("#jspsych-audio-slider-response-next") - .addEventListener("click", () => { - // measure response time - var endTime = performance.now(); - var rt = Math.round(endTime - startTime); - if (context !== null) { - endTime = context.currentTime; - rt = Math.round((endTime - startTime) * 1000); - } - response.rt = rt; - response.response = display_element.querySelector( - "#jspsych-audio-slider-response-response" - ).valueAsNumber; - - if (trial.response_ends_trial) { - end_trial(); - } else { - display_element.querySelector( - "#jspsych-audio-slider-response-next" - ).disabled = true; - } - }); - - startTime = performance.now(); - // start audio - if (context !== null) { - startTime = context.currentTime; - this.audio.start(startTime); - } else { - this.audio.play(); - } + if (this.params.response_ends_trial) { + this.end_trial(); + } else { + this.display.querySelector( + "#jspsych-audio-slider-response-next" + ).disabled = true; + } + }); - // end trial if trial_duration is set - if (trial.trial_duration !== null) { - this.jsPsych.pluginAPI.setTimeout(() => { - end_trial(); - }, trial.trial_duration); - } + this.startTime = performance.now(); - on_load(); - }; + // start audio + this.audio.play(); - // function to enable slider after audio ends - function enable_slider() { - document.querySelector("#jspsych-audio-slider-response-response").disabled = - false; - if (!trial.require_movement) { - document.querySelector("#jspsych-audio-slider-response-next").disabled = - false; - } + // end trial if trial_duration is set + if (this.params.trial_duration !== null) { + this.jsPsych.pluginAPI.setTimeout(() => { + this.end_trial(); + }, this.params.trial_duration); } + }; - const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); + private end_trial = () => { + // kill any remaining setTimeout handlers + this.jsPsych.pluginAPI.clearAllTimeouts(); - // stop the audio file if it is playing - // remove end event listeners if they exist - if (context !== null) { - this.audio.stop(); - } else { - this.audio.pause(); - } - - this.audio.removeEventListener("ended", end_trial); - this.audio.removeEventListener("ended", enable_slider); - - // save data - var trialdata = { - rt: response.rt, - stimulus: trial.stimulus, - slider_start: trial.slider_start, - response: response.response, - }; + // stop the audio file if it is playing + this.audio.stop(); - display_element.innerHTML = ""; + // remove end event listeners if they exist + this.audio.removeEventListener("ended", this.end_trial); + this.audio.removeEventListener("ended", this.enable_slider); - // next trial - this.jsPsych.finishTrial(trialdata); - - trial_complete(); + // save data + var trialdata = { + rt: this.response.rt, + stimulus: this.params.stimulus, + slider_start: this.params.slider_start, + response: this.response.response, }; - return new Promise((resolve) => { - trial_complete = resolve; - }); - } + this.display.innerHTML = ""; + + // next trial + this.trial_complete(trialdata); + }; simulate( trial: TrialType, diff --git a/packages/plugin-browser-check/src/index.spec.ts b/packages/plugin-browser-check/src/index.spec.ts index b3e5296e6c..5e70bc028e 100644 --- a/packages/plugin-browser-check/src/index.spec.ts +++ b/packages/plugin-browser-check/src/index.spec.ts @@ -126,7 +126,7 @@ describe("browser-check", () => { expect(getHTML()).toMatch("1200"); expect(getHTML()).toMatch("1000"); - clickTarget(displayElement.querySelector("button")); + await clickTarget(displayElement.querySelector("button")); jest.runAllTimers(); @@ -152,7 +152,7 @@ describe("browser-check", () => { expect(displayElement.querySelector("button").innerHTML).toMatch("foo"); - clickTarget(displayElement.querySelector("button")); + await clickTarget(displayElement.querySelector("button")); jest.runAllTimers(); diff --git a/packages/plugin-browser-check/src/index.ts b/packages/plugin-browser-check/src/index.ts index 912cc18900..1613fe3a5a 100644 --- a/packages/plugin-browser-check/src/index.ts +++ b/packages/plugin-browser-check/src/index.ts @@ -1,11 +1,14 @@ import { detect } from "detect-browser"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "browser-check", + version: version, parameters: { /** - * List of features to check and record in the data + * The list of browser features to record. The default value includes all of the available options. */ features: { type: ParameterType.STRING, @@ -25,8 +28,7 @@ const info = { ], }, /** - * Any features listed here will be skipped, even if they appear in `features`. Useful for - * when you want to run most of the defaults. + * Any features listed here will be skipped, even if they appear in `features`. Use this when you want to run most of the defaults. */ skip_features: { type: ParameterType.STRING, @@ -34,38 +36,52 @@ const info = { default: [], }, /** - * The number of animation frames to sample when calculating vsync_rate. + * The number of frames to sample when measuring the display refresh rate (`"vsync_rate"`). + * Increasing the number will potenially improve the stability of the estimate at the cost of + * increasing the amount of time the plugin takes during this test. On most devices, 60 frames takes + * about 1 second to measure. */ vsync_frame_count: { type: ParameterType.INT, default: 60, }, /** - * If `true`, show a message when window size is too small to allow the user - * to adjust if their screen allows for it. + * Whether to allow the participant to resize the browser window if the window is smaller than `minimum_width` + * and/or `minimum_height`. If `false`, then the `minimum_width` and `minimum_height` parameters are ignored + * and you can validate the size in the `inclusion_function`. */ allow_window_resize: { type: ParameterType.BOOL, default: true, }, /** - * When `allow_window_resize` is `true`, this is the minimum width (px) that the window - * needs to be before the experiment will continue. + * If `allow_window_resize` is `true`, then this is the minimum width of the window (in pixels) + * that must be met before continuing. */ minimum_width: { type: ParameterType.INT, default: 0, }, /** - * When `allow_window_resize` is `true`, this is the minimum height (px) that the window - * needs to be before the experiment will continue. + * If `allow_window_resize` is `true`, then this is the minimum height of the window (in pixels) that + * must be met before continuing. */ minimum_height: { type: ParameterType.INT, default: 0, }, /** - * Message to display during interactive window resizing. + * The message that will be displayed during the interactive resize when `allow_window_resize` is `true` + * and the window is too small. If the message contains HTML elements with the special IDs `browser-check-min-width`, + * `browser-check-min-height`, `browser-check-actual-height`, and/or `browser-check-actual-width`, then the + * contents of those elements will be dynamically updated to reflect the `minimum_width`, `minimum_height` and + * measured width and height of the browser. + * The default message is: + * `

Your browser window is too small to complete this experiment. Please maximize the size of your browser window. If your browser window is already maximized, you will not be able to complete this experiment.

+ *

The minimum window width is px.

+ *

Your current window width is px.

+ *

The minimum window height is px.

+ *

Your current window height is px.

`. */ window_resize_message: { type: ParameterType.HTML_STRING, @@ -99,7 +115,10 @@ const info = { }, }, /** - * The message to display if `inclusion_function` returns `false` + * A function that returns the message to display if `inclusion_function` evaluates to `false` or if the participant + * clicks on the resize fail button during the interactive resize. In order to allow customization of the message, + * the first argument to the function will be an object containing key value pairs with the measured features of the + * browser. The keys will be the same as those listed in `features`. See example below. */ exclusion_message: { type: ParameterType.FUNCTION, @@ -108,17 +127,84 @@ const info = { }, }, }, + data: { + /** The width of the browser window in pixels. If interactive resizing happens, this is the width *after* resizing. */ + width: { + type: ParameterType.INT, + }, + /** The height of the browser window in pixels. If interactive resizing happens, this is the height *after* resizing.*/ + height: { + type: ParameterType.INT, + }, + /** The browser used. */ + browser: { + type: ParameterType.STRING, + }, + /** The version of the browser used. */ + browser_version: { + type: ParameterType.STRING, + }, + /** The operating system used. */ + os: { + type: ParameterType.STRING, + }, + /** Whether the browser is a mobile device. */ + mobile: { + type: ParameterType.BOOL, + }, + /** Whether the browser supports the WebAudio API. */ + webaudio: { + type: ParameterType.BOOL, + }, + /** Whether the browser supports the Fullscreen API. */ + fullscreen: { + type: ParameterType.BOOL, + }, + /** An estimate of the refresh rate of the screen, in frames per second. */ + vsync_rate: { + type: ParameterType.FLOAT, + }, + /** Whether there is a webcam device available. Note that the participant still must grant permission to access the device before it can be used. */ + webcam: { + type: ParameterType.BOOL, + }, + /** Whether there is an audio input device available. Note that the participant still must grant permission to access the device before it can be used. */ + microphone: { + type: ParameterType.BOOL, + }, + }, }; type Info = typeof info; /** - * **browser-check** + * This plugin measures and records various features of the participant's browser and can end the experiment if defined inclusion criteria are not met. + * + * The plugin currently can record the following features: * - * jsPsych plugin for checking features of the browser and validating against a set of inclusion criteria. + * The width and height of the browser window in pixels. + * The type of browser used (e.g., Chrome, Firefox, Edge, etc.) and the version number of the browser.* + * Whether the participant is using a mobile device.* + * The operating system.* + * Support for the WebAudio API. + * Support for the Fullscreen API, e.g., through the [fullscreen plugin](../plugins/fullscreen.md). + * The display refresh rate in frames per second. + * Whether the device has a webcam and microphone. Note that this only reveals whether a webcam/microphone exists. The participant still needs to grant permission in order for the experiment to use these devices. * + * !!! warning + * Features with an * are recorded by parsing the [user agent string](https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent). + * This method is accurate most of the time, but is not guaranteed to be correct. + * The plugin uses the [detect-browser package](https://github.com/DamonOehlman/detect-browser) to perform user agent parsing. + * You can find a list of supported browsers and OSes in the [source file](https://github.com/DamonOehlman/detect-browser/blob/master/src/index.ts). + * + * The plugin begins by measuring the set of features requested. + * An inclusion function is evaluated to see if the paricipant passes the inclusion criteria. + * If they do, then the trial ends and the experiment continues. + * If they do not, then the experiment ends immediately. + * If a minimum width and/or minimum height is desired, the plugin will optionally display a message to participants whose browser windows are too small to give them an opportunity to make the window larger if possible. + * See the examples below for more guidance. * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-browser-check/ browser-check plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/browser-check/ browser-check plugin documentation on jspsych.org} */ class BrowserCheckPlugin implements JsPsychPlugin { static info = info; @@ -361,8 +447,6 @@ class BrowserCheckPlugin implements JsPsychPlugin { } private end_trial(feature_data) { - this.jsPsych.getDisplayElement().innerHTML = ""; - const trial_data = { ...Object.fromEntries(feature_data) }; this.jsPsych.finishTrial(trial_data); @@ -373,7 +457,7 @@ class BrowserCheckPlugin implements JsPsychPlugin { const trial_data = { ...Object.fromEntries(feature_data) }; - this.jsPsych.endExperiment(this.t.exclusion_message(trial_data), trial_data); + this.jsPsych.abortExperiment(this.t.exclusion_message(trial_data), trial_data); } simulate( @@ -429,7 +513,7 @@ class BrowserCheckPlugin implements JsPsychPlugin { if (trial.inclusion_function(data)) { this.jsPsych.finishTrial(data); } else { - this.jsPsych.endExperiment(trial.exclusion_message(data), data); + this.jsPsych.abortExperiment(trial.exclusion_message(data), data); } }); } diff --git a/packages/plugin-call-function/src/index.ts b/packages/plugin-call-function/src/index.ts index 6371aceaeb..90ba161097 100644 --- a/packages/plugin-call-function/src/index.ts +++ b/packages/plugin-call-function/src/index.ts @@ -1,32 +1,43 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "call-function", + version: version, parameters: { - /** Function to call */ + /** The function to call. */ func: { type: ParameterType.FUNCTION, - pretty_name: "Function", default: undefined, }, - /** Is the function call asynchronous? */ + /** Set to true if `func` is an asynchoronous function. If this is true, then the first argument passed to `func` + * will be a callback that you should call when the async operation is complete. You can pass data to the callback. + * See example below. + */ async: { type: ParameterType.BOOL, - pretty_name: "Asynchronous", default: false, }, }, + data: { + /** The return value of the called function. */ + value: { + type: ParameterType.COMPLEX, + default: undefined, + }, + }, }; type Info = typeof info; /** - * **call-function** + * This plugin executes a specified function. This allows the experimenter to run arbitrary code at any point during the experiment. * - * jsPsych plugin for calling an arbitrary function during a jsPsych experiment + * The function cannot take any arguments. If arguments are needed, then an anonymous function should be used to wrap the function call (see examples below). * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-call-function/ call-function plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/call-function/ call-function plugin documentation on jspsych.org} */ class CallFunctionPlugin implements JsPsychPlugin { static info = info; diff --git a/packages/plugin-canvas-button-response/src/index.ts b/packages/plugin-canvas-button-response/src/index.ts index 07302b88a2..dddeda2833 100644 --- a/packages/plugin-canvas-button-response/src/index.ts +++ b/packages/plugin-canvas-button-response/src/index.ts @@ -1,83 +1,132 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "canvas-button-response", + version: version, parameters: { - /** The drawing function to apply to the canvas. Should take the canvas object as argument. */ + /** + * The function to draw on the canvas. This function automatically takes a canvas element as its only argument, + * e.g. `function(c) {...}` or `function drawStim(c) {...}`, where `c` refers to the canvas element. Note that + * the stimulus function will still generally need to set the correct context itself, using a line like + * `let ctx = c.getContext("2d")`. + */ stimulus: { type: ParameterType.FUNCTION, - pretty_name: "Stimulus", default: undefined, }, - /** Array containing the label(s) for the button(s). */ + /** Labels for the buttons. Each different string in the array will generate a different button. */ choices: { type: ParameterType.STRING, - pretty_name: "Choices", default: undefined, array: true, }, - /** The html of the button. Can create own style. */ + /** + * ``(choice: string, choice_index: number)=>```; | A + * function that generates the HTML for each button in the `choices` array. The function gets the + * string and index of the item in the `choices` array and should return valid HTML. If you want + * to use different markup for each button, you can do that by using a conditional on either parameter. + * The default parameter returns a button element with the text label of the choice. + */ button_html: { - type: ParameterType.HTML_STRING, - pretty_name: "Button HTML", - default: '', - array: true, + type: ParameterType.FUNCTION, + default: function (choice: string, choice_index: number) { + return ``; + }, }, - /** Any content here will be displayed under the button. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. + * The intention is that it can be used to provide a reminder about the action the participant is supposed + * to take (e.g., what question to answer). + */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to hide the stimulus. */ + /** How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be + * set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until + * the trial ends. + */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show the trial. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. + * If the participant fails to make a response before this timer is reached, the participant's response + * will be recorded as null for the trial and the trial will end. If the value of this parameter is null, + * the trial will wait for a response indefinitely. + */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** The vertical margin of the button. */ - margin_vertical: { + /** Setting to `'grid'` will make the container element have the CSS property `display: grid` and enable + * the use of `grid_rows` and `grid_columns`. Setting to `'flex'` will make the container element have the + * CSS property `display: flex`. You can customize how the buttons are laid out by adding inline CSS in + * the `button_html` parameter. + */ + button_layout: { type: ParameterType.STRING, - pretty_name: "Margin vertical", - default: "0px", + default: "grid", }, - /** The horizontal margin of the button. */ - margin_horizontal: { - type: ParameterType.STRING, - pretty_name: "Margin horizontal", - default: "8px", + /** + * The number of rows in the button grid. Only applicable when `button_layout` is set to `'grid'`. + * If null, the number of rows will be determined automatically based on the number of buttons and the number of columns. + */ + grid_rows: { + type: ParameterType.INT, + default: 1, }, - /** If true, then trial will end when user responds. */ + /** + * The number of columns in the button grid. Only applicable when `button_layout` is set to `'grid'`. + * If null, the number of columns will be determined automatically based on the number of buttons and the number of rows. + */ + grid_columns: { + type: ParameterType.INT, + default: null, + }, + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response + * before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until + * the value for `trial_duration` is reached. You can use this parameter to force the participant to view a + * stimulus for a fixed amount of time, even if they respond before the time is complete. + */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** Array containing the height (first value) and width (second value) of the canvas element. */ + /** Array that defines the size of the canvas element in pixels. First value is height, second value is width. */ canvas_size: { type: ParameterType.INT, array: true, - pretty_name: "Canvas size", default: [500, 500], }, }, + data: { + /** Indicates which button the participant pressed. The first button in the `choices` array is 0, the second is 1, and so on. */ + response: { + type: ParameterType.INT, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the + * stimulus first appears on the screen until the participant's response. + */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **canvas-button-response** - * - * jsPsych plugin for displaying a canvas stimulus and getting a button response + * This plugin can be used to draw a stimulus on a [HTML canvas element](https://www.w3schools.com/html/html5_canvas.asp), and record + * a button click response and response time. The canvas stimulus can be useful for displaying dynamic, parametrically-defined + * graphics, and for controlling the positioning of multiple graphical elements (shapes, text, images). The stimulus can be + * displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the + * participant has failed to respond within a fixed length of time. One or more button choices will be displayed under the canvas, + * and the button style can be customized using HTML formatting. * * @author Chris Jungerius (modified from Josh de Leeuw) - * @see {@link https://www.jspsych.org/plugins/jspsych-canvas-button-response/ canvas-button-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/canvas-button-response/ canvas-button-response plugin documentation on jspsych.org} */ class CanvasButtonResponsePlugin implements JsPsychPlugin { static info = info; @@ -85,73 +134,65 @@ class CanvasButtonResponsePlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { - // create canvas - var html = - '
' + - '' + - "
"; - - //display buttons - var buttons = []; - if (Array.isArray(trial.button_html)) { - if (trial.button_html.length == trial.choices.length) { - buttons = trial.button_html; - } else { - console.error( - "Error in canvas-button-response plugin. The length of the button_html array does not equal the length of the choices array" + // Create canvas + const stimulusElement = document.createElement("div"); + stimulusElement.id = "jspsych-canvas-button-response-stimulus"; + + const canvasElement = document.createElement("canvas"); + canvasElement.id = "jspsych-canvas-stimulus"; + canvasElement.height = trial.canvas_size[0]; + canvasElement.width = trial.canvas_size[1]; + canvasElement.style.display = "block"; + stimulusElement.appendChild(canvasElement); + + display_element.appendChild(stimulusElement); + + // Display buttons + const buttonGroupElement = document.createElement("div"); + buttonGroupElement.id = "jspsych-canvas-button-response-btngroup"; + if (trial.button_layout === "grid") { + buttonGroupElement.classList.add("jspsych-btn-group-grid"); + if (trial.grid_rows === null && trial.grid_columns === null) { + throw new Error( + "You cannot set `grid_rows` to `null` without providing a value for `grid_columns`." ); } - } else { - for (var i = 0; i < trial.choices.length; i++) { - buttons.push(trial.button_html); - } + const n_cols = + trial.grid_columns === null + ? Math.ceil(trial.choices.length / trial.grid_rows) + : trial.grid_columns; + const n_rows = + trial.grid_rows === null + ? Math.ceil(trial.choices.length / trial.grid_columns) + : trial.grid_rows; + buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`; + buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`; + } else if (trial.button_layout === "flex") { + buttonGroupElement.classList.add("jspsych-btn-group-flex"); } - html += '
'; - for (var i = 0; i < trial.choices.length; i++) { - var str = buttons[i].replace(/%choice%/g, trial.choices[i]); - html += - '
' + - str + - "
"; + + for (const [choiceIndex, choice] of trial.choices.entries()) { + buttonGroupElement.insertAdjacentHTML("beforeend", trial.button_html(choice, choiceIndex)); + const buttonElement = buttonGroupElement.lastChild as HTMLElement; + buttonElement.dataset.choice = choiceIndex.toString(); + buttonElement.addEventListener("click", () => { + after_response(choiceIndex); + }); } - html += "
"; - //show prompt if there is one + display_element.appendChild(buttonGroupElement); + + // Show prompt if there is one if (trial.prompt !== null) { - html += trial.prompt; + display_element.insertAdjacentHTML("beforeend", trial.prompt); } - display_element.innerHTML = html; //draw - let c = document.getElementById("jspsych-canvas-stimulus"); - trial.stimulus(c); + trial.stimulus(canvasElement); // start time var start_time = performance.now(); - // add event listeners to buttons - for (var i = 0; i < trial.choices.length; i++) { - display_element - .querySelector("#jspsych-canvas-button-response-button-" + i) - .addEventListener("click", (e: MouseEvent) => { - var btn_el = e.currentTarget as Element; - var choice = btn_el.getAttribute("data-choice"); // don't use dataset for jsdom compatibility - after_response(choice); - }); - } - // store response var response = { rt: null, @@ -160,18 +201,12 @@ class CanvasButtonResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial var trial_data = { rt: response.rt, response: response.button, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; @@ -186,14 +221,11 @@ class CanvasButtonResponsePlugin implements JsPsychPlugin { // after a valid response, the stimulus will have the CSS class 'responded' // which can be used to provide visual feedback that a response was recorded - display_element.querySelector("#jspsych-canvas-button-response-stimulus").className += - " responded"; + stimulusElement.classList.add("responded"); // disable all the buttons after a response - var btns = document.querySelectorAll(".jspsych-canvas-button-response-button button"); - for (var i = 0; i < btns.length; i++) { - //btns[i].removeEventListener('click'); - btns[i].setAttribute("disabled", "disabled"); + for (const button of buttonGroupElement.children) { + button.setAttribute("disabled", "disabled"); } if (trial.response_ends_trial) { @@ -204,9 +236,7 @@ class CanvasButtonResponsePlugin implements JsPsychPlugin { // hide image if timing is set if (trial.stimulus_duration !== null) { this.jsPsych.pluginAPI.setTimeout(() => { - display_element.querySelector( - "#jspsych-canvas-button-response-stimulus" - ).style.visibility = "hidden"; + stimulusElement.style.visibility = "hidden"; }, trial.stimulus_duration); } @@ -262,7 +292,9 @@ class CanvasButtonResponsePlugin implements JsPsychPlugin { if (data.rt !== null) { this.jsPsych.pluginAPI.clickTarget( - display_element.querySelector(`div[data-choice="${data.response}"] button`), + display_element.querySelector( + `#jspsych-canvas-button-response-btngroup [data-choice="${data.response}"]` + ), data.rt ); } diff --git a/packages/plugin-canvas-keyboard-response/src/index.ts b/packages/plugin-canvas-keyboard-response/src/index.ts index fa11c123bb..f2aecfd6de 100644 --- a/packages/plugin-canvas-keyboard-response/src/index.ts +++ b/packages/plugin-canvas-keyboard-response/src/index.ts @@ -1,63 +1,96 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "canvas-keyboard-response", + version: version, parameters: { - /** The drawing function to apply to the canvas. Should take the canvas object as argument. */ + /** The function to draw on the canvas. This function automatically takes a canvas element as its only + * argument, e.g. `function(c) {...}` or `function drawStim(c) {...}`, where `c` refers to the canvas element. + * Note that the stimulus function will still generally need to set the correct context itself, using a line + * like `let ctx = c.getContext("2d")`. + */ stimulus: { type: ParameterType.FUNCTION, - pretty_name: "Stimulus", default: undefined, }, - /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */ + /** This array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. + * Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - + * see [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) + * and [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/) + * for more examples. Any key presses that are not listed in the array will be ignored. The default value + * of `"ALL_KEYS"` means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean + * that no responses are allowed. + */ choices: { type: ParameterType.KEYS, - pretty_name: "Choices", default: "ALL_KEYS", }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention + * is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). + */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to show the stimulus. */ + /** How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to + * `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. + */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show trial before it ends. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the + * participant fails to make a response before this timer is reached, the participant's response will be + * recorded as null for the trial and the trial will end. If the value of this parameter is null, then the + * trial will wait for a response indefinitely. + */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** If true, trial will end when subject makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their + * response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will + * continue until the value for `trial_duration` is reached. You can use this parameter to force the participant + * to view a stimulus for a fixed amount of time, even if they respond before the time is complete. + */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** Array containing the height (first value) and width (second value) of the canvas element. */ + /** Array that defines the size of the canvas element in pixels. First value is height, second value is width. */ canvas_size: { type: ParameterType.INT, array: true, - pretty_name: "Canvas size", default: [500, 500], }, }, + data: { + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from + * when the stimulus first appears on the screen until the participant's response. + */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **canvas-keyboard-response** - * - * jsPsych plugin for displaying a canvas stimulus and getting a keyboard response + * This plugin can be used to draw a stimulus on a [HTML canvas element](https://www.w3schools.com/html/html5_canvas.asp) and + * record a keyboard response. The canvas stimulus can be useful for displaying dynamic, parametrically-defined graphics, + * and for controlling the positioning of multiple graphical elements (shapes, text, images). The stimulus can be + * displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically + * if the participant has failed to respond within a fixed length of time. * * @author Chris Jungerius (modified from Josh de Leeuw) - * @see {@link https://www.jspsych.org/plugins/jspsych-canvas-keyboard-response/ canvas-keyboard-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/canvas-keyboard-response/ canvas-keyboard-response plugin documentation on jspsych.org} */ class CanvasKeyboardResponsePlugin implements JsPsychPlugin { static info = info; @@ -81,6 +114,7 @@ class CanvasKeyboardResponsePlugin implements JsPsychPlugin { // draw display_element.innerHTML = new_html; let c = document.getElementById("jspsych-canvas-stimulus"); + c.style.display = "block"; trial.stimulus(c); // store response var response = { @@ -90,9 +124,6 @@ class CanvasKeyboardResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // kill keyboard listeners if (typeof keyboardListener !== "undefined") { this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); @@ -104,9 +135,6 @@ class CanvasKeyboardResponsePlugin implements JsPsychPlugin { response: response.key, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-canvas-slider-response/src/index.ts b/packages/plugin-canvas-slider-response/src/index.ts index 88633fe1f3..0d202d6e3f 100644 --- a/packages/plugin-canvas-slider-response/src/index.ts +++ b/packages/plugin-canvas-slider-response/src/index.ts @@ -1,107 +1,104 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "canvas-slider-response", + version: version, parameters: { - /** The drawing function to apply to the canvas. Should take the canvas object as argument. */ + /** The function to draw on the canvas. This function automatically takes a canvas element as its only argument, e.g. `function(c) {...}` or `function drawStim(c) {...}`, where `c` refers to the canvas element. Note that the stimulus function will still generally need to set the correct context itself, using a line like `let ctx = c.getContext("2d")`. */ stimulus: { type: ParameterType.FUNCTION, - pretty_name: "Stimulus", default: undefined, }, /** Sets the minimum value of the slider. */ min: { type: ParameterType.INT, - pretty_name: "Min slider", default: 0, }, /** Sets the maximum value of the slider */ max: { type: ParameterType.INT, - pretty_name: "Max slider", default: 100, }, /** Sets the starting value of the slider */ slider_start: { type: ParameterType.INT, - pretty_name: "Slider starting value", default: 50, }, - /** Sets the step of the slider */ + /** Sets the step of the slider. This is the smallest amount by which the slider can change. */ step: { type: ParameterType.INT, - pretty_name: "Step", default: 1, }, - /** Array containing the labels for the slider. Labels will be displayed at equidistant locations along the slider. */ + /** Labels displayed at equidistant locations on the slider. For example, two labels will be placed at the ends of the slider. Three labels would place two at the ends and one in the middle. Four will place two at the ends, and the other two will be at 33% and 67% of the slider width. */ labels: { type: ParameterType.HTML_STRING, - pretty_name: "Labels", default: [], array: true, }, - /** Width of the slider in pixels. */ + /** Set the width of the slider in pixels. If left null, then the width will be equal to the widest element in the display. */ slider_width: { type: ParameterType.INT, - pretty_name: "Slider width", default: null, }, - /** Label of the button to advance. */ + /** Label of the button to end the trial. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", array: false, }, - /** If true, the participant will have to move the slider before continuing. */ + /** If true, the participant must click the slider before clicking the continue button. */ require_movement: { type: ParameterType.BOOL, - pretty_name: "Require movement", default: false, }, - /** Any content here will be displayed below the slider */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., what question to answer). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to show the stimulus. */ + /** How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show the trial. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the participant fails to make a response before this timer is reached, the participant's response will be recorded as null for the trial and the trial will end. If the value of this parameter is null, then the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** If true, trial will end when user makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the value for `trial_duration` is reached. You can use this parameter to force the participant to view a stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** Array containing the height (first value) and width (second value) of the canvas element. */ + /** Array that defines the size of the canvas element in pixels. First value is height, second value is width. */ canvas_size: { type: ParameterType.INT, array: true, - pretty_name: "Canvas size", default: [500, 500], }, }, + data: { + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * **canvas-slider-response** - * - * jsPsych plugin for displaying a canvas stimulus and getting a slider response + * This plugin can be used to draw a stimulus on a [HTML canvas element](https://www.w3schools.com/html/html5_canvas.asp) and collect a response within a range of values, which is made by dragging a slider. The canvas stimulus can be useful for displaying dynamic, parametrically-defined graphics, and for controlling the positioning of multiple graphical elements (shapes, text, images). The stimulus can be displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant has failed to respond within a fixed length of time. * * @author Chris Jungerius (modified from Josh de Leeuw) - * @see {@link https://www.jspsych.org/plugins/jspsych-canvas-slider-response/ canvas-slider-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/canvas-slider-response/ canvas-slider-response plugin documentation on jspsych.org} */ class CanvasSliderResponsePlugin implements JsPsychPlugin { static info = info; @@ -169,6 +166,7 @@ class CanvasSliderResponsePlugin implements JsPsychPlugin { // draw let c = document.getElementById("jspsych-canvas-stimulus"); + c.style.display = "block"; trial.stimulus(c); var response = { @@ -177,8 +175,6 @@ class CanvasSliderResponsePlugin implements JsPsychPlugin { }; const end_trial = () => { - this.jsPsych.pluginAPI.clearAllTimeouts(); - // save data var trialdata = { rt: response.rt, @@ -186,8 +182,6 @@ class CanvasSliderResponsePlugin implements JsPsychPlugin { slider_start: trial.slider_start, }; - display_element.innerHTML = ""; - // next trial this.jsPsych.finishTrial(trialdata); }; diff --git a/packages/plugin-categorize-animation/src/index.spec.ts b/packages/plugin-categorize-animation/src/index.spec.ts index c1d5e828e1..7caa7e79d5 100644 --- a/packages/plugin-categorize-animation/src/index.spec.ts +++ b/packages/plugin-categorize-animation/src/index.spec.ts @@ -75,7 +75,7 @@ describe("categorize-animation plugin", () => { ]); jest.advanceTimersByTime(1000); - pressKey("d"); + await pressKey("d"); jest.advanceTimersByTime(1000); expect(getHTML()).toBe("Correct."); }); @@ -94,7 +94,7 @@ describe("categorize-animation plugin", () => { ]); jest.advanceTimersByTime(1000); - pressKey("s"); + await pressKey("s"); jest.advanceTimersByTime(1000); expect(getHTML()).toBe("Wrong."); }); @@ -116,7 +116,7 @@ describe("categorize-animation plugin", () => { ]); jest.advanceTimersByTime(1000); - pressKey("d"); + await pressKey("d"); jest.advanceTimersByTime(1000); expect(getHTML()).toBe("

Correct. The faces had different expressions.

"); }); @@ -137,7 +137,7 @@ describe("categorize-animation plugin", () => { ]); jest.advanceTimersByTime(1000); - pressKey("d"); + await pressKey("d"); jest.advanceTimersByTime(1000); expect(getHTML()).toBe("

You pressed the correct key

"); }); @@ -158,7 +158,7 @@ describe("categorize-animation plugin", () => { ]); jest.advanceTimersByTime(1500); - pressKey("s"); + await pressKey("s"); jest.advanceTimersByTime(1000); expect(getHTML()).toBe("

Incorrect. You pressed the wrong key.

"); }); @@ -240,7 +240,7 @@ describe("categorize-animation plugin", () => { ]); jest.advanceTimersByTime(500); - pressKey("d"); + await pressKey("d"); jest.advanceTimersByTime(1000); expect(getHTML()).toEqual( '

You pressed the correct key

' @@ -265,7 +265,7 @@ describe("categorize-animation plugin", () => { ]); jest.advanceTimersByTime(1000); - pressKey("d"); + await pressKey("d"); jest.advanceTimersByTime(500); expect(getHTML()).toBe("

You pressed the correct key

"); jest.advanceTimersByTime(2000); diff --git a/packages/plugin-categorize-animation/src/index.ts b/packages/plugin-categorize-animation/src/index.ts index 76b4f9e5f7..2abfc45148 100644 --- a/packages/plugin-categorize-animation/src/index.ts +++ b/packages/plugin-categorize-animation/src/index.ts @@ -1,96 +1,104 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "categorize-animation", + version: version, parameters: { - /** Array of paths to image files. */ + /** Each element of the array is a path to an image file. */ stimuli: { type: ParameterType.IMAGE, - pretty_name: "Stimuli", default: undefined, array: true, }, - /** The key to indicate correct response */ + /** The key character indicating the correct response. */ key_answer: { type: ParameterType.KEY, - pretty_name: "Key answer", default: undefined, }, - /** Array containing the key(s) the subject is allowed to press to respond to the stimuli. */ + /** This array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) and [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/) for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"ALL_KEYS"` means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean that no responses are allowed. */ choices: { type: ParameterType.KEYS, - pretty_name: "Choices", default: "ALL_KEYS", }, - /** Text to describe correct answer. */ + /** A text label that describes the correct answer. Used in conjunction with the `correct_text` and `incorrect_text` parameters. */ text_answer: { type: ParameterType.HTML_STRING, - pretty_name: "Text answer", default: null, }, - /** String to show when subject gives correct answer */ + /** String to show when the correct answer is given. Can contain HTML formatting. The special string `%ANS%` can be used within the string. If present, the plugin will put the `text_answer` for the trial in place of the %ANS% string (see example below). */ correct_text: { type: ParameterType.HTML_STRING, - pretty_name: "Correct text", default: "Correct.", }, - /** String to show when subject gives incorrect answer. */ + /** String to show when the wrong answer is given. Can contain HTML formatting. The special string `%ANS%` can be used within the string. If present, the plugin will put the `text_answer` for the trial in place of the %ANS% string (see example below). */ incorrect_text: { type: ParameterType.HTML_STRING, - pretty_name: "Incorrect text", default: "Wrong.", }, - /** Duration to display each image. */ + /** How long to display each image (in milliseconds). */ frame_time: { type: ParameterType.INT, - pretty_name: "Frame time", default: 500, }, - /** How many times to display entire sequence. */ + /** How many times to show the entire sequence. */ sequence_reps: { type: ParameterType.INT, - pretty_name: "Sequence repetitions", default: 1, }, - /** If true, subject can response before the animation sequence finishes */ + /** If true, the participant can respond before the animation sequence finishes. */ allow_response_before_complete: { type: ParameterType.BOOL, - pretty_name: "Allow response before complete", default: false, }, - /** How long to show feedback */ + /** How long to show the feedback (milliseconds). */ feedback_duration: { type: ParameterType.INT, - pretty_name: "Feedback duration", default: 2000, }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus or the end of the animation depending on the allow_response_before_complete parameter. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, /** - * If true, the images will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers). - * If false, the image will be shown via an img element. + * If true, the images will be drawn onto a canvas element. This prevents a blank screen (white flash) between consecutive images in some browsers, like Firefox and Edge. + * If false, the image will be shown via an img element, as in previous versions of jsPsych. */ render_on_canvas: { type: ParameterType.BOOL, - pretty_name: "Render on canvas", default: true, }, }, + data: { + /** Array of stimuli displayed in the trial. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + stimulus: { + type: ParameterType.STRING, + array: true, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** `true` if the participant got the correct answer, `false` otherwise. */ + correct: { + type: ParameterType.BOOL, + }, + }, }; type Info = typeof info; /** - * **categorize-animation** - * - * jsPsych plugin for categorization trials with feedback and animated stimuli + * The categorize animation plugin shows a sequence of images at a specified frame rate. The participant responds by pressing a key. Feedback indicating the correctness of the response is given. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-categorize-animation/ categorize-animation plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/categorize-animation/ categorize-animation plugin documentation on jspsych.org} */ class CategorizeAnimationPlugin implements JsPsychPlugin { static info = info; @@ -232,7 +240,6 @@ class CategorizeAnimationPlugin implements JsPsychPlugin { const endTrial = () => { clearInterval(animate_interval); // stop animation! - display_element.innerHTML = ""; // clear everything this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-categorize-html/src/index.spec.ts b/packages/plugin-categorize-html/src/index.spec.ts index 408c04f2a8..1523bf4c0f 100644 --- a/packages/plugin-categorize-html/src/index.spec.ts +++ b/packages/plugin-categorize-html/src/index.spec.ts @@ -16,7 +16,7 @@ describe("categorize-html plugin", () => { ]); expect(getHTML()).toMatch("FOO"); - pressKey("d"); + await pressKey("d"); expect(getHTML()).toMatch("Correct"); jest.advanceTimersByTime(2000); diff --git a/packages/plugin-categorize-html/src/index.ts b/packages/plugin-categorize-html/src/index.ts index 68cd88abc4..e3092d5d95 100644 --- a/packages/plugin-categorize-html/src/index.ts +++ b/packages/plugin-categorize-html/src/index.ts @@ -1,104 +1,109 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "categorize-html", + version: version, parameters: { - /** The HTML content to be displayed. */ + /** The HTML stimulus to display. */ stimulus: { type: ParameterType.HTML_STRING, - pretty_name: "Stimulus", default: undefined, }, - /** The key to indicate the correct response. */ + /** The key character indicating the correct response. */ key_answer: { type: ParameterType.KEY, - pretty_name: "Key answer", default: undefined, }, - /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */ + /** This array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) and [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/) for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"ALL_KEYS"` means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean that no responses are allowed. */ choices: { type: ParameterType.KEYS, - pretty_name: "Choices", default: "ALL_KEYS", }, - /** Label that is associated with the correct answer. */ + /** A label that is associated with the correct answer. Used in conjunction with the `correct_text` and `incorrect_text` parameters. */ text_answer: { type: ParameterType.HTML_STRING, - pretty_name: "Text answer", default: null, }, - /** String to show when correct answer is given. */ + /** String to show when the correct answer is given. Can contain HTML formatting. The special string `%ANS%` can be used within the string. If present, the plugin will put the `text_answer` for the trial in place of the `%ANS%` string (see example below). */ correct_text: { type: ParameterType.HTML_STRING, - pretty_name: "Correct text", default: "", }, - /** String to show when incorrect answer is given. */ + /** String to show when the wrong answer is given. Can contain HTML formatting. The special string `%ANS%` can be used within the string. If present, the plugin will put the `text_answer` for the trial in place of the `%ANS%` string (see example below). */ incorrect_text: { type: ParameterType.HTML_STRING, - pretty_name: "Incorrect text", default: "", }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** If set to true, then the subject must press the correct response key after feedback in order to advance to next trial. */ + /** If set to true, then the participant must press the correct response key after feedback is given in order to advance to the next trial. */ force_correct_button_press: { type: ParameterType.BOOL, - pretty_name: "Force correct button press", default: false, }, - /** If true, stimulus will be shown during feedback. If false, only the text feedback will be displayed during feedback. */ + /** If set to true, then the stimulus will be shown during feedback. If false, then only the text feedback will display during feedback. */ show_stim_with_feedback: { type: ParameterType.BOOL, - default: true, - no_function: false, + default: false, }, - /** Whether or not to show feedback following a response timeout. */ + /** If true, then category feedback will be displayed for an incorrect response after a timeout (trial_duration is exceeded). If false, then a timeout message will be shown. */ show_feedback_on_timeout: { type: ParameterType.BOOL, - pretty_name: "Show feedback on timeout", default: false, }, - /** The message displayed on a timeout non-response. */ + /** The message to show on a timeout non-response. */ timeout_message: { type: ParameterType.HTML_STRING, - pretty_name: "Timeout message", default: "

Please respond faster.

", }, - /** How long to show the stimulus. */ + /** How long to show the stimulus for (milliseconds). If null, then the stimulus is shown until a response is given. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show trial */ + /** The maximum time allowed for a response. If null, then the experiment will wait indefinitely for a response. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** How long to show feedback. */ + /** How long to show the feedback for (milliseconds). */ feedback_duration: { type: ParameterType.INT, - pretty_name: "Feedback duration", default: 2000, }, }, + data: { + /** Either the path to the image file or the string containing the HTML formatted content that the participant saw on this trial. */ + stimulus: { + type: ParameterType.STRING, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** `true` if the participant got the correct answer, `false` otherwise. */ + correct: { + type: ParameterType.BOOL, + }, + }, }; type Info = typeof info; /** - * **categorize-html** - * - * jsPsych plugin for categorization trials with feedback + * The categorize html plugin shows an HTML object on the screen. The participant responds by pressing a key. Feedback indicating the correctness of the response is given. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-categorize-html/ categorize-html plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/categorize-html/ categorize-html plugin documentation on jspsych.org} */ class CategorizeHtmlPlugin implements JsPsychPlugin { static info = info; @@ -129,9 +134,6 @@ class CategorizeHtmlPlugin implements JsPsychPlugin { // create response function const after_response = (info: { key: string; rt: number }) => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // clear keyboard listener this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); @@ -148,8 +150,6 @@ class CategorizeHtmlPlugin implements JsPsychPlugin { response: info.key, }; - display_element.innerHTML = ""; - var timeout = info.rt == null; doFeedback(correct, timeout); }; @@ -172,7 +172,6 @@ class CategorizeHtmlPlugin implements JsPsychPlugin { } const endTrial = () => { - display_element.innerHTML = ""; this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-categorize-image/src/index.spec.ts b/packages/plugin-categorize-image/src/index.spec.ts index 821f173d84..9b3da78f17 100644 --- a/packages/plugin-categorize-image/src/index.spec.ts +++ b/packages/plugin-categorize-image/src/index.spec.ts @@ -16,7 +16,7 @@ describe("categorize-image plugin", () => { ]); expect(getHTML()).toMatch("FOO.png"); - pressKey("d"); + await pressKey("d"); expect(getHTML()).toMatch("Correct"); jest.advanceTimersByTime(2000); diff --git a/packages/plugin-categorize-image/src/index.ts b/packages/plugin-categorize-image/src/index.ts index 3fdab1f2e4..e6b36c9371 100644 --- a/packages/plugin-categorize-image/src/index.ts +++ b/packages/plugin-categorize-image/src/index.ts @@ -1,104 +1,110 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "categorize-image", + version: version, parameters: { - /** The image content to be displayed. */ + /** The path to the image file. */ stimulus: { type: ParameterType.IMAGE, - pretty_name: "Stimulus", default: undefined, }, - /** The key to indicate the correct response. */ + /** The key character indicating the correct response. */ key_answer: { type: ParameterType.KEY, - pretty_name: "Key answer", default: undefined, }, - /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */ + /** This array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) and [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/) for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"ALL_KEYS"` means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean that no responses are allowed. */ choices: { type: ParameterType.KEYS, - pretty_name: "Choices", default: "ALL_KEYS", }, - /** Label that is associated with the correct answer. */ + /** A label that is associated with the correct answer. Used in conjunction with the `correct_text` and `incorrect_text` parameters.*/ text_answer: { type: ParameterType.HTML_STRING, - pretty_name: "Text answer", default: null, }, - /** String to show when correct answer is given. */ + /** String to show when the correct answer is given. Can contain HTML formatting. The special string `%ANS%` can be used within the string. If present, the plugin will put the `text_answer` for the trial in place of the %ANS% string (see example below). */ correct_text: { type: ParameterType.HTML_STRING, - pretty_name: "Correct text", default: "", }, - /** String to show when incorrect answer is given. */ + /** String to show when the wrong answer is given. Can contain HTML formatting. The special string `%ANS%` can be used within the string. If present, the plugin will put the `text_answer` for the trial in place of the %ANS% string (see example below). */ incorrect_text: { type: ParameterType.HTML_STRING, - pretty_name: "Incorrect text", default: "", }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** If set to true, then the subject must press the correct response key after feedback in order to advance to next trial. */ + /** If set to true, then the participant must press the correct response key after feedback is given in order to advance to the next trial. */ force_correct_button_press: { type: ParameterType.BOOL, - pretty_name: "Force correct button press", default: false, }, - /** Whether or not the stimulus should be shown on the feedback screen. */ + /** If set to true, then the stimulus will be shown during feedback. If false, then only the text feedback will display during feedback.*/ show_stim_with_feedback: { type: ParameterType.BOOL, default: true, no_function: false, }, - /** If true, stimulus will be shown during feedback. If false, only the text feedback will be displayed during feedback. */ + /** If true, then category feedback will be displayed for an incorrect response after a timeout (trial_duration is exceeded). If false, then a timeout message will be shown. */ show_feedback_on_timeout: { type: ParameterType.BOOL, - pretty_name: "Show feedback on timeout", default: false, }, - /** The message displayed on a timeout non-response. */ + /** The message to show on a timeout non-response. */ timeout_message: { type: ParameterType.HTML_STRING, - pretty_name: "Timeout message", default: "

Please respond faster.

", }, - /** How long to show the stimulus. */ + /** How long to show the stimulus for (milliseconds). If null, then the stimulus is shown until a response is given. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show the trial. */ + /** The maximum time allowed for a response. If null, then the experiment will wait indefinitely for a response. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** How long to show the feedback. */ + /** How long to show the feedback for (milliseconds). */ feedback_duration: { type: ParameterType.INT, - pretty_name: "Feedback duration", default: 2000, }, }, + data: { + /** Either the path to the image file or the string containing the HTML formatted content that the participant saw on this trial.*/ + stimulus: { + type: ParameterType.STRING, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** `true` if the participant got the correct answer, `false` otherwise. */ + correct: { + type: ParameterType.BOOL, + }, + }, }; type Info = typeof info; /** - * **categorize-image** - * - * jsPsych plugin for image categorization trials with feedback + * The categorize image plugin shows an image object on the screen. The participant responds by pressing a key. Feedback indicating the correctness of the response is given. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-categorize-image/ categorize-image plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/categorize-image/ categorize-image plugin documentation on jspsych.org} */ class CategorizeImagePlugin implements JsPsychPlugin { static info = info; @@ -129,9 +135,6 @@ class CategorizeImagePlugin implements JsPsychPlugin { // create response function const after_response = (info: { key: string; rt: number }) => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // clear keyboard listener this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); @@ -148,8 +151,6 @@ class CategorizeImagePlugin implements JsPsychPlugin { response: info.key, }; - display_element.innerHTML = ""; - var timeout = info.rt == null; doFeedback(correct, timeout); }; @@ -172,7 +173,6 @@ class CategorizeImagePlugin implements JsPsychPlugin { } const endTrial = () => { - display_element.innerHTML = ""; this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-cloze/src/index.spec.ts b/packages/plugin-cloze/src/index.spec.ts index 3c383818f5..a522016a91 100644 --- a/packages/plugin-cloze/src/index.spec.ts +++ b/packages/plugin-cloze/src/index.spec.ts @@ -1,4 +1,4 @@ -import { clickTarget, simulateTimeline, startTimeline } from "@jspsych/test-utils"; +import { clickTarget, flushPromises, simulateTimeline, startTimeline } from "@jspsych/test-utils"; import cloze from "."; @@ -6,9 +6,11 @@ jest.useFakeTimers(); const getInputElementById = (id: string) => document.getElementById(id) as HTMLInputElement; +const clickFinishButton = () => clickTarget(document.querySelector("#finish_cloze_button")); + describe("cloze", () => { test("displays cloze", async () => { - const { getHTML } = await startTimeline([ + const { getHTML, expectFinished } = await startTimeline([ { type: cloze, text: "This is a %cloze% text.", @@ -18,10 +20,13 @@ describe("cloze", () => { expect(getHTML()).toContain( '
This is a text.
' ); + + await clickFinishButton(); + await expectFinished(); }); test("displays default button text", async () => { - const { getHTML } = await startTimeline([ + const { getHTML, expectFinished } = await startTimeline([ { type: cloze, text: "This is a %cloze% text.", @@ -31,10 +36,13 @@ describe("cloze", () => { expect(getHTML()).toContain( '' ); + + await clickFinishButton(); + await expectFinished(); }); test("displays custom button text", async () => { - const { getHTML } = await startTimeline([ + const { getHTML, expectFinished } = await startTimeline([ { type: cloze, text: "This is a %cloze% text.", @@ -45,6 +53,9 @@ describe("cloze", () => { expect(getHTML()).toContain( '' ); + + await clickFinishButton(); + await expectFinished(); }); test("ends trial on button click when using default settings, i.e. answers are not checked", async () => { @@ -55,7 +66,7 @@ describe("cloze", () => { }, ]); - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); await expectFinished(); }); @@ -69,7 +80,7 @@ describe("cloze", () => { ]); getInputElementById("input0").value = "cloze"; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); await expectFinished(); }); @@ -83,12 +94,12 @@ describe("cloze", () => { ]); getInputElementById("input0").value = "filler"; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); await expectFinished(); }); - test("does not end trial on button click when answers are checked and not correct", async () => { - const { expectRunning } = await startTimeline([ + test("does not end trial on button click when answers are checked and not correct or missing", async () => { + const { expectRunning, expectFinished } = await startTimeline([ { type: cloze, text: "This is a %cloze% text.", @@ -97,28 +108,22 @@ describe("cloze", () => { ]); getInputElementById("input0").value = "some wrong answer"; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); await expectRunning(); - }); - - test("does not end trial on button click when answers are checked for completion and some are missing", async () => { - const { expectRunning } = await startTimeline([ - { - type: cloze, - text: "This is a %cloze% text.", - allow_blanks: false, - }, - ]); getInputElementById("input0").value = ""; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); await expectRunning(); + + getInputElementById("input0").value = "cloze"; + await clickFinishButton(); + await expectFinished(); }); test("does not call mistake function on button click when answers are checked and correct", async () => { const mistakeFn = jest.fn(); - await startTimeline([ + const { expectFinished } = await startTimeline([ { type: cloze, text: "This is a %cloze% text.", @@ -128,14 +133,16 @@ describe("cloze", () => { ]); getInputElementById("input0").value = "cloze"; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); expect(mistakeFn).not.toHaveBeenCalled(); + + await expectFinished(); }); test("does not call mistake function on button click when answers are checked for completion and are complete", async () => { const mistakeFn = jest.fn(); - await startTimeline([ + const { expectFinished } = await startTimeline([ { type: cloze, text: "This is a %cloze% text.", @@ -145,14 +152,16 @@ describe("cloze", () => { ]); getInputElementById("input0").value = "cloze"; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); expect(mistakeFn).not.toHaveBeenCalled(); + + await expectFinished(); }); - test("calls mistake function on button click when answers are checked and not correct", async () => { + test("calls mistake function on button click when answers are checked and not correct or missing", async () => { const mistakeFn = jest.fn(); - await startTimeline([ + const { expectFinished } = await startTimeline([ { type: cloze, text: "This is a %cloze% text.", @@ -162,29 +171,22 @@ describe("cloze", () => { ]); getInputElementById("input0").value = "some wrong answer"; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); expect(mistakeFn).toHaveBeenCalled(); - }); - test("calls mistake function on button click when answers are checked for completion and are not complete", async () => { - const mistakeFn = jest.fn(); - - await startTimeline([ - { - type: cloze, - text: "This is a %cloze% text.", - check_answers: true, - mistake_fn: mistakeFn, - }, - ]); + mistakeFn.mockReset(); getInputElementById("input0").value = ""; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); expect(mistakeFn).toHaveBeenCalled(); + + getInputElementById("input0").value = "cloze"; + await clickFinishButton(); + await expectFinished(); }); test("response data is stored as an array", async () => { - const { getData, getHTML } = await startTimeline([ + const { getData, expectFinished } = await startTimeline([ { type: cloze, text: "This is a %cloze% text. Here is another cloze response box %%.", @@ -193,12 +195,11 @@ describe("cloze", () => { getInputElementById("input0").value = "cloze1"; getInputElementById("input1").value = "cloze2"; - clickTarget(document.querySelector("#finish_cloze_button")); + await clickFinishButton(); + await expectFinished(); const data = getData().values()[0].response; - expect(data.length).toBe(2); - expect(data[0]).toBe("cloze1"); - expect(data[1]).toBe("cloze2"); + expect(data).toEqual(["cloze1", "cloze2"]); }); }); @@ -213,9 +214,9 @@ describe("cloze simulation", () => { await expectFinished(); - const data = getData().values()[0]; - expect(data.response[0]).toBe("cloze"); - expect(data.response[1]).not.toBe(""); + const response = getData().values()[0].response; + expect(response[0]).toBe("cloze"); + expect(response[1]).not.toBe(""); }); test("visual mode works", async () => { const { getData, expectFinished, expectRunning } = await simulateTimeline( @@ -234,8 +235,8 @@ describe("cloze simulation", () => { await expectFinished(); - const data = getData().values()[0]; - expect(data.response[0]).toBe("cloze"); - expect(data.response[1]).not.toBe(""); + const response = getData().values()[0].response; + expect(response[0]).toBe("cloze"); + expect(response[1]).not.toBe(""); }); }); diff --git a/packages/plugin-cloze/src/index.ts b/packages/plugin-cloze/src/index.ts index 71d538cf2f..28c2023564 100644 --- a/packages/plugin-cloze/src/index.ts +++ b/packages/plugin-cloze/src/index.ts @@ -1,37 +1,42 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "cloze", + version: version, parameters: { - /** The cloze text to be displayed. Blanks are indicated by %% signs and automatically replaced by input fields. If there is a correct answer you want the system to check against, it must be typed between the two percentage signs (i.e. %solution%). */ + /** The cloze text to be displayed. Blanks are indicated by %% signs and automatically replaced by input fields. If there is a correct answer you want the system to check against, it must be typed between the two percentage signs (i.e. % correct solution %). */ text: { type: ParameterType.HTML_STRING, - pretty_name: "Cloze text", default: undefined, }, /** Text of the button participants have to press for finishing the cloze test. */ button_text: { type: ParameterType.STRING, - pretty_name: "Button text", default: "OK", }, - /** Boolean value indicating if the answers given by participants should be compared against a correct solution given in the text (between % signs) after the button was clicked. */ + /** Boolean value indicating if the answers given by participants should be compared against a correct solution given in the text (between % signs) after the button was clicked. If ```true```, answers are checked and in case of differences, the ```mistake_fn``` is called. In this case, the trial does not automatically finish. If ```false```, no checks are performed and the trial automatically ends when clicking the button. */ check_answers: { type: ParameterType.BOOL, - pretty_name: "Check answers", default: false, }, - /** Boolean value indicating if the participant may leave answers blank. */ + /** Boolean value indicating if the answers given by participants should be checked for completion after the button was clicked. If ```true```, answers are not checked for completion and blank answers are allowed. The trial will then automatically finish upon the clicking the button. If ```false```, answers are checked for completion, and in case there are some fields with missing answers, the ```mistake_fn``` is called. In this case, the trial does not automatically finish. */ allow_blanks: { type: ParameterType.BOOL, - pretty_name: "Allow blanks", default: true, }, - /** Function called if either the check_answers is set to TRUE or the allow_blanks is set to FALSE and there is a discrepancy between the set answers and the answers provide or if all input fields aren't filled out, respectively. */ + /** Function called if ```check_answers``` is set to ```true``` and there is a difference between the participant's answers and the correct solution provided in the text, or if ```allow_blanks``` is set to ```false``` and there is at least one field with a blank answer. */ mistake_fn: { type: ParameterType.FUNCTION, - pretty_name: "Mistake function", - default: () => { }, + default: () => {}, + }, + }, + data: { + /** Answers the partcipant gave. */ + response: { + type: ParameterType.STRING, + array: true, }, }, }; @@ -39,17 +44,15 @@ const info = { type Info = typeof info; /** - * **cloze** - * - * jsPsych plugin for displaying a cloze test and checking participants answers against a correct solution + * This plugin displays a text with certain words omitted. Participants are asked to replace the missing items. Responses are recorded when clicking a button. Responses can be evaluated and a function is called in case of either differences or incomplete answers, making it possible to inform participants about mistakes before proceeding. * * @author Philipp Sprengholz - * @see {@link https://www.jspsych.org/plugins/jspsych-cloze/ cloze plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/cloze/ cloze plugin documentation on jspsych.org} */ class ClozePlugin implements JsPsychPlugin { static info = info; - constructor(private jsPsych: JsPsych) { } + constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { var html = '
'; @@ -102,7 +105,6 @@ class ClozePlugin implements JsPsychPlugin { response: answers, }; - display_element.innerHTML = ""; this.jsPsych.finishTrial(trial_data); } }; diff --git a/packages/plugin-external-html/src/index.spec.ts b/packages/plugin-external-html/src/index.spec.ts index 4a27c4061e..f394539c41 100644 --- a/packages/plugin-external-html/src/index.spec.ts +++ b/packages/plugin-external-html/src/index.spec.ts @@ -24,7 +24,7 @@ describe("external-html", () => { await expectRunning(); expect(getHTML()).toMatch("This is external HTML"); - clickTarget(displayElement.querySelector("#finished")); + await clickTarget(displayElement.querySelector("#finished")); await expectFinished(); }); diff --git a/packages/plugin-external-html/src/index.ts b/packages/plugin-external-html/src/index.ts index 590544be3f..87879c2461 100644 --- a/packages/plugin-external-html/src/index.ts +++ b/packages/plugin-external-html/src/index.ts @@ -1,58 +1,62 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "external-html", + version: version, parameters: { - /** The url of the external html page */ + /** The URL of the page to display. */ url: { type: ParameterType.STRING, - pretty_name: "URL", default: undefined, }, - /** The key to continue to the next page. */ + /** The key character the participant can use to advance to the next trial. If left as null, then the participant will not be able to advance trials using the keyboard. */ cont_key: { type: ParameterType.KEY, - pretty_name: "Continue key", default: null, }, - /** The button to continue to the next page. */ + /** The ID of a clickable element on the page. When the element is clicked, the trial will advance. */ cont_btn: { type: ParameterType.STRING, - pretty_name: "Continue button", default: null, }, - /** Function to check whether user is allowed to continue after clicking cont_key or clicking cont_btn */ + /** `function(){ return true; }` | This function is called with the jsPsych `display_element` as the only argument when the participant attempts to advance the trial. The trial will only advance if the function return `true`. This can be used to verify that the participant has correctly filled out a form before continuing, for example. */ check_fn: { type: ParameterType.FUNCTION, - pretty_name: "Check function", default: () => true, }, - /** Whether or not to force a page refresh. */ + /** If `true`, then the plugin will avoid using the cached version of the HTML page to load if one exists. */ force_refresh: { type: ParameterType.BOOL, - pretty_name: "Force refresh", default: false, }, - /** If execute_Script == true, then all JavasScript code on the external page will be executed. */ + /** If `true`, then scripts on the remote page will be executed. */ execute_script: { type: ParameterType.BOOL, pretty_name: "Execute scripts", default: false, }, }, + data: { + /** The url of the page. */ + url: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to finish the trial. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **external-html** - * - * jsPsych plugin to load and display an external html page. To proceed to the next trial, the - * user might either press a button on the page or a specific key. Afterwards, the page will be hidden and - * the experiment will continue. + * The HTML plugin displays an external HTML document (often a consent form). Either a keyboard response or a button press can be used to continue to the next trial. It allows the experimenter to check if conditions are met (such as indicating informed consent) before continuing. * * @author Erik Weitnauer - * @see {@link https://www.jspsych.org/plugins/jspsych-external-html/ external-html plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/external-html/ external-html plugin documentation on jspsych.org} */ class ExternalHtmlPlugin implements JsPsychPlugin { static info = info; @@ -94,7 +98,6 @@ class ExternalHtmlPlugin implements JsPsychPlugin { rt: Math.round(performance.now() - t0), url: trial.url, }; - display_element.innerHTML = ""; this.jsPsych.finishTrial(trial_data); trial_complete(); }; diff --git a/packages/plugin-free-sort/src/index.ts b/packages/plugin-free-sort/src/index.ts index ccfad66c97..a736bba808 100644 --- a/packages/plugin-free-sort/src/index.ts +++ b/packages/plugin-free-sort/src/index.ts @@ -1,71 +1,65 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; import { inside_ellipse, make_arr, random_coordinate, shuffle } from "./utils"; +// import { parameterPathArrayToString } from "jspsych/src/timeline/util"; + const info = { name: "free-sort", + version: version, parameters: { - /** Array of images to be displayed and sorted. */ + /** Each element of this array is an image path. */ stimuli: { type: ParameterType.IMAGE, - pretty_name: "Stimuli", default: undefined, array: true, }, - /** Height of items in pixels. */ + /** The height of the images in pixels. */ stim_height: { type: ParameterType.INT, - pretty_name: "Stimulus height", default: 100, }, - /** Width of items in pixels */ + /** The width of the images in pixels. */ stim_width: { type: ParameterType.INT, - pretty_name: "Stimulus width", default: 100, }, - /** How much larger to make the stimulus while moving (1 = no scaling) */ + /** How much larger to make the stimulus while moving (1 = no scaling). */ scale_factor: { type: ParameterType.FLOAT, - pretty_name: "Stimulus scaling factor", default: 1.5, }, - /** The height in pixels of the container that subjects can move the stimuli in. */ + /** The height of the container that participants can move the stimuli in. Stimuli will be constrained to this area. */ sort_area_height: { type: ParameterType.INT, - pretty_name: "Sort area height", default: 700, }, - /** The width in pixels of the container that subjects can move the stimuli in. */ + /** The width of the container that participants can move the stimuli in. Stimuli will be constrained to this area. */ sort_area_width: { type: ParameterType.INT, - pretty_name: "Sort area width", default: 700, }, - /** The shape of the sorting area */ + /** The shape of the sorting area, can be "ellipse" or "square". */ sort_area_shape: { type: ParameterType.SELECT, - pretty_name: "Sort area shape", options: ["square", "ellipse"], default: "ellipse", }, - /** HTML to display above/below the sort area. It can be used to provide a reminder about the action the subject is supposed to take. */ + /** This string can contain HTML markup. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: "", }, - /** Indicates whether to show prompt "above" or "below" the sorting area. */ + /** Indicates whether to show the prompt `"above"` or `"below"` the sorting area. */ prompt_location: { type: ParameterType.SELECT, - pretty_name: "Prompt location", options: ["above", "below"], default: "above", }, /** The text that appears on the button to continue to the next trial. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, /** @@ -75,7 +69,6 @@ const info = { */ change_border_background_color: { type: ParameterType.BOOL, - pretty_name: "Change border background color", default: true, }, /** @@ -85,7 +78,6 @@ const info = { */ border_color_in: { type: ParameterType.STRING, - pretty_name: "Border color - in", default: "#a1d99b", }, /** @@ -94,13 +86,11 @@ const info = { */ border_color_out: { type: ParameterType.STRING, - pretty_name: "Border color - out", default: "#fc9272", }, /** The width in pixels of the border around the sort area. If null, the border width defaults to 3% of the sort area height. */ border_width: { type: ParameterType.INT, - pretty_name: "Border width", default: null, }, /** @@ -110,13 +100,11 @@ const info = { * */ counter_text_unfinished: { type: ParameterType.HTML_STRING, - pretty_name: "Counter text unfinished", default: "You still need to place %n% item%s% inside the sort area.", }, /** Text that will take the place of the counter_text_unfinished text when all items have been moved inside the sort area. */ counter_text_finished: { type: ParameterType.HTML_STRING, - pretty_name: "Counter text finished", default: "All items placed. Feel free to reposition items if necessary.", }, /** @@ -125,7 +113,6 @@ const info = { */ stim_starts_inside: { type: ParameterType.BOOL, - pretty_name: "Stim starts inside", default: false, }, /** @@ -134,21 +121,61 @@ const info = { */ column_spread_factor: { type: ParameterType.FLOAT, - pretty_name: "column spread factor", default: 1, }, }, + data: { + /** An array containing objects representing the initial 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. */ + init_locations: { + type: ParameterType.STRING, + array: true, + }, + /** An array containing objects representing all of the moves the participant made when sorting. Each object represents a move. Each element in the array has a "src", "x", and "y" value. "src" is the image path, and "x" and "y" are the object location after the move. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + moves: { + type: ParameterType.COMPLEX, + array: true, + parameters: { + src: { + type: ParameterType.STRING, + }, + x: { + type: ParameterType.INT, + }, + y: { + type: ParameterType.INT, + }, + }, + }, + /** 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. */ + final_locations: { + type: ParameterType.COMPLEX, + array: true, + parameters: { + src: { + type: ParameterType.STRING, + }, + x: { + type: ParameterType.INT, + }, + y: { + type: ParameterType.INT, + }, + }, + }, + /** The response time in milliseconds for the participant to finish all sorting. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **free-sort** - * - * jsPsych plugin for drag-and-drop sorting of a collection of images + * The free-sort plugin displays one or more images on the screen that the participant can interact with by clicking and dragging with a mouse, or touching and dragging with a touchscreen device. When the trial starts, the images can be positioned outside or inside the sort area. All images must be moved into the sorting area before the participant can click a button to end the trial. All of the moves that the participant performs are recorded, as well as the final positions of all images. This plugin could be useful when asking participants to position images based on similarity to one another, or to recall image spatial locations. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-free-sort/ free-sort plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/free-sort/ free-sort plugin documentation on jspsych.org} */ class FreeSortPlugin implements JsPsychPlugin { static info = info; @@ -447,8 +474,6 @@ class FreeSortPlugin implements JsPsychPlugin { rt: rt, }; - // advance to next part - display_element.innerHTML = ""; this.jsPsych.finishTrial(trial_data); } }); diff --git a/packages/plugin-fullscreen/src/index.spec.ts b/packages/plugin-fullscreen/src/index.spec.ts index 19f0af7d9c..fac7a44c04 100644 --- a/packages/plugin-fullscreen/src/index.spec.ts +++ b/packages/plugin-fullscreen/src/index.spec.ts @@ -20,7 +20,7 @@ describe("fullscreen plugin", () => { ]); expect(document.documentElement.requestFullscreen).not.toHaveBeenCalled(); - clickTarget(document.querySelector("#jspsych-fullscreen-btn")); + await clickTarget(document.querySelector("#jspsych-fullscreen-btn")); expect(document.documentElement.requestFullscreen).toHaveBeenCalled(); }); diff --git a/packages/plugin-fullscreen/src/index.ts b/packages/plugin-fullscreen/src/index.ts index 4978d8df1b..44642af0e7 100644 --- a/packages/plugin-fullscreen/src/index.ts +++ b/packages/plugin-fullscreen/src/index.ts @@ -1,49 +1,63 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "fullscreen", + version: version, parameters: { - /** If true, experiment will enter fullscreen mode. If false, the browser will exit fullscreen mode. */ + /** A value of `true` causes the experiment to enter fullscreen mode. A value of `false` causes the browser to exit fullscreen mode. */ fullscreen_mode: { type: ParameterType.BOOL, - pretty_name: "Fullscreen mode", default: true, array: false, }, - /** HTML content to display above the button to enter fullscreen mode */ + /** `

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

` | The HTML content to display above the button to enter fullscreen mode. */ message: { type: ParameterType.HTML_STRING, - pretty_name: "Message", default: "

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

", array: false, }, - /** The text that appears on the button to enter fullscreen */ + /** The text that appears on the button to enter fullscreen mode. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", array: false, }, - /** The length of time to delay after entering fullscreen mode before ending the trial. */ + /** The length of time to delay after entering fullscreen mode before ending the trial. This can be useful because entering fullscreen is jarring and most browsers display some kind of message that the browser has entered fullscreen mode. */ delay_after: { type: ParameterType.INT, - pretty_name: "Delay after", default: 1000, array: false, }, }, + data: { + /** true if the browser supports fullscreen mode (i.e., is not Safari) */ + success: { + type: ParameterType.BOOL, + default: null, + description: "True if the user entered fullscreen mode, false if not.", + }, + /** Response time to click the button that launches fullscreen mode */ + rt: { + type: ParameterType.INT, + default: null, + description: "Time in milliseconds until the user entered fullscreen mode.", + }, + }, }; type Info = typeof info; /** - * **fullscreen** + * The fullscreen plugin allows the experiment to enter or exit fullscreen mode. For security reasons, all browsers require that entry into fullscreen mode is triggered by a user action. To enter fullscreen mode, this plugin has the user click a button. Exiting fullscreen mode can be done without user input. * - * jsPsych plugin for toggling fullscreen mode in the browser + * !!! warning + * Safari does not support keyboard input when the browser is in fullscreen mode. Therefore, the function will not launch fullscreen mode on Safari. The experiment will ignore any trials using the fullscreen plugin in Safari. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-fullscreen/ fullscreen plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/fullscreen/ fullscreen plugin documentation on jspsych.org} */ class FullscreenPlugin implements JsPsychPlugin { static info = info; diff --git a/packages/plugin-html-audio-response/src/index.ts b/packages/plugin-html-audio-response/src/index.ts index dfe602a78b..dc56b782bf 100644 --- a/packages/plugin-html-audio-response/src/index.ts +++ b/packages/plugin-html-audio-response/src/index.ts @@ -1,63 +1,110 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "html-audio-response", + version: version, parameters: { - /** The HTML string to be displayed */ + /** The HTML content to be displayed. */ stimulus: { type: ParameterType.HTML_STRING, default: undefined, }, - /** How long to show the stimulus. */ + /** How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. */ stimulus_duration: { type: ParameterType.INT, default: null, }, - /** How long to show the trial. */ + /** The maximum length of the recording, in milliseconds. The default value is intentionally set low because of the potential to accidentally record very large data files if left too high. You can set this to `null` to allow the participant to control the length of the recording via the done button, but be careful with this option as it can lead to crashing the browser if the participant waits too long to stop the recording. */ recording_duration: { type: ParameterType.INT, default: 2000, }, - /** Whether or not to show a button to end the recording. If false, the recording_duration must be set. */ + /** Whether to show a button on the screen that the participant can click to finish the recording. */ show_done_button: { type: ParameterType.BOOL, default: true, }, - /** Label for the done (stop recording) button. Only used if show_done_button is true. */ + /** The label for the done button. */ done_button_label: { type: ParameterType.STRING, default: "Continue", }, - /** Label for the record again button (only used if allow_playback is true). */ + /** The label for the record again button enabled when `allow_playback: true`. + */ record_again_button_label: { type: ParameterType.STRING, default: "Record again", }, - /** Label for the button to accept the audio recording (only used if allow_playback is true). */ + /** The label for the accept button enabled when `allow_playback: true`. */ accept_button_label: { type: ParameterType.STRING, default: "Continue", }, - /** Whether or not to allow the participant to playback the recording and either accept or re-record. */ + /** Whether to allow the participant to listen to their recording and decide whether to rerecord or not. If `true`, then the participant will be shown an interface to play their recorded audio and click one of two buttons to either accept the recording or rerecord. If rerecord is selected, then stimulus will be shown again, as if the trial is starting again from the beginning. */ allow_playback: { type: ParameterType.BOOL, default: false, }, - /** Whether or not to save the video URL to the trial data. */ + /** If `true`, then an [Object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) will be generated and stored for the recorded audio. Only set this to `true` if you plan to reuse the recorded audio later in the experiment, as it is a potentially memory-intensive feature. */ save_audio_url: { type: ParameterType.BOOL, default: false, }, }, + data: { + /** The time, since the onset of the stimulus, for the participant to click the done button. If the button is not clicked (or not enabled), then `rt` will be `null`. */ + rt: { + type: ParameterType.INT, + }, + /** The base64-encoded audio data. */ + response: { + type: ParameterType.STRING, + }, + /** The HTML content that was displayed on the screen. */ + stimulus: { + type: ParameterType.HTML_STRING, + }, + /** This is an estimate of when the stimulus appeared relative to the start of the audio recording. The plugin is configured so that the recording should start prior to the display of the stimulus. We have not yet been able to verify the accuracy of this estimate with external measurement devices. */ + estimated_stimulus_onset: { + type: ParameterType.INT, + }, + /** A URL to a copy of the audio data. */ + audio_url: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * html-audio-response - * jsPsych plugin for displaying a stimulus and recording an audio response through a microphone + * This plugin displays HTML content and records audio from the participant via a microphone. + * + * In order to get access to the microphone, you need to use the [initialize-microphone plugin](initialize-microphone.md) on your timeline prior to using this plugin. + * Once access is granted for an experiment you do not need to get permission again. + * + * This plugin records audio data in [base 64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64). + * This is a text-based representation of the audio which can be coverted to various audio formats using a variety of [online tools](https://www.google.com/search?q=base64+audio+decoder) as well as in languages like python and R. + * + * **This plugin will generate a large amount of data, and you will need to be careful about how you handle this data.** + * Even a few seconds of audio recording will add 10s of kB to jsPsych's data. + * Multiply this by a handful (or more) of trials, and the data objects will quickly get large. + * If you need to record a lot of audio, either many trials worth or just a few trials with longer responses, we recommend that you save the data to your server immediately after the trial and delete the data in jsPsych's data object. + * See below for an example of how to do this. + * + * This plugin also provides the option to store the recorded audio files as [Object URLs](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) via `save_audio_url: true`. + * This will generate a URL that is storing a copy of the recorded audio, which can be used for subsequent playback. + * See below for an example where the recorded audio is used as the stimulus in a subsequent trial. + * This feature is turned off by default because it uses a relatively large amount of memory compared to most jsPsych features. + * If you are running an experiment where you need this feature and you are recording lots of audio snippets, you may want to manually revoke the URLs when you no longer need them using [`URL.revokeObjectURL(objectURL)`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL). + * + * !!! warning + * When recording from a microphone your experiment will need to be running over `https://` protocol. If you try to run the experiment locally using the `file://` protocol or over `http://` protocol you will not be able to access the microphone because of [potential security problems](https://blog.mozilla.org/webrtc/camera-microphone-require-https-in-firefox-68/). + * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-html-audio-response/ html-audio-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/html-audio-response/ html-audio-response plugin documentation on jspsych.org} */ class HtmlAudioResponsePlugin implements JsPsychPlugin { static info = info; @@ -222,9 +269,6 @@ class HtmlAudioResponsePlugin implements JsPsychPlugin { this.recorder.removeEventListener("start", this.start_event_handler); this.recorder.removeEventListener("stop", this.stop_event_handler); - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial var trial_data: any = { rt: this.rt, @@ -239,9 +283,6 @@ class HtmlAudioResponsePlugin implements JsPsychPlugin { URL.revokeObjectURL(this.audio_url); } - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); } diff --git a/packages/plugin-html-button-response/src/index.spec.ts b/packages/plugin-html-button-response/src/index.spec.ts index a5dcf36e7e..2dd527bfe7 100644 --- a/packages/plugin-html-button-response/src/index.spec.ts +++ b/packages/plugin-html-button-response/src/index.spec.ts @@ -5,7 +5,7 @@ import htmlButtonResponse from "."; jest.useFakeTimers(); describe("html-button-response", () => { - test("displays html stimulus", async () => { + it("displays html stimulus and buttons", async () => { const { getHTML } = await startTimeline([ { type: htmlButtonResponse, @@ -14,39 +14,43 @@ describe("html-button-response", () => { }, ]); - expect(getHTML()).toContain( - '
this is html
' + expect(getHTML()).toMatchInlineSnapshot( + '"
this is html
"' ); }); - test("display button labels", async () => { + it("respects the `button_html` parameter", async () => { + const buttonHtmlFn = jest.fn(); + buttonHtmlFn.mockReturnValue(""); + const { getHTML } = await startTimeline([ { type: htmlButtonResponse, stimulus: "this is html", - choices: ["button-choice1", "button-choice2"], + choices: ["buttonChoice"], + button_html: buttonHtmlFn, }, ]); - expect(getHTML()).toContain(''); - expect(getHTML()).toContain(''); + expect(buttonHtmlFn).toHaveBeenCalledWith("buttonChoice", 0); + expect(getHTML()).toContain("something-unique"); }); - test("display button html", async () => { + test("prompt should append below button", async () => { const { getHTML } = await startTimeline([ { type: htmlButtonResponse, stimulus: "this is html", - choices: ["buttonChoice"], - button_html: '', + choices: ["button-choice"], + prompt: "

this is a prompt

", }, ]); - expect(getHTML()).toContain(''); + expect(getHTML()).toContain("

this is a prompt

"); }); - test("display should clear after button click", async () => { - const { getHTML, expectFinished } = await startTimeline([ + it("should clear the display after the button has been clicked", async () => { + const { getHTML, expectFinished, displayElement } = await startTimeline([ { type: htmlButtonResponse, stimulus: "this is html", @@ -54,31 +58,14 @@ describe("html-button-response", () => { }, ]); - expect(getHTML()).toContain( - '
this is html
' - ); - - clickTarget(document.querySelector("#jspsych-html-button-response-button-0")); + await clickTarget(displayElement.querySelector('[data-choice="0"]')); await expectFinished(); - }); - test("prompt should append below button", async () => { - const { getHTML } = await startTimeline([ - { - type: htmlButtonResponse, - stimulus: "this is html", - choices: ["button-choice"], - prompt: "

this is a prompt

", - }, - ]); - - expect(getHTML()).toContain( - '

this is a prompt

' - ); + expect(getHTML()).toEqual(""); }); - test("should hide stimulus if stimulus-duration is set", async () => { + it("should hide stimulus if stimulus-duration is set", async () => { const { displayElement } = await startTimeline([ { type: htmlButtonResponse, @@ -98,7 +85,7 @@ describe("html-button-response", () => { expect(stimulusElement.style.visibility).toBe("hidden"); }); - test("should end trial when trial duration is reached", async () => { + it("should end trial when trial duration is reached", async () => { const { getHTML, expectFinished } = await startTimeline([ { type: htmlButtonResponse, @@ -116,8 +103,8 @@ describe("html-button-response", () => { await expectFinished(); }); - test("should end trial when button is clicked", async () => { - const { getHTML, expectFinished } = await startTimeline([ + it("should end trial when button is clicked", async () => { + const { getHTML, expectFinished, displayElement } = await startTimeline([ { type: htmlButtonResponse, stimulus: "this is html", @@ -130,12 +117,12 @@ describe("html-button-response", () => { '
this is html
' ); - clickTarget(document.querySelector("#jspsych-html-button-response-button-0")); + await clickTarget(displayElement.querySelector('[data-choice="0"]')); await expectFinished(); }); test("class should have responded when button is clicked", async () => { - const { getHTML } = await startTimeline([ + const { getHTML, displayElement } = await startTimeline([ { type: htmlButtonResponse, stimulus: "this is html", @@ -148,10 +135,10 @@ describe("html-button-response", () => { '
this is html
' ); - clickTarget(document.querySelector("#jspsych-html-button-response-button-0")); - expect(document.querySelector("#jspsych-html-button-response-stimulus").className).toBe( - " responded" - ); + await clickTarget(displayElement.querySelector('[data-choice="0"]')); + expect( + displayElement.querySelector("#jspsych-html-button-response-stimulus").classList + ).toContain("responded"); }); test("buttons should be disabled first and then enabled after enable_button_after is set", async () => { diff --git a/packages/plugin-html-button-response/src/index.ts b/packages/plugin-html-button-response/src/index.ts index 6f18cdc5eb..81b3dda8fb 100644 --- a/packages/plugin-html-button-response/src/index.ts +++ b/packages/plugin-html-button-response/src/index.ts @@ -1,80 +1,99 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "html-button-response", + version: version, parameters: { - /** The HTML string to be displayed */ + /** The HTML content to be displayed. */ stimulus: { type: ParameterType.HTML_STRING, - pretty_name: "Stimulus", default: undefined, }, - /** Array containing the label(s) for the button(s). */ + /** Labels for the buttons. Each different string in the array will generate a different button. */ choices: { type: ParameterType.STRING, - pretty_name: "Choices", default: undefined, array: true, }, - /** The HTML for creating button. Can create own style. Use the "%choice%" string to indicate where the label from the choices parameter should be inserted. */ + /** + * A function that generates the HTML for each button in the `choices` array. The function gets the string and index of the item in the `choices` array and should return valid HTML. If you want to use different markup for each button, you can do that by using a conditional on either parameter. The default parameter returns a button element with the text label of the choice. + */ button_html: { - type: ParameterType.HTML_STRING, - pretty_name: "Button HTML", - default: '', - array: true, + type: ParameterType.FUNCTION, + default: function (choice: string, choice_index: number) { + return ``; + }, }, - /** Any content here will be displayed under the button(s). */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to show the stimulus. */ + /** How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show the trial. */ + /** ow long to wait for the participant to make a response before ending the trial in milliseconds. If the participant fails to make a response before this timer is reached, the participant's response will be recorded as null for the trial and the trial will end. If the value of this parameter is null, the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** The vertical margin of the button. */ - margin_vertical: { + /** Setting to `'grid'` will make the container element have the CSS property `display: grid` and enable the use of `grid_rows` and `grid_columns`. Setting to `'flex'` will make the container element have the CSS property `display: flex`. You can customize how the buttons are laid out by adding inline CSS in the `button_html` parameter. */ + button_layout: { type: ParameterType.STRING, - pretty_name: "Margin vertical", - default: "0px", + default: "grid", }, - /** The horizontal margin of the button. */ - margin_horizontal: { - type: ParameterType.STRING, - pretty_name: "Margin horizontal", - default: "8px", + /** + * The number of rows in the button grid. Only applicable when `button_layout` is set to `'grid'`. If null, the number of rows will be determined automatically based on the number of buttons and the number of columns. + */ + grid_rows: { + type: ParameterType.INT, + default: 1, }, - /** If true, then trial will end when user responds. */ + /** + * The number of columns in the button grid. Only applicable when `button_layout` is set to `'grid'`. If null, the number of columns will be determined automatically based on the number of buttons and the number of rows. + */ + grid_columns: { + type: ParameterType.INT, + default: null, + }, + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the value for `trial_duration` is reached. You can set this parameter to `false` to force the participant to view a stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** The delay of enabling button */ + /** How long the button will delay enabling in milliseconds. */ enable_button_after: { type: ParameterType.INT, - pretty_name: "Enable button after", default: 0, }, }, + data: { + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** Indicates which button the participant pressed. The first button in the `choices` array is 0, the second is 1, and so on. */ + response: { + type: ParameterType.INT, + }, + /** The HTML content that was displayed on the screen. */ + stimulus: { + type: ParameterType.HTML_STRING, + }, + }, }; type Info = typeof info; /** - * html-button-response - * jsPsych plugin for displaying a stimulus and getting a button response + * This plugin displays HTML content and records responses generated by button click. The stimulus can be displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant has failed to respond within a fixed length of time. The button itself can be customized using HTML formatting. + * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-html-button-response/ html-button-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/html-button-response/ html-button-response plugin documentation on jspsych.org} */ class HtmlButtonResponsePlugin implements JsPsychPlugin { static info = info; @@ -82,62 +101,56 @@ class HtmlButtonResponsePlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { - // display stimulus - var html = '
' + trial.stimulus + "
"; - - //display buttons - var buttons = []; - if (Array.isArray(trial.button_html)) { - if (trial.button_html.length == trial.choices.length) { - buttons = trial.button_html; - } else { - console.error( - "Error in html-button-response plugin. The length of the button_html array does not equal the length of the choices array" + // Display stimulus + const stimulusElement = document.createElement("div"); + stimulusElement.id = "jspsych-html-button-response-stimulus"; + stimulusElement.innerHTML = trial.stimulus; + + display_element.appendChild(stimulusElement); + + // Display buttons + const buttonGroupElement = document.createElement("div"); + buttonGroupElement.id = "jspsych-html-button-response-btngroup"; + if (trial.button_layout === "grid") { + buttonGroupElement.classList.add("jspsych-btn-group-grid"); + if (trial.grid_rows === null && trial.grid_columns === null) { + throw new Error( + "You cannot set `grid_rows` to `null` without providing a value for `grid_columns`." ); } - } else { - for (var i = 0; i < trial.choices.length; i++) { - buttons.push(trial.button_html); - } + const n_cols = + trial.grid_columns === null + ? Math.ceil(trial.choices.length / trial.grid_rows) + : trial.grid_columns; + const n_rows = + trial.grid_rows === null + ? Math.ceil(trial.choices.length / trial.grid_columns) + : trial.grid_rows; + buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`; + buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`; + } else if (trial.button_layout === "flex") { + buttonGroupElement.classList.add("jspsych-btn-group-flex"); } - html += '
'; - for (var i = 0; i < trial.choices.length; i++) { - var str = buttons[i].replace(/%choice%/g, trial.choices[i]); - html += - '
' + - str + - "
"; + + for (const [choiceIndex, choice] of trial.choices.entries()) { + buttonGroupElement.insertAdjacentHTML("beforeend", trial.button_html(choice, choiceIndex)); + const buttonElement = buttonGroupElement.lastChild as HTMLElement; + buttonElement.dataset.choice = choiceIndex.toString(); + buttonElement.addEventListener("click", () => { + after_response(choiceIndex); + }); } - html += "
"; - //show prompt if there is one + display_element.appendChild(buttonGroupElement); + + // Show prompt if there is one if (trial.prompt !== null) { - html += trial.prompt; + display_element.insertAdjacentHTML("beforeend", trial.prompt); } - display_element.innerHTML = html; // start time var start_time = performance.now(); - // add event listeners to buttons - for (var i = 0; i < trial.choices.length; i++) { - display_element - .querySelector("#jspsych-html-button-response-button-" + i) - .addEventListener("click", (e) => { - var btn_el = e.currentTarget as HTMLButtonElement; - var choice = btn_el.getAttribute("data-choice"); // don't use dataset for jsdom compatibility - after_response(choice); - }); - } - // store response var response = { rt: null, @@ -146,9 +159,6 @@ class HtmlButtonResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial var trial_data = { rt: response.rt, @@ -156,9 +166,6 @@ class HtmlButtonResponsePlugin implements JsPsychPlugin { response: response.button, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; @@ -173,14 +180,11 @@ class HtmlButtonResponsePlugin implements JsPsychPlugin { // after a valid response, the stimulus will have the CSS class 'responded' // which can be used to provide visual feedback that a response was recorded - display_element.querySelector("#jspsych-html-button-response-stimulus").className += - " responded"; + stimulusElement.classList.add("responded"); // disable all the buttons after a response - var btns = document.querySelectorAll(".jspsych-html-button-response-button button"); - for (var i = 0; i < btns.length; i++) { - //btns[i].removeEventListener('click'); - btns[i].setAttribute("disabled", "disabled"); + for (const button of buttonGroupElement.children) { + button.setAttribute("disabled", "disabled"); } if (trial.response_ends_trial) { @@ -191,9 +195,7 @@ class HtmlButtonResponsePlugin implements JsPsychPlugin { // hide image if timing is set if (trial.stimulus_duration !== null) { this.jsPsych.pluginAPI.setTimeout(() => { - display_element.querySelector( - "#jspsych-html-button-response-stimulus" - ).style.visibility = "hidden"; + stimulusElement.style.visibility = "hidden"; }, trial.stimulus_duration); } @@ -264,7 +266,9 @@ class HtmlButtonResponsePlugin implements JsPsychPlugin { if (data.rt !== null) { this.jsPsych.pluginAPI.clickTarget( - display_element.querySelector(`div[data-choice="${data.response}"] button`), + display_element.querySelector( + `#jspsych-html-button-response-btngroup [data-choice="${data.response}"]` + ), data.rt ); } diff --git a/packages/plugin-html-keyboard-response/src/index.spec.ts b/packages/plugin-html-keyboard-response/src/index.spec.ts index 3428271970..ce7608a3df 100644 --- a/packages/plugin-html-keyboard-response/src/index.spec.ts +++ b/packages/plugin-html-keyboard-response/src/index.spec.ts @@ -14,7 +14,7 @@ describe("html-keyboard-response", () => { ]); expect(getHTML()).toBe('
this is html
'); - pressKey("a"); + await pressKey("a"); await expectFinished(); }); @@ -30,7 +30,8 @@ describe("html-keyboard-response", () => { '
this is html
' ); - pressKey("f"); + await pressKey("f"); + expect(getHTML()).toBe(""); await expectFinished(); }); @@ -48,7 +49,7 @@ describe("html-keyboard-response", () => { '
this is html
this is a prompt
' ); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -74,7 +75,7 @@ describe("html-keyboard-response", () => { .visibility ).toBe("hidden"); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -107,7 +108,7 @@ describe("html-keyboard-response", () => { '
this is html
' ); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -125,7 +126,7 @@ describe("html-keyboard-response", () => { '
this is html
' ); - pressKey("f"); + await pressKey("f"); expect(document.querySelector("#jspsych-html-keyboard-response-stimulus").className).toBe( " responded" diff --git a/packages/plugin-html-keyboard-response/src/index.ts b/packages/plugin-html-keyboard-response/src/index.ts index d086ea5616..093fa821e8 100644 --- a/packages/plugin-html-keyboard-response/src/index.ts +++ b/packages/plugin-html-keyboard-response/src/index.ts @@ -1,72 +1,99 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "html-keyboard-response", + version: version, parameters: { /** - * The HTML string to be displayed. + * The string to be displayed. */ stimulus: { type: ParameterType.HTML_STRING, - pretty_name: "Stimulus", default: undefined, }, /** - * Array containing the key(s) the subject is allowed to press to respond to the stimulus. + * This array contains the key(s) that the participant is allowed to press in order to respond + * to the stimulus. Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see + * {@link https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values this page} + * and + * {@link https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/ this page (event.key column)} + * for more examples. Any key presses that are not listed in the + * array will be ignored. The default value of `"ALL_KEYS"` means that all keys will be accepted as valid responses. + * Specifying `"NO_KEYS"` will mean that no responses are allowed. */ choices: { type: ParameterType.KEYS, - pretty_name: "Choices", default: "ALL_KEYS", }, /** - * Any content here will be displayed below the stimulus. + * This string can contain HTML markup. Any content here will be displayed below the stimulus. + * The intention is that it can be used to provide a reminder about the action the participant + * is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, /** - * How long to show the stimulus. + * How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus + * will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will + * remain visible until the trial ends. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, /** - * How long to show trial before it ends. + * How long to wait for the participant to make a response before ending the trial in milliseconds. + * If the participant fails to make a response before this timer is reached, the participant's response + * will be recorded as null for the trial and the trial will end. If the value of this parameter is null, + * then the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, /** - * If true, trial will end when subject makes a response. + * If true, then the trial will end whenever the participant makes a response (assuming they make their + * response before the cutoff specified by the trial_duration parameter). If false, then the trial will + * continue until the value for trial_duration is reached. You can set this parameter to false to force + * the participant to view a stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, }, + data: { + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** The HTML content that was displayed on the screen. */ + stimulus: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * **html-keyboard-response** - * - * jsPsych plugin for displaying a stimulus and getting a keyboard response + * This plugin displays HTML content and records responses generated with the keyboard. + * The stimulus can be displayed until a response is given, or for a pre-determined amount of time. + * The trial can be ended automatically if the participant has failed to respond within a fixed length of time. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-html-keyboard-response/ html-keyboard-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/html-keyboard-response/ html-keyboard-response plugin documentation on jspsych.org} */ class HtmlKeyboardResponsePlugin implements JsPsychPlugin { static info = info; - constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { @@ -88,9 +115,6 @@ class HtmlKeyboardResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // kill keyboard listeners if (typeof keyboardListener !== "undefined") { this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); @@ -103,9 +127,6 @@ class HtmlKeyboardResponsePlugin implements JsPsychPlugin { response: response.key, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-html-slider-response/src/index.spec.ts b/packages/plugin-html-slider-response/src/index.spec.ts index df0e84506c..4e85869e09 100644 --- a/packages/plugin-html-slider-response/src/index.spec.ts +++ b/packages/plugin-html-slider-response/src/index.spec.ts @@ -4,6 +4,13 @@ import htmlSliderResponse from "."; jest.useFakeTimers(); +afterEach(async () => { + const nextButton = document.querySelector("#jspsych-html-slider-response-next"); + if (nextButton) { + await clickTarget(nextButton); + } +}); + describe("html-slider-response", () => { test("displays html stimulus", async () => { const { getHTML } = await startTimeline([ @@ -137,7 +144,7 @@ describe("html-slider-response", () => { '
this is html
' ); - clickTarget(document.querySelector("#jspsych-html-slider-response-next")); + await clickTarget(document.querySelector("#jspsych-html-slider-response-next")); await expectFinished(); }); diff --git a/packages/plugin-html-slider-response/src/index.ts b/packages/plugin-html-slider-response/src/index.ts index e78ace505e..1351cb25ff 100644 --- a/packages/plugin-html-slider-response/src/index.ts +++ b/packages/plugin-html-slider-response/src/index.ts @@ -1,100 +1,105 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "html-slider-response", + version: version, parameters: { /** The HTML string to be displayed */ stimulus: { type: ParameterType.HTML_STRING, - pretty_name: "Stimulus", default: undefined, }, /** Sets the minimum value of the slider. */ min: { type: ParameterType.INT, - pretty_name: "Min slider", default: 0, }, /** Sets the maximum value of the slider */ max: { type: ParameterType.INT, - pretty_name: "Max slider", default: 100, }, /** Sets the starting value of the slider */ slider_start: { type: ParameterType.INT, - pretty_name: "Slider starting value", default: 50, }, - /** Sets the step of the slider */ + /** Sets the step of the slider. This is the smallest amount by which the slider can change. */ step: { type: ParameterType.INT, - pretty_name: "Step", default: 1, }, - /** Array containing the labels for the slider. Labels will be displayed at equidistant locations along the slider. */ + /** Labels displayed at equidistant locations on the slider. For example, two labels will be placed at the ends of the slider. Three labels would place two at the ends and one in the middle. Four will place two at the ends, and the other two will be at 33% and 67% of the slider width. */ labels: { type: ParameterType.HTML_STRING, - pretty_name: "Labels", default: [], array: true, }, - /** Width of the slider in pixels. */ + /** Set the width of the slider in pixels. If left null, then the width will be equal to the widest element in the display. */ slider_width: { type: ParameterType.INT, - pretty_name: "Slider width", default: null, }, - /** Label of the button to advance. */ + /** Label of the button to end the trial. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", array: false, }, - /** If true, the participant will have to move the slider before continuing. */ + /** If true, the participant must move the slider before clicking the continue button. */ require_movement: { type: ParameterType.BOOL, - pretty_name: "Require movement", default: false, }, - /** Any content here will be displayed below the slider. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to show the stimulus. */ + /** How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show the trial. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the participant fails to make a response before this timer is reached, the participant's response will be recorded as null for the trial and the trial will end. If the value of this parameter is null, then the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** If true, trial will end when user makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the value for `trial_duration` is reached. You can set this parameter to `false` to force the participant to view a stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, }, + data: { + /** The time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** The numeric value of the slider. */ + response: { + type: ParameterType.INT, + }, + /** The HTML content that was displayed on the screen. */ + stimulus: { + type: ParameterType.HTML_STRING, + }, + /** The starting value of the slider. */ + slider_start: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **html-slider-response** - * - * jsPsych plugin for showing an HTML stimulus and collecting a slider response - * + * This plugin displays HTML content and allows the participant to respond by dragging a slider. * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-html-slider-response/ html-slider-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/html-slider-response/ html-slider-response plugin documentation on jspsych.org} */ class HtmlSliderResponsePlugin implements JsPsychPlugin { static info = info; @@ -189,8 +194,6 @@ class HtmlSliderResponsePlugin implements JsPsychPlugin { } const end_trial = () => { - this.jsPsych.pluginAPI.clearAllTimeouts(); - // save data var trialdata = { rt: response.rt, @@ -199,8 +202,6 @@ class HtmlSliderResponsePlugin implements JsPsychPlugin { response: response.response, }; - display_element.innerHTML = ""; - // next trial this.jsPsych.finishTrial(trialdata); }; diff --git a/packages/plugin-html-video-response/src/index.ts b/packages/plugin-html-video-response/src/index.ts index cdfd2f92cd..0c0b0acad3 100644 --- a/packages/plugin-html-video-response/src/index.ts +++ b/packages/plugin-html-video-response/src/index.ts @@ -1,19 +1,26 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "html-video-response", + version: version, parameters: { /** The HTML string to be displayed */ stimulus: { type: ParameterType.HTML_STRING, default: undefined, }, - /** How long to show the stimulus. */ + /** How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` + * after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. */ stimulus_duration: { type: ParameterType.INT, default: null, }, - /** How long to show the trial. */ + /** The maximum length of the recording, in milliseconds. The default value is intentionally set low because of the + * potential to accidentally record very large data files if left too high. You can set this to `null` to allow the + * participant to control the length of the recording via the done button, but be careful with this option as it can + * lead to crashing the browser if the participant waits too long to stop the recording. */ recording_duration: { type: ParameterType.INT, default: 2000, @@ -28,36 +35,85 @@ const info = { type: ParameterType.STRING, default: "Continue", }, - /** Label for the record again button (only used if allow_playback is true). */ + /** The label for the record again button enabled when `allow_playback: true`.*/ record_again_button_label: { type: ParameterType.STRING, default: "Record again", }, - /** Label for the button to accept the video recording (only used if allow_playback is true). */ + /** The label for the accept button enabled when `allow_playback: true`. */ accept_button_label: { type: ParameterType.STRING, default: "Continue", }, - /** Whether or not to allow the participant to playback the recording and either accept or re-record. */ + /** Whether to allow the participant to listen to their recording and decide whether to rerecord or not. If `true`, + * then the participant will be shown an interface to play their recorded video and click one of two buttons to + * either accept the recording or rerecord. If rerecord is selected, then stimulus will be shown again, as if the + * trial is starting again from the beginning. */ allow_playback: { type: ParameterType.BOOL, default: false, }, - /** Whether or not to save the video URL to the trial data. */ + /** If `true`, then an [Object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) will be + * generated and stored for the recorded video. Only set this to `true` if you plan to reuse the recorded video + * later in the experiment, as it is a potentially memory-intensive feature. */ save_video_url: { type: ParameterType.BOOL, default: false, }, }, + data: { + /** The time, since the onset of the stimulus, for the participant to click the done button. If the button is not clicked (or not enabled), then `rt` will be `null`. */ + rt: { + type: ParameterType.INT, + default: null, + }, + /** The HTML content that was displayed on the screen.*/ + stimulus: { + type: ParameterType.HTML_STRING, + }, + /** The base64-encoded video data. */ + response: { + type: ParameterType.STRING, + }, + /** A URL to a copy of the video data. */ + video_url: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * html-video-response - * jsPsych plugin for displaying a stimulus and recording a video response through a camera + * + * This plugin displays HTML content and records video from the participant via a webcam. + * + * In order to get access to the camera, you need to use the [initialize-camera plugin](initialize-camera.md) on your timeline prior to using this plugin. + * Once access is granted for an experiment you do not need to get permission again. + * + * This plugin records video data in [base 64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64). + * This is a text-based representation of the video which can be coverted to various video formats using a variety of [online tools](https://www.google.com/search?q=base64+video+decoder) as well as in languages like python and R. + * + * **This plugin will generate a large amount of data, and you will need to be careful about how you handle this data.** + * Even a few seconds of video recording will add 10s of kB to jsPsych's data. + * Multiply this by a handful (or more) of trials, and the data objects will quickly get large. + * If you need to record a lot of video, either many trials worth or just a few trials with longer responses, we recommend that you save the data to your server immediately after the trial and delete the data in jsPsych's data object. + * See below for an example of how to do this. + * + * This plugin also provides the option to store the recorded video files as [Object URLs](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) via `save_video_url: true`. + * This will generate a URL that stores a copy of the recorded video, which can be used for subsequent playback during the experiment. + * See below for an example where the recorded video is used as the stimulus in a subsequent trial. + * This feature is turned off by default because it uses a relatively large amount of memory compared to most jsPsych features. + * If you are running an experiment where you need this feature and you are recording lots of video clips, you may want to manually revoke the URLs when you no longer need them using [`URL.revokeObjectURL(objectURL)`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL). + * + * !!! warning + * When recording from a camera your experiment will need to be running over `https://` protocol. + * If you try to run the experiment locally using the `file://` protocol or over `http://` protocol you will not + * be able to access the camera because of + * [potential security problems](https://blog.mozilla.org/webrtc/camera-microphone-require-https-in-firefox-68/). + * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-html-video-response/ html-video-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/html-video-response/ html-video-response plugin documentation on jspsych.org} */ class HtmlVideoResponsePlugin implements JsPsychPlugin { static info = info; @@ -214,9 +270,6 @@ class HtmlVideoResponsePlugin implements JsPsychPlugin { this.recorder.removeEventListener("start", this.start_event_handler); this.recorder.removeEventListener("stop", this.stop_event_handler); - // clear any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial var trial_data: any = { rt: this.rt, @@ -230,9 +283,6 @@ class HtmlVideoResponsePlugin implements JsPsychPlugin { URL.revokeObjectURL(this.video_url); } - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); } diff --git a/packages/plugin-iat-html/src/index.spec.ts b/packages/plugin-iat-html/src/index.spec.ts index b2f0115ed1..ace002f92a 100644 --- a/packages/plugin-iat-html/src/index.spec.ts +++ b/packages/plugin-iat-html/src/index.spec.ts @@ -23,7 +23,7 @@ describe("iat-html plugin", () => { expect(getHTML()).toContain('

dogs

'); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -39,10 +39,10 @@ describe("iat-html plugin", () => { }, ]); - pressKey(" "); + await pressKey(" "); expect(getHTML()).toContain('

hello

'); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -58,10 +58,10 @@ describe("iat-html plugin", () => { }, ]); - pressKey(" "); + await pressKey(" "); expect(getHTML()).toContain('

hello

'); - pressKey("j"); + await pressKey("j"); await expectFinished(); }); @@ -80,10 +80,10 @@ describe("iat-html plugin", () => { }, ]); - pressKey("f"); + await pressKey("f"); expect(getHTML()).toContain('

hello

'); - pressKey(" "); + await pressKey(" "); await expectFinished(); }); @@ -102,10 +102,10 @@ describe("iat-html plugin", () => { }, ]); - pressKey("j"); + await pressKey("j"); expect(getHTML()).toContain('

hello

'); - pressKey("x"); + await pressKey("x"); await expectFinished(); }); @@ -125,7 +125,7 @@ describe("iat-html plugin", () => { expect(getHTML()).toContain("

Press j for:
UNFRIENDLY"); expect(getHTML()).toContain("

Press f for:
FRIENDLY"); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -150,10 +150,10 @@ describe("iat-html plugin", () => { expect(wrongImageContainer.style.visibility).toBe("hidden"); - pressKey("j"); + await pressKey("j"); expect(wrongImageContainer.style.visibility).toBe("visible"); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -191,7 +191,7 @@ describe("iat-html plugin", () => { }, ]); - pressKey("f"); + await pressKey("f"); expect(getHTML()).toContain('

hello

'); await expectRunning(); @@ -218,7 +218,7 @@ describe("iat-html plugin", () => { jest.advanceTimersByTime(500); - pressKey("i"); + await pressKey("i"); expect(displayElement.querySelector("#wrongImgContainer").style.visibility).toBe( "visible" ); @@ -247,14 +247,14 @@ describe("iat-html plugin", () => { expect(getHTML()).toContain('

hello

'); - pressKey("i"); + await pressKey("i"); expect(getHTML()).toContain('

hello

'); jest.advanceTimersByTime(1000); expect(getHTML()).toContain('

hello

'); jest.advanceTimersByTime(500); - pressKey("e"); + await pressKey("e"); await expectFinished(); }); @@ -276,7 +276,7 @@ describe("iat-html plugin", () => { ]); expect(getHTML()).toContain('

dogs

'); - pressKey("j"); + await pressKey("j"); await expectFinished(); }); diff --git a/packages/plugin-iat-html/src/index.ts b/packages/plugin-iat-html/src/index.ts index 8f0220c6c4..b6d5946c27 100644 --- a/packages/plugin-iat-html/src/index.ts +++ b/packages/plugin-iat-html/src/index.ts @@ -1,101 +1,125 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "iat-html", + version: version, parameters: { /** The HTML string to be displayed. */ stimulus: { type: ParameterType.HTML_STRING, - pretty_name: "Stimulus", default: undefined, }, - /** Key press that is associated with the left category label.*/ + /** Key press that is associated with the `left_category_label`. */ left_category_key: { type: ParameterType.KEY, - pretty_name: "Left category key", default: "e", }, - /** Key press that is associated with the right category label. */ + /** Key press that is associated with the `right_category_label`. */ right_category_key: { type: ParameterType.KEY, - pretty_name: "Right category key", default: "i", }, - /** The label that is associated with the stimulus. Aligned to the left side of page */ + /** An array that contains the words/labels associated with a certain stimulus. The labels are aligned to the left + * side of the page. */ left_category_label: { type: ParameterType.STRING, - pretty_name: "Left category label", array: true, default: ["left"], }, - /** The label that is associated with the stimulus. Aligned to the right side of the page. */ + /** An array that contains the words/labels associated with a certain stimulus. The labels are aligned to the right + * side of the page. */ right_category_label: { type: ParameterType.STRING, - pretty_name: "Right category label", array: true, default: ["right"], }, - /** Array containing the key(s) that allow the user to advance to the next trial if their key press was incorrect. */ + /** This array contains the characters the participant is allowed to press to move on to the next trial if their key + * press was incorrect and feedback was displayed. Can also have 'other key' as an option which will only allow the + * user to select the right key to move forward. */ key_to_move_forward: { type: ParameterType.KEYS, - pretty_name: "Key to move forward", default: "ALL_KEYS", }, - /** If true, then html when wrong will be displayed when user makes an incorrect key press. */ + /** If `true`, then `html_when_wrong` and `wrong_image_name` is required. If `false`, `trial_duration` is needed + * and trial will continue automatically. */ display_feedback: { type: ParameterType.BOOL, - pretty_name: "Display feedback", default: false, }, - /** The HTML to display when a user presses the wrong key. */ + /** The content to display when a user presses the wrong key. */ html_when_wrong: { type: ParameterType.HTML_STRING, - pretty_name: "HTML when wrong", default: 'X', }, - /** Instructions shown at the bottom of the page. */ + /** Instructions about making a wrong key press and whether another key press is needed to continue. */ bottom_instructions: { type: ParameterType.HTML_STRING, - pretty_name: "Bottom instructions", default: "

If you press the wrong key, a red X will appear. Press any key to continue.

", }, - /** If true, in order to advance to the next trial after a wrong key press the user will be forced to press the correct key. */ + /** If this is `true` and the user presses the wrong key then they have to press the other key to continue. An example + * would be two keys 'e' and 'i'. If the key associated with the stimulus is 'e' and key 'i' was pressed, then + * pressing 'e' is needed to continue the trial. When this is `true`, then parameter `key_to_move_forward` + * is not used. If this is `true` and the user presses the wrong key then they have to press the other key to + * continue. An example would be two keys 'e' and 'i'. If the key associated with the stimulus is 'e' and key + * 'i' was pressed, then pressing 'e' is needed to continue the trial. When this is `true`, then parameter + * `key_to_move_forward` is not used. */ force_correct_key_press: { type: ParameterType.BOOL, - pretty_name: "Force correct key press", default: false, }, - /** Stimulus will be associated with either "left" or "right". */ + /** Either 'left' or 'right'. This indicates whether the stimulus is associated with the key press and + * category on the left or right side of the page (`left_category_key` or `right_category_key`). */ stim_key_association: { type: ParameterType.SELECT, - pretty_name: "Stimulus key association", options: ["left", "right"], default: undefined, }, - /** If true, trial will end when user makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their + * response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will + * continue until the value for `trial_duration` is reached. You can use this parameter to force the participant + * to view a stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** How long to show the trial. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the + * participant fails to make a response before this timer is reached, the participant's response will be + * recorded as `null` for the trial and the trial will end. If the value of this parameter is `null`, then + * the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, }, + data: { + /** The string containing the HTML-formatted content that the participant saw on this trial. */ + stimulus: { + type: ParameterType.HTML_STRING, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** Boolean indicating whether the user's key press was correct or incorrect for the given stimulus. */ + correct: { + type: ParameterType.BOOL, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **iat-html** - * - * jsPsych plugin for running an IAT (Implicit Association Test) with an HTML-formatted stimulus + * This plugin runs a single trial of the [implicit association test (IAT)](https://implicit.harvard.edu/implicit/iatdetails.html), using HTML content as the stimulus. * * @author Kristin Diep - * @see {@link https://www.jspsych.org/plugins/jspsych-iat-html/ iat-html plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/iat-html/ iat-html plugin documentation on jspsych.org} */ class IatHtmlPlugin implements JsPsychPlugin { static info = info; @@ -178,9 +202,6 @@ class IatHtmlPlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // kill keyboard listeners if (typeof keyboardListener !== "undefined") { this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); @@ -194,9 +215,6 @@ class IatHtmlPlugin implements JsPsychPlugin { correct: response.correct, }; - // clears the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-iat-image/src/index.spec.ts b/packages/plugin-iat-image/src/index.spec.ts index 3552a680bc..30833dd0db 100644 --- a/packages/plugin-iat-image/src/index.spec.ts +++ b/packages/plugin-iat-image/src/index.spec.ts @@ -23,7 +23,7 @@ describe("iat-image plugin", () => { expect(getHTML()).toContain("blue.png"); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -39,10 +39,10 @@ describe("iat-image plugin", () => { }, ]); - pressKey("j"); + await pressKey("j"); expect(getHTML()).toContain(''); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -58,10 +58,10 @@ describe("iat-image plugin", () => { }, ]); - pressKey("f"); + await pressKey("f"); expect(getHTML()).toContain(''); - pressKey("j"); + await pressKey("j"); await expectFinished(); }); @@ -80,12 +80,12 @@ describe("iat-image plugin", () => { }, ]); - pressKey("f"); + await pressKey("f"); expect(getHTML()).toContain( '' ); - pressKey("a"); + await pressKey("a"); await expectFinished(); }); @@ -104,12 +104,12 @@ describe("iat-image plugin", () => { }, ]); - pressKey("j"); + await pressKey("j"); expect(getHTML()).toContain( '' ); - pressKey("x"); + await pressKey("x"); await expectFinished(); }); @@ -129,7 +129,7 @@ describe("iat-image plugin", () => { expect(getHTML()).toContain("

Press j for:
UNFRIENDLY"); expect(getHTML()).toContain("

Press f for:
FRIENDLY"); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -152,10 +152,10 @@ describe("iat-image plugin", () => { const wrongImageContainer = displayElement.querySelector("#wrongImgContainer"); expect(wrongImageContainer.style.visibility).toBe("hidden"); - pressKey("j"); + await pressKey("j"); expect(wrongImageContainer.style.visibility).toBe("visible"); - pressKey("a"); + await pressKey("a"); await expectFinished(); }); @@ -193,7 +193,7 @@ describe("iat-image plugin", () => { }, ]); - pressKey("f"); + await pressKey("f"); expect(getHTML()).toContain( '' ); @@ -242,7 +242,7 @@ describe("iat-image plugin", () => { jest.advanceTimersByTime(500); - pressKey("i"); + await pressKey("i"); expect(displayElement.querySelector("#wrongImgContainer").style.visibility).toBe( "visible" ); @@ -294,7 +294,7 @@ describe("iat-image plugin", () => { expect(getHTML()).toContain(''); - pressKey("i"); + await pressKey("i"); expect(getHTML()).toContain( '' ); @@ -307,7 +307,7 @@ describe("iat-image plugin", () => { jest.advanceTimersByTime(500); - pressKey("a"); + await pressKey("a"); await expectFinished(); }); @@ -329,7 +329,7 @@ describe("iat-image plugin", () => { ]); expect(getHTML()).toContain("blue.png"); - pressKey("j"); + await pressKey("j"); await expectFinished(); }); diff --git a/packages/plugin-iat-image/src/index.ts b/packages/plugin-iat-image/src/index.ts index 79f070f392..b67dcb63d0 100644 --- a/packages/plugin-iat-image/src/index.ts +++ b/packages/plugin-iat-image/src/index.ts @@ -1,101 +1,125 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "iat-image", + version: version, parameters: { - /** The image to be displayed */ + /** The stimulus to display. The path to an image. */ stimulus: { type: ParameterType.IMAGE, - pretty_name: "Stimulus", default: undefined, }, - /** Key press that is associated with the left category label. */ + /** Key press that is associated with the `left_category_label`. */ left_category_key: { type: ParameterType.KEY, - pretty_name: "Left category key", default: "e", }, - /** Key press that is associated with the right category label. */ + /** Key press that is associated with the `right_category_label`. */ right_category_key: { type: ParameterType.KEY, - pretty_name: "Right category key", default: "i", }, - /** The label that is associated with the stimulus. Aligned to the left side of page. */ + /** An array that contains the words/labels associated with a certain stimulus. The labels are aligned to the left + * side of the page. */ left_category_label: { type: ParameterType.STRING, - pretty_name: "Left category label", array: true, default: ["left"], }, - /** The label that is associated with the stimulus. Aligned to the right side of the page. */ + /** An array that contains the words/labels associated with a certain stimulus. The labels are aligned to the right + * side of the page. */ right_category_label: { type: ParameterType.STRING, - pretty_name: "Right category label", array: true, default: ["right"], }, - /** Array containing the key(s) that allow the user to advance to the next trial if their key press was incorrect. */ + /** This array contains the characters the participant is allowed to press to move on to the next trial if their key + * press was incorrect and feedback was displayed. Can also have 'other key' as an option which will only allow the + * user to select the right key to move forward. */ key_to_move_forward: { type: ParameterType.KEYS, - pretty_name: "Key to move forward", default: "ALL_KEYS", }, - /** If true, then html when wrong will be displayed when user makes an incorrect key press. */ + /** If `true`, then `html_when_wrong` and `wrong_image_name` is required. If `false`, `trial_duration` is needed + * and trial will continue automatically. */ display_feedback: { type: ParameterType.BOOL, - pretty_name: "Display feedback", default: false, }, - /** The HTML to display when a user presses the wrong key. */ + /** The content to display when a user presses the wrong key. */ html_when_wrong: { type: ParameterType.HTML_STRING, - pretty_name: "HTML when wrong", default: 'X', }, - /** Instructions shown at the bottom of the page. */ + /** Instructions about making a wrong key press and whether another key press is needed to continue. */ bottom_instructions: { type: ParameterType.HTML_STRING, - pretty_name: "Bottom instructions", default: "

If you press the wrong key, a red X will appear. Press any key to continue.

", }, - /** If true, in order to advance to the next trial after a wrong key press the user will be forced to press the correct key. */ + /** If this is `true` and the user presses the wrong key then they have to press the other key to continue. An example + * would be two keys 'e' and 'i'. If the key associated with the stimulus is 'e' and key 'i' was pressed, then + * pressing 'e' is needed to continue the trial. When this is `true`, then parameter `key_to_move_forward` + * is not used. If this is `true` and the user presses the wrong key then they have to press the other key to + * continue. An example would be two keys 'e' and 'i'. If the key associated with the stimulus is 'e' and key + * 'i' was pressed, then pressing 'e' is needed to continue the trial. When this is `true`, then parameter + * `key_to_move_forward` is not used. */ force_correct_key_press: { type: ParameterType.BOOL, - pretty_name: "Force correct key press", default: false, }, - /** Stimulus will be associated with either "left" or "right". */ + /** Either 'left' or 'right'. This indicates whether the stimulus is associated with the key press and + * category on the left or right side of the page (`left_category_key` or `right_category_key`). */ stim_key_association: { type: ParameterType.SELECT, - pretty_name: "Stimulus key association", options: ["left", "right"], default: undefined, }, - /** If true, trial will end when user makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their + * response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will + * continue until the value for `trial_duration` is reached. You can use this parameter to force the participant + * to view a stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** How long to show the trial. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the + * participant fails to make a response before this timer is reached, the participant's response will be + * recorded as `null` for the trial and the trial will end. If the value of this parameter is `null`, then + * the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, }, + data: { + /** The path to the image file that the participant saw on this trial. */ + stimulus: { + type: ParameterType.STRING, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** Boolean indicating whether the user's key press was correct or incorrect for the given stimulus. */ + correct: { + type: ParameterType.BOOL, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **iat-image** - * - * jsPsych plugin for running an IAT (Implicit Association Test) with an image stimulus + * This plugin runs a single trial of the [implicit association test (IAT)](https://implicit.harvard.edu/implicit/iatdetails.html), using an image as the stimulus. * * @author Kristin Diep - * @see {@link https://www.jspsych.org/plugins/jspsych-iat-image/ iat-image plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/iat-image/ iat-image plugin documentation on jspsych.org} */ class IatImagePlugin implements JsPsychPlugin { static info = info; @@ -178,9 +202,6 @@ class IatImagePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // kill keyboard listeners if (typeof keyboardListener !== "undefined") { this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); @@ -194,9 +215,6 @@ class IatImagePlugin implements JsPsychPlugin { correct: response.correct, }; - // clears the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-image-button-response/src/index.spec.ts b/packages/plugin-image-button-response/src/index.spec.ts index d3e64a71dd..83973fa233 100644 --- a/packages/plugin-image-button-response/src/index.spec.ts +++ b/packages/plugin-image-button-response/src/index.spec.ts @@ -5,7 +5,7 @@ import imageButtonResponse from "."; jest.useFakeTimers(); describe("image-button-response", () => { - test("displays image stimulus", async () => { + test("displays image stimulus and buttons", async () => { const { getHTML } = await startTimeline([ { type: imageButtonResponse, @@ -15,39 +15,31 @@ describe("image-button-response", () => { }, ]); - expect(getHTML()).toContain('
"' + ); }); - test("display button labels", async () => { - const { getHTML } = await startTimeline([ - { - type: imageButtonResponse, - stimulus: "../media/blue.png", - choices: ["button-choice1", "button-choice2"], - render_on_canvas: false, - }, - ]); - - expect(getHTML()).toContain(''); - expect(getHTML()).toContain(''); - }); + it("respects the `button_html` parameter", async () => { + const buttonHtmlFn = jest.fn(); + buttonHtmlFn.mockReturnValue(""); - test("display button html", async () => { const { getHTML } = await startTimeline([ { type: imageButtonResponse, stimulus: "../media/blue.png", choices: ["buttonChoice"], - button_html: '', - render_on_canvas: false, + button_html: buttonHtmlFn, }, ]); - expect(getHTML()).toContain(''); + expect(buttonHtmlFn).toHaveBeenCalledWith("buttonChoice", 0); + expect(getHTML()).toContain("something-unique"); }); test("display should clear after button click", async () => { - const { getHTML, expectFinished } = await startTimeline([ + const { getHTML, displayElement, expectFinished } = await startTimeline([ { type: imageButtonResponse, stimulus: "../media/blue.png", @@ -56,12 +48,9 @@ describe("image-button-response", () => { }, ]); - expect(getHTML()).toContain( - ' { @@ -75,9 +64,7 @@ describe("image-button-response", () => { }, ]); - expect(getHTML()).toContain( - '

This is a prompt

' - ); + expect(getHTML()).toContain("

This is a prompt

"); }); test("should hide stimulus if stimulus-duration is set", async () => { @@ -94,9 +81,9 @@ describe("image-button-response", () => { const stimulusElement = displayElement.querySelector( "#jspsych-image-button-response-stimulus" ); - expect(stimulusElement.style.visibility).toContain(""); + expect(stimulusElement.style.visibility).toEqual(""); jest.advanceTimersByTime(500); - expect(stimulusElement.style.visibility).toContain("hidden"); + expect(stimulusElement.style.visibility).toEqual("hidden"); }); test("should end trial when trial duration is reached", async () => { @@ -118,7 +105,7 @@ describe("image-button-response", () => { }); test("should end trial when button is clicked", async () => { - const { getHTML, expectFinished } = await startTimeline([ + const { getHTML, expectFinished, displayElement } = await startTimeline([ { type: imageButtonResponse, stimulus: "../media/blue.png", @@ -128,11 +115,7 @@ describe("image-button-response", () => { }, ]); - expect(getHTML()).toContain( - '{ name: "image-button-response", + version: version, parameters: { - /** The image to be displayed */ + /** The path of the image file to be displayed. */ stimulus: { type: ParameterType.IMAGE, - pretty_name: "Stimulus", default: undefined, }, - /** Set the image height in pixels */ + /** Set the height of the image in pixels. If left null (no value specified), then the image will display at its natural height. */ stimulus_height: { type: ParameterType.INT, - pretty_name: "Image height", default: null, }, - /** Set the image width in pixels */ + /** Set the width of the image in pixels. If left null (no value specified), then the image will display at its natural width. */ stimulus_width: { type: ParameterType.INT, - pretty_name: "Image width", default: null, }, - /** Maintain the aspect ratio after setting width or height */ + /** If setting *only* the width or *only* the height and this parameter is true, then the other dimension will be + * scaled to maintain the image's aspect ratio. */ maintain_aspect_ratio: { type: ParameterType.BOOL, - pretty_name: "Maintain aspect ratio", default: true, }, - /** Array containing the label(s) for the button(s). */ + /** Labels for the buttons. Each different string in the array will generate a different button. */ choices: { type: ParameterType.STRING, - pretty_name: "Choices", default: undefined, array: true, }, - /** The HTML for creating button. Can create own style. Use the "%choice%" string to indicate where the label from the choices parameter should be inserted. */ + /** + * ``(choice: string, choice_index: number)=>```; | A function that + * generates the HTML for each button in the `choices` array. The function gets the string and index of the item in + * the `choices` array and should return valid HTML. If you want to use different markup for each button, you can do + * that by using a conditional on either parameter. The default parameter returns a button element with the text + * label of the choice. + */ button_html: { - type: ParameterType.HTML_STRING, - pretty_name: "Button HTML", - default: '', - array: true, + type: ParameterType.FUNCTION, + default: function (choice: string, choice_index: number) { + return ``; + }, }, - /** Any content here will be displayed under the button. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that + * it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to show the stimulus. */ + /** How long to show the stimulus for in milliseconds. If the value is null, then the stimulus will be shown until + * the participant makes a response. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show the trial. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the participant + * fails to make a response before this timer is reached, the participant's response will be recorded as null for the + * trial and the trial will end. If the value of this parameter is null, the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** The vertical margin of the button. */ - margin_vertical: { + /** Setting to `'grid'` will make the container element have the CSS property `display: grid` and enable the use of + * `grid_rows` and `grid_columns`. Setting to `'flex'` will make the container element have the CSS property + * `display: flex`. You can customize how the buttons are laid out by adding inline CSS in the `button_html` parameter. */ + button_layout: { type: ParameterType.STRING, - pretty_name: "Margin vertical", - default: "0px", + default: "grid", }, - /** The horizontal margin of the button. */ - margin_horizontal: { - type: ParameterType.STRING, - pretty_name: "Margin horizontal", - default: "8px", + /** + * The number of rows in the button grid. Only applicable when `button_layout` is set to `'grid'`. If null, the + * number of rows will be determined automatically based on the number of buttons and the number of columns. + */ + grid_rows: { + type: ParameterType.INT, + default: 1, + }, + /** + * The number of columns in the button grid. Only applicable when `button_layout` is set to `'grid'`. If null, the + * number of columns will be determined automatically based on the number of buttons and the number of rows. + */ + grid_columns: { + type: ParameterType.INT, + default: null, }, - /** If true, then trial will end when user responds. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response + * before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until + * the value for `trial_duration` is reached. You can set this parameter to `false` to force the participant to + * view a stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, /** - * If true, the image will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers). - * If false, the image will be shown via an img element. + * If true, the image will be drawn onto a canvas element. This prevents a blank screen (white flash) between consecutive image trials in some browsers, like Firefox and Edge. + * If false, the image will be shown via an img element, as in previous versions of jsPsych. If the stimulus is an **animated gif**, you must set this parameter to false, because the canvas rendering method will only present static images. */ render_on_canvas: { type: ParameterType.BOOL, - pretty_name: "Render on canvas", default: true, }, - /** The delay of enabling button */ + /** How long the button will delay enabling in milliseconds. */ enable_button_after: { type: ParameterType.INT, - pretty_name: "Enable button after", default: 0, }, }, + data: { + /** The path of the image that was displayed. */ + stimulus: { + type: ParameterType.STRING, + }, + /** Indicates which button the participant pressed. The first button in the `choices` array is 0, the second is 1, and so on. */ + response: { + type: ParameterType.INT, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **image-button-response** + * This plugin displays an image and records responses generated with a button click. The stimulus can be displayed until + * a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant + * has failed to respond within a fixed length of time. The button itself can be customized using HTML formatting. * - * jsPsych plugin for displaying an image stimulus and getting a button response + * Image files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you + * are using timeline variables or another dynamic method to specify the image stimulus, you will need to + * [manually preload](../overview/media-preloading.md#manual-preloading) the images. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-image-button-response/ image-button-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/image-button-response/ image-button-response plugin documentation on jspsych.org} */ class ImageButtonResponsePlugin implements JsPsychPlugin { static info = info; @@ -111,186 +146,122 @@ class ImageButtonResponsePlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { - var height, width; - var html; - if (trial.render_on_canvas) { - var image_drawn = false; - // first clear the display element (because the render_on_canvas method appends to display_element instead of overwriting it with .innerHTML) - if (display_element.hasChildNodes()) { - // can't loop through child list because the list will be modified by .removeChild() - while (display_element.firstChild) { - display_element.removeChild(display_element.firstChild); - } - } - // create canvas element and image - var canvas = document.createElement("canvas"); - canvas.id = "jspsych-image-button-response-stimulus"; - canvas.style.margin = "0"; - canvas.style.padding = "0"; - var ctx = canvas.getContext("2d"); - var img = new Image(); - img.onload = () => { - // if image wasn't preloaded, then it will need to be drawn whenever it finishes loading - if (!image_drawn) { - getHeightWidth(); // only possible to get width/height after image loads - ctx.drawImage(img, 0, 0, width, height); - } - }; - img.src = trial.stimulus; - // get/set image height and width - this can only be done after image loads because uses image's naturalWidth/naturalHeight properties - const getHeightWidth = () => { - if (trial.stimulus_height !== null) { - height = trial.stimulus_height; - if (trial.stimulus_width == null && trial.maintain_aspect_ratio) { - width = img.naturalWidth * (trial.stimulus_height / img.naturalHeight); - } - } else { - height = img.naturalHeight; - } - if (trial.stimulus_width !== null) { - width = trial.stimulus_width; - if (trial.stimulus_height == null && trial.maintain_aspect_ratio) { - height = img.naturalHeight * (trial.stimulus_width / img.naturalWidth); - } - } else if (!(trial.stimulus_height !== null && trial.maintain_aspect_ratio)) { - // if stimulus width is null, only use the image's natural width if the width value wasn't set - // in the if statement above, based on a specified height and maintain_aspect_ratio = true - width = img.naturalWidth; - } - canvas.height = height; - canvas.width = width; - }; - getHeightWidth(); // call now, in case image loads immediately (is cached) - // create buttons - var buttons = []; - if (Array.isArray(trial.button_html)) { - if (trial.button_html.length == trial.choices.length) { - buttons = trial.button_html; - } else { - console.error( - "Error in image-button-response plugin. The length of the button_html array does not equal the length of the choices array" - ); - } - } else { - for (var i = 0; i < trial.choices.length; i++) { - buttons.push(trial.button_html); - } - } - var btngroup_div = document.createElement("div"); - btngroup_div.id = "jspsych-image-button-response-btngroup"; - html = ""; - for (var i = 0; i < trial.choices.length; i++) { - var str = buttons[i].replace(/%choice%/g, trial.choices[i]); - html += - '
' + - str + - "
"; - } - btngroup_div.innerHTML = html; - // add canvas to screen and draw image - display_element.insertBefore(canvas, null); - if (img.complete && Number.isFinite(width) && Number.isFinite(height)) { - // if image has loaded and width/height have been set, then draw it now - // (don't rely on img onload function to draw image when image is in the cache, because that causes a delay in the image presentation) - ctx.drawImage(img, 0, 0, width, height); - image_drawn = true; - } - // add buttons to screen - display_element.insertBefore(btngroup_div, canvas.nextElementSibling); - // add prompt if there is one - if (trial.prompt !== null) { - display_element.insertAdjacentHTML("beforeend", trial.prompt); - } - } else { - // display stimulus as an image element - html = ''; - //display buttons - var buttons = []; - if (Array.isArray(trial.button_html)) { - if (trial.button_html.length == trial.choices.length) { - buttons = trial.button_html; - } else { - console.error( - "Error in image-button-response plugin. The length of the button_html array does not equal the length of the choices array" - ); - } - } else { - for (var i = 0; i < trial.choices.length; i++) { - buttons.push(trial.button_html); - } - } - html += '
'; - - for (var i = 0; i < trial.choices.length; i++) { - var str = buttons[i].replace(/%choice%/g, trial.choices[i]); - html += - '
' + - str + - "
"; - } - html += "
"; - // add prompt - if (trial.prompt !== null) { - html += trial.prompt; - } - // update the page content - display_element.innerHTML = html; - - // set image dimensions after image has loaded (so that we have access to naturalHeight/naturalWidth) - var img = display_element.querySelector( - "#jspsych-image-button-response-stimulus" - ) as HTMLImageElement; + const calculateImageDimensions = (image: HTMLImageElement): [number, number] => { + let width: number, height: number; + // calculate image height and width - this can only be done after image loads because it uses + // the image's naturalWidth/naturalHeight properties if (trial.stimulus_height !== null) { height = trial.stimulus_height; if (trial.stimulus_width == null && trial.maintain_aspect_ratio) { - width = img.naturalWidth * (trial.stimulus_height / img.naturalHeight); + width = image.naturalWidth * (trial.stimulus_height / image.naturalHeight); } } else { - height = img.naturalHeight; + height = image.naturalHeight; } if (trial.stimulus_width !== null) { width = trial.stimulus_width; if (trial.stimulus_height == null && trial.maintain_aspect_ratio) { - height = img.naturalHeight * (trial.stimulus_width / img.naturalWidth); + height = image.naturalHeight * (trial.stimulus_width / image.naturalWidth); } } else if (!(trial.stimulus_height !== null && trial.maintain_aspect_ratio)) { // if stimulus width is null, only use the image's natural width if the width value wasn't set // in the if statement above, based on a specified height and maintain_aspect_ratio = true - width = img.naturalWidth; + width = image.naturalWidth; } - img.style.height = height.toString() + "px"; - img.style.width = width.toString() + "px"; + + return [width, height]; + }; + + display_element.innerHTML = ""; + let stimulusElement: HTMLCanvasElement | HTMLImageElement; + let canvas: HTMLCanvasElement; + + const image = trial.render_on_canvas ? new Image() : document.createElement("img"); + + if (trial.render_on_canvas) { + canvas = document.createElement("canvas"); + canvas.style.margin = "0"; + canvas.style.padding = "0"; + stimulusElement = canvas; + } else { + stimulusElement = image; } - // start timing - var start_time = performance.now(); + const drawImage = () => { + const [width, height] = calculateImageDimensions(image); + if (trial.render_on_canvas) { + canvas.width = width; + canvas.height = height; + canvas.getContext("2d").drawImage(image, 0, 0, width, height); + } else { + image.style.width = `${width}px`; + image.style.height = `${height}px`; + } + }; + + let hasImageBeenDrawn = false; + + // if image wasn't preloaded, then it will need to be drawn whenever it finishes loading + image.onload = () => { + if (!hasImageBeenDrawn) { + drawImage(); + } + }; - for (var i = 0; i < trial.choices.length; i++) { - display_element - .querySelector("#jspsych-image-button-response-button-" + i) - .addEventListener("click", (e) => { - var btn_el = e.currentTarget as HTMLButtonElement; - var choice = btn_el.getAttribute("data-choice"); // don't use dataset for jsdom compatibility - after_response(choice); - }); + image.src = trial.stimulus; + if (image.complete && image.naturalWidth !== 0) { + // if image has loaded then draw it now (don't rely on img onload function to draw image + // when image is in the cache, because that causes a delay in the image presentation) + drawImage(); + hasImageBeenDrawn = true; } + stimulusElement.id = "jspsych-image-button-response-stimulus"; + display_element.appendChild(stimulusElement); + + // Display buttons + const buttonGroupElement = document.createElement("div"); + buttonGroupElement.id = "jspsych-image-button-response-btngroup"; + if (trial.button_layout === "grid") { + buttonGroupElement.classList.add("jspsych-btn-group-grid"); + if (trial.grid_rows === null && trial.grid_columns === null) { + throw new Error( + "You cannot set `grid_rows` to `null` without providing a value for `grid_columns`." + ); + } + const n_cols = + trial.grid_columns === null + ? Math.ceil(trial.choices.length / trial.grid_rows) + : trial.grid_columns; + const n_rows = + trial.grid_rows === null + ? Math.ceil(trial.choices.length / trial.grid_columns) + : trial.grid_rows; + buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`; + buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`; + } else if (trial.button_layout === "flex") { + buttonGroupElement.classList.add("jspsych-btn-group-flex"); + } + + for (const [choiceIndex, choice] of trial.choices.entries()) { + buttonGroupElement.insertAdjacentHTML("beforeend", trial.button_html(choice, choiceIndex)); + const buttonElement = buttonGroupElement.lastChild as HTMLElement; + buttonElement.dataset.choice = choiceIndex.toString(); + buttonElement.addEventListener("click", () => { + after_response(choiceIndex); + }); + } + + display_element.appendChild(buttonGroupElement); + + // Show prompt if there is one + if (trial.prompt !== null) { + display_element.insertAdjacentHTML("beforeend", trial.prompt); + } + + // start timing + var start_time = performance.now(); + // store response var response = { rt: null, @@ -299,9 +270,6 @@ class ImageButtonResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial var trial_data = { rt: response.rt, @@ -309,9 +277,6 @@ class ImageButtonResponsePlugin implements JsPsychPlugin { response: response.button, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; @@ -326,14 +291,11 @@ class ImageButtonResponsePlugin implements JsPsychPlugin { // after a valid response, the stimulus will have the CSS class 'responded' // which can be used to provide visual feedback that a response was recorded - display_element.querySelector("#jspsych-image-button-response-stimulus").className += - " responded"; + stimulusElement.classList.add("responded"); // disable all the buttons after a response - var btns = document.querySelectorAll(".jspsych-image-button-response-button button"); - for (var i = 0; i < btns.length; i++) { - //btns[i].removeEventListener('click'); - btns[i].setAttribute("disabled", "disabled"); + for (const button of buttonGroupElement.children) { + button.setAttribute("disabled", "disabled"); } if (trial.response_ends_trial) { @@ -366,9 +328,7 @@ class ImageButtonResponsePlugin implements JsPsychPlugin { // hide image if timing is set if (trial.stimulus_duration !== null) { this.jsPsych.pluginAPI.setTimeout(() => { - display_element.querySelector( - "#jspsych-image-button-response-stimulus" - ).style.visibility = "hidden"; + stimulusElement.style.visibility = "hidden"; }, trial.stimulus_duration); } @@ -431,7 +391,9 @@ class ImageButtonResponsePlugin implements JsPsychPlugin { if (data.rt !== null) { this.jsPsych.pluginAPI.clickTarget( - display_element.querySelector(`div[data-choice="${data.response}"] button`), + display_element.querySelector( + `#jspsych-image-button-response-btngroup [data-choice="${data.response}"]` + ), data.rt ); } diff --git a/packages/plugin-image-keyboard-response/src/index.spec.ts b/packages/plugin-image-keyboard-response/src/index.spec.ts index 2c054527dd..532245a0ec 100644 --- a/packages/plugin-image-keyboard-response/src/index.spec.ts +++ b/packages/plugin-image-keyboard-response/src/index.spec.ts @@ -18,7 +18,7 @@ describe("image-keyboard-response", () => { ' { ' { ]); expect(getHTML()).toContain('
this is a prompt
'); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -76,7 +76,7 @@ describe("image-keyboard-response", () => { jest.advanceTimersByTime(500); expect(stimulusElement.style.visibility).toContain("hidden"); - pressKey("f"); + await pressKey("f"); await expectFinished(); }); @@ -113,7 +113,7 @@ describe("image-keyboard-response", () => { '{ name: "image-keyboard-response", + version: version, parameters: { - /** The image to be displayed */ + /** The path of the image file to be displayed. */ stimulus: { type: ParameterType.IMAGE, - pretty_name: "Stimulus", default: undefined, }, - /** Set the image height in pixels */ + /** Set the height of the image in pixels. If left null (no value specified), then the image will display at its natural height. */ stimulus_height: { type: ParameterType.INT, - pretty_name: "Image height", default: null, }, - /** Set the image width in pixels */ + /** Set the width of the image in pixels. If left null (no value specified), then the image will display at its natural width. */ stimulus_width: { type: ParameterType.INT, - pretty_name: "Image width", default: null, }, - /** Maintain the aspect ratio after setting width or height */ + /** If setting *only* the width or *only* the height and this parameter is true, then the other dimension will be scaled + * to maintain the image's aspect ratio. */ maintain_aspect_ratio: { type: ParameterType.BOOL, - pretty_name: "Maintain aspect ratio", default: true, }, - /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */ + /**his array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. Keys should + * be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see + * [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) and + * [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/) + * for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"ALL_KEYS"` + * means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean that no responses are allowed. */ choices: { type: ParameterType.KEYS, - pretty_name: "Choices", default: "ALL_KEYS", }, - /** Any content here will be displayed below the stimulus. */ + /**This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can + * be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to show the stimulus. */ + /** How long to show the stimulus for in milliseconds. If the value is `null`, then the stimulus will be shown until the + * participant makes a response. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show trial before it ends */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the participant + * fails to make a response before this timer is reached, the participant's response will be recorded as null for the + * trial and the trial will end. If the value of this parameter is `null`, then the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** If true, trial will end when subject makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response before + * the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the value for + * `trial_duration` is reached. You can set this parameter to `false` to force the participant to view a stimulus for a + * fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, /** - * If true, the image will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers). - * If false, the image will be shown via an img element. + * If `true`, the image will be drawn onto a canvas element. This prevents a blank screen (white flash) between consecutive image trials in some browsers, like Firefox and Edge. + * If `false`, the image will be shown via an img element, as in previous versions of jsPsych. If the stimulus is an **animated gif**, you must set this parameter to false, because the canvas rendering method will only present static images. */ render_on_canvas: { type: ParameterType.BOOL, - pretty_name: "Render on canvas", default: true, }, }, + data: { + /** The path of the image that was displayed. */ + stimulus: { + type: ParameterType.STRING, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus + * first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **image-keyboard-response** + * This plugin displays an image and records responses generated with the keyboard. The stimulus can be displayed until a + * response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant has + * failed to respond within a fixed length of time. * - * jsPsych plugin for displaying an image stimulus and getting a keyboard response + * Image files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using + * timeline variables or another dynamic method to specify the image stimulus, you will need to + * [manually preload](../overview/media-preloading.md#manual-preloading) the images. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-image-keyboard-response/ image-keyboard-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/image-keyboard-response/ image-keyboard-response plugin documentation on jspsych.org} */ class ImageKeyboardResponsePlugin implements JsPsychPlugin { static info = info; @@ -190,9 +215,6 @@ class ImageKeyboardResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // kill keyboard listeners if (typeof keyboardListener !== "undefined") { this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); @@ -205,9 +227,6 @@ class ImageKeyboardResponsePlugin implements JsPsychPlugin { response: response.key, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-image-slider-response/src/index.spec.ts b/packages/plugin-image-slider-response/src/index.spec.ts index 49f2f46ab3..7d21ec220a 100644 --- a/packages/plugin-image-slider-response/src/index.spec.ts +++ b/packages/plugin-image-slider-response/src/index.spec.ts @@ -19,7 +19,7 @@ describe("image-slider-response", () => { expect(getHTML()).toContain( '
{ expect(getHTML()).toContain('left'); expect(getHTML()).toContain('right'); - clickTarget(document.querySelector("#jspsych-image-slider-response-next")); + await clickTarget(document.querySelector("#jspsych-image-slider-response-next")); await expectFinished(); }); @@ -56,7 +56,7 @@ describe("image-slider-response", () => { '' ); - clickTarget(document.querySelector("#jspsych-image-slider-response-next")); + await clickTarget(document.querySelector("#jspsych-image-slider-response-next")); await expectFinished(); }); @@ -82,7 +82,7 @@ describe("image-slider-response", () => { expect(responseElement.max).toBe("10"); expect(responseElement.step).toBe("2"); - clickTarget(document.querySelector("#jspsych-image-slider-response-next")); + await clickTarget(document.querySelector("#jspsych-image-slider-response-next")); await expectFinished(); }); @@ -100,7 +100,7 @@ describe("image-slider-response", () => { expect(getHTML()).toContain("

This is a prompt

"); - clickTarget(document.querySelector("#jspsych-image-slider-response-next")); + await clickTarget(document.querySelector("#jspsych-image-slider-response-next")); await expectFinished(); }); @@ -123,7 +123,7 @@ describe("image-slider-response", () => { jest.advanceTimersByTime(500); expect(stimulusElement.style.visibility).toContain("hidden"); - clickTarget(document.querySelector("#jspsych-image-slider-response-next")); + await clickTarget(document.querySelector("#jspsych-image-slider-response-next")); await expectFinished(); }); @@ -163,7 +163,7 @@ describe("image-slider-response", () => { '
{ name: "image-slider-response", + version: version, parameters: { - /** The image to be displayed */ + /** The path to the image file to be displayed. */ stimulus: { type: ParameterType.IMAGE, - pretty_name: "Stimulus", default: undefined, }, - /** Set the image height in pixels */ + /** Set the height of the image in pixels. If left null (no value specified), then the image will display at its natural height. */ stimulus_height: { type: ParameterType.INT, - pretty_name: "Image height", default: null, }, - /** Set the image width in pixels */ + /** Set the width of the image in pixels. If left null (no value specified), then the image will display at its natural width. */ stimulus_width: { type: ParameterType.INT, - pretty_name: "Image width", default: null, }, - /** Maintain the aspect ratio after setting width or height */ + /** If setting *only* the width or *only* the height and this parameter is true, then the other dimension will be scaled + * to maintain the image's aspect ratio. */ maintain_aspect_ratio: { type: ParameterType.BOOL, - pretty_name: "Maintain aspect ratio", default: true, }, /** Sets the minimum value of the slider. */ min: { type: ParameterType.INT, - pretty_name: "Min slider", default: 0, }, - /** Sets the maximum value of the slider */ + /** Sets the maximum value of the slider. */ max: { type: ParameterType.INT, - pretty_name: "Max slider", default: 100, }, - /** Sets the starting value of the slider */ + /** Sets the starting value of the slider. */ slider_start: { type: ParameterType.INT, - pretty_name: "Slider starting value", default: 50, }, - /** Sets the step of the slider */ + /** Sets the step of the slider. */ step: { type: ParameterType.INT, - pretty_name: "Step", default: 1, }, - /** Array containing the labels for the slider. Labels will be displayed at equidistant locations along the slider. */ + /** abels displayed at equidistant locations on the slider. For example, two labels will be placed at the ends of the slider. + * Three labels would place two at the ends and one in the middle. Four will place two at the ends, and the other two will + * be at 33% and 67% of the slider width. */ labels: { - type: ParameterType.HTML_STRING, - pretty_name: "Labels", + type: ParameterType.STRING, default: [], array: true, }, - /** Width of the slider in pixels. */ + /** Set the width of the slider in pixels. If left null, then the width will be equal to the widest element in the display. */ slider_width: { type: ParameterType.INT, - pretty_name: "Slider width", default: null, }, - /** Label of the button to advance. */ + /** Label of the button to advance/submit. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", array: false, }, - /** If true, the participant will have to move the slider before continuing. */ + /** If true, the participant must move the slider before clicking the continue button. */ require_movement: { type: ParameterType.BOOL, - pretty_name: "Require movement", default: false, }, - /** Any content here will be displayed below the slider. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be + * used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** How long to show the stimulus. */ + /** How long to show the stimulus for in milliseconds. If the value is null, then the stimulus will be shown until the participant + * makes a response. */ stimulus_duration: { type: ParameterType.INT, - pretty_name: "Stimulus duration", default: null, }, - /** How long to show the trial. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the participant + * fails to make a response before this timer is reached, the participant's response will be recorded as null for the trial + * and the trial will end. If the value of this parameter is null, then the trial will wait for a response indefinitely. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** If true, trial will end when user makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response + * before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the + * value for `trial_duration` is reached. You can set this parameter to `false` to force the participant to view a + * stimulus for a fixed amount of time, even if they respond before the time is complete. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, /** - * If true, the image will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers). - * If false, the image will be shown via an img element. + * If true, the image will be drawn onto a canvas element. This prevents a blank screen (white flash) between + * consecutive image trials in some browsers, like Firefox and Edge. + * If false, the image will be shown via an img element, as in previous versions of jsPsych. If the stimulus is + * an **animated gif**, you must set this parameter to false, because the canvas rendering method will only present static images. */ render_on_canvas: { type: ParameterType.BOOL, - pretty_name: "Render on canvas", default: true, }, }, + data: { + /** The path of the image that was displayed. */ + stimulus: { + type: ParameterType.STRING, + }, + /** The numeric value of the slider. */ + response: { + type: ParameterType.INT, + }, + /** The time in milliseconds for the participant to make a response. The time is measured from when the stimulus + * first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** The starting value of the slider. */ + slider_start: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **image-slider-response** + * This plugin displays and image and allows the participant to respond by dragging a slider. * - * jsPsych plugin for showing an image stimulus and getting a slider response + * Image files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are + * using timeline variables or another dynamic method to specify the image stimulus, you will need + * to [manually preload](../overview/media-preloading.md#manual-preloading) the images. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-image-slider-response/ image-slider-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/image-slider-response/ image-slider-response plugin documentation on jspsych.org} */ class ImageSliderResponsePlugin implements JsPsychPlugin { static info = info; @@ -372,8 +391,6 @@ class ImageSliderResponsePlugin implements JsPsychPlugin { } const end_trial = () => { - this.jsPsych.pluginAPI.clearAllTimeouts(); - // save data var trialdata = { rt: response.rt, @@ -382,8 +399,6 @@ class ImageSliderResponsePlugin implements JsPsychPlugin { response: response.response, }; - display_element.innerHTML = ""; - // next trial this.jsPsych.finishTrial(trialdata); }; diff --git a/packages/plugin-initialize-camera/src/index.ts b/packages/plugin-initialize-camera/src/index.ts index 4e8d9c082f..f1ba6637d5 100644 --- a/packages/plugin-initialize-camera/src/index.ts +++ b/packages/plugin-initialize-camera/src/index.ts @@ -1,50 +1,73 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "initialize-camera", + version: version, parameters: { - /** Message to display with the selection box */ + /** The message to display when the user is presented with a dropdown list of available devices. */ device_select_message: { type: ParameterType.HTML_STRING, default: `

Please select the camera you would like to use.

`, }, - /** Label to use for the button that confirms selection */ + /** The label for the select button. */ button_label: { type: ParameterType.STRING, default: "Use this camera", }, - /** Set to `true` to include audio in the recording */ + /** Set to `true` to include an audio track in the recordings. */ include_audio: { type: ParameterType.BOOL, default: false, }, - /** Desired width of the camera stream */ + /** Request a specific width for the recording. This is not a guarantee that this width will be used, as it + * depends on the capabilities of the participant's device. Learn more about `MediaRecorder` constraints + * [here](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API/Constraints#requesting_a_specific_value_for_a_setting). */ width: { type: ParameterType.INT, default: null, }, - /** Desired height of the camera stream */ + /** Request a specific height for the recording. This is not a guarantee that this height will be used, as it + * depends on the capabilities of the participant's device. Learn more about `MediaRecorder` constraints + * [here](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API/Constraints#requesting_a_specific_value_for_a_setting). */ height: { type: ParameterType.INT, default: null, }, - /** MIME type of the recording. Set as a full string, e.g., 'video/webm; codecs="vp8, vorbis"'. */ + /** Set this to use a specific [MIME type](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/mimeType) for the + * recording. Set the entire type, e.g., `'video/mp4; codecs="avc1.424028, mp4a.40.2"'`. */ mime_type: { type: ParameterType.STRING, default: null, }, }, + data: { + /** The [device ID](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId) of the selected camera. */ + device_id: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * **initialize-camera** + * This plugin asks the participant to grant permission to access a camera. + * If multiple cameras are connected to the participant's device, then it allows the participant to pick which device to use. + * Once access is granted for an experiment you do not need to get permission again. + * + * Once the camera is selected with this plugin it can be accessed with + * [`jsPsych.pluginAPI.getCameraRecorder()`](../reference/jspsych-pluginAPI.md#getcamerarecorder). + * + * !!! warning + * When recording from a camera your experiment will need to be running over `https://` protocol. If you try to + * run the experiment locally using the `file://` protocol or over `http://` protocol you will not be able to access + * the microphone because of [potential security problems](https://blog.mozilla.org/webrtc/camera-microphone-require-https-in-firefox-68/). * - * jsPsych plugin for getting permission to initialize a camera and setting properties of the recording. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-initialize-camera/ initialize-camera plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/initialize-camera/ initialize-camera plugin documentation on jspsych.org} */ class InitializeCameraPlugin implements JsPsychPlugin { static info = info; @@ -53,7 +76,6 @@ class InitializeCameraPlugin implements JsPsychPlugin { trial(display_element: HTMLElement, trial: TrialType) { this.run_trial(display_element, trial).then((id) => { - display_element.innerHTML = ""; this.jsPsych.finishTrial({ device_id: id, }); diff --git a/packages/plugin-initialize-microphone/src/index.ts b/packages/plugin-initialize-microphone/src/index.ts index 9a08c97bfe..f14a272a21 100644 --- a/packages/plugin-initialize-microphone/src/index.ts +++ b/packages/plugin-initialize-microphone/src/index.ts @@ -1,30 +1,48 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "initialize-microphone", + version: version, parameters: { - /** Function to call */ + /** The message to display when the user is presented with a dropdown list of available devices. */ device_select_message: { type: ParameterType.HTML_STRING, default: `

Please select the microphone you would like to use.

`, }, - /** Is the function call asynchronous? */ + /** The label for the select button. */ button_label: { type: ParameterType.STRING, default: "Use this microphone", }, }, + data: { + /** The [device ID](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId) of the selected microphone. */ + device_id: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * **initialize-microphone** + * This plugin asks the participant to grant permission to access a microphone. + * If multiple microphones are connected to the participant's device, then it allows the participant to pick which device to use. + * Once access is granted for an experiment you do not need to get permission again. + * + * Once the microphone is selected with this plugin it can be accessed with + * [`jsPsych.pluginAPI.getMicrophoneRecorder()`](../reference/jspsych-pluginAPI.md#getmicrophonerecorder). * - * jsPsych plugin for getting permission to initialize a microphone + * !!! warning + * When recording from a microphone your experiment will need to be running over `https://` protocol. + * If you try to run the experiment locally using the `file://` protocol or over `http://` protocol you will not + * be able to access the microphone because of + * [potential security problems](https://blog.mozilla.org/webrtc/camera-microphone-require-https-in-firefox-68/). * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-initialize-microphone/ initialize-microphone plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/initialize-microphone/ initialize-microphone plugin documentation on jspsych.org} */ class InitializeMicrophonePlugin implements JsPsychPlugin { static info = info; diff --git a/packages/plugin-instructions/src/index.spec.ts b/packages/plugin-instructions/src/index.spec.ts index d43b4c927f..6ed199784b 100644 --- a/packages/plugin-instructions/src/index.spec.ts +++ b/packages/plugin-instructions/src/index.spec.ts @@ -16,10 +16,10 @@ describe("instructions plugin", () => { expect(getHTML()).toContain("page 1"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toContain("page 2"); - pressKey("a"); + await pressKey("a"); await expectFinished(); }); @@ -35,13 +35,13 @@ describe("instructions plugin", () => { expect(getHTML()).toContain("page 1"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toContain("page 2"); - pressKey("ArrowLeft"); + await pressKey("ArrowLeft"); expect(getHTML()).toContain("page 2"); - pressKey("a"); + await pressKey("a"); await expectFinished(); }); @@ -54,8 +54,8 @@ describe("instructions plugin", () => { }, ]); - pressKey("a"); - pressKey("a"); + await pressKey("a"); + await pressKey("a"); await expectFinished(); @@ -63,6 +63,37 @@ describe("instructions plugin", () => { expect(data[0].page_index).toEqual(0); expect(data[1].page_index).toEqual(1); }); + + test("forward and backward callback works", async () => { + let count = [0, 0, 0, 0]; + const { expectFinished } = await startTimeline([ + { + type: instructions, + pages: ["page 1", "page 2", "page 3"], + on_page_change: function (page_number: number) { + count[page_number]++; + }, + }, + ]); + + // Go to second page; count[1]++ + await pressKey("ArrowRight"); + + // Go to first page; count[0]++ + await pressKey("ArrowLeft"); + + // Go to second page; count[1]++ + await pressKey("ArrowRight"); + + // Go to last page; count[2]++ + await pressKey("ArrowRight"); + + // Finish trial; count[3]++ + await pressKey("ArrowRight"); + await expectFinished(); + + expect(count).toEqual([1, 2, 1, 1]); + }); }); describe("instructions plugin simulation", () => { diff --git a/packages/plugin-instructions/src/index.ts b/packages/plugin-instructions/src/index.ts index 3dbb25e21c..248ea53789 100644 --- a/packages/plugin-instructions/src/index.ts +++ b/packages/plugin-instructions/src/index.ts @@ -1,83 +1,108 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { parameterPathArrayToString } from "jspsych/src/timeline/util"; + +import { version } from "../package.json"; const info = { name: "instructions", + version: version, parameters: { - /** Each element of the array is the HTML-formatted content for a single page. */ + /** Each element of the array is the content for a single page. Each page should be an HTML-formatted string. */ pages: { type: ParameterType.HTML_STRING, - pretty_name: "Pages", default: undefined, array: true, }, - /** The key the subject can press in order to advance to the next page. */ + /** This is the key that the participant can press in order to advance to the next page. This key should be + * specified as a string (e.g., `'a'`, `'ArrowLeft'`, `' '`, `'Enter'`). */ key_forward: { type: ParameterType.KEY, - pretty_name: "Key forward", default: "ArrowRight", }, - /** The key that the subject can press to return to the previous page. */ + /** This is the key that the participant can press to return to the previous page. This key should be specified as a + * string (e.g., `'a'`, `'ArrowLeft'`, `' '`, `'Enter'`). */ key_backward: { type: ParameterType.KEY, - pretty_name: "Key backward", default: "ArrowLeft", }, - /** If true, the subject can return to the previous page of the instructions. */ + /** If true, the participant can return to previous pages of the instructions. If false, they may only advace to the next page. */ allow_backward: { type: ParameterType.BOOL, - pretty_name: "Allow backward", default: true, }, - /** If true, the subject can use keyboard keys to navigate the pages. */ + /** If `true`, the participant can use keyboard keys to navigate the pages. If `false`, they may not. */ allow_keys: { type: ParameterType.BOOL, - pretty_name: "Allow keys", default: true, }, - /** If true, then a "Previous" and "Next" button will be displayed beneath the instructions. */ + /** If true, then a `Previous` and `Next` button will be displayed beneath the instructions. Participants can + * click the buttons to navigate. */ show_clickable_nav: { type: ParameterType.BOOL, - pretty_name: "Show clickable nav", default: false, }, /** If true, and clickable navigation is enabled, then Page x/y will be shown between the nav buttons. */ show_page_number: { type: ParameterType.BOOL, - pretty_name: "Show page number", default: false, }, - /** The text that appears before x/y (current/total) pages displayed with show_page_number. */ + /** The text that appears before x/y pages displayed when show_page_number is true.*/ page_label: { type: ParameterType.STRING, - pretty_name: "Page label", default: "Page", }, /** The text that appears on the button to go backwards. */ button_label_previous: { type: ParameterType.STRING, - pretty_name: "Button label previous", default: "Previous", }, /** The text that appears on the button to go forwards. */ button_label_next: { type: ParameterType.STRING, - pretty_name: "Button label next", default: "Next", }, + /** The callback function when page changes */ + on_page_change: { + type: ParameterType.FUNCTION, + pretty_name: "Page change callback", + default: function (current_page: number) {}, + }, + }, + data: { + /** An array containing the order of pages the participant viewed (including when the participant returned to previous pages) + * and the time spent viewing each page. Each object in the array represents a single page view, + * and contains keys called `page_index` (the page number, starting with 0) and `viewing_time` + * (duration of the page view). This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` + * functions. + */ + view_history: { + type: ParameterType.COMPLEX, + array: true, + parameters: { + page_index: { + type: ParameterType.INT, + }, + viewing_time: { + type: ParameterType.INT, + }, + }, + }, + /** The response time in milliseconds for the participant to view all of the pages. */ + rt: { + type: ParameterType.INT, + }, }, }; type Info = typeof info; /** - * **instructions** - * - * jsPsych plugin to display text (including HTML-formatted strings) during the experiment. - * Use it to show a set of pages that participants can move forward/backward through. - * Page numbers can be displayed to help with navigation by setting show_page_number to true. + * This plugin is for showing instructions to the participant. It allows participants to navigate through multiple pages + * of instructions at their own pace, recording how long the participant spends on each page. Navigation can be done using + * the mouse or keyboard. participants can be allowed to navigate forwards and backwards through pages, if desired. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-instructions/ instructions plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/instructions/ instructions plugin documentation on jspsych.org} */ class InstructionsPlugin implements JsPsychPlugin { static info = info; @@ -93,8 +118,7 @@ class InstructionsPlugin implements JsPsychPlugin { var last_page_update_time = start_time; - function btnListener(evt) { - evt.target.removeEventListener("click", btnListener); + function btnListener() { if (this.id === "jspsych-instructions-back") { back(); } else if (this.id === "jspsych-instructions-next") { @@ -143,12 +167,12 @@ class InstructionsPlugin implements JsPsychPlugin { if (current_page != 0 && trial.allow_backward) { display_element .querySelector("#jspsych-instructions-back") - .addEventListener("click", btnListener); + .addEventListener("click", btnListener, { once: true }); } display_element .querySelector("#jspsych-instructions-next") - .addEventListener("click", btnListener); + .addEventListener("click", btnListener, { once: true }); } else { if (trial.show_page_number && trial.pages.length > 1) { // page numbers for non-mouse navigation @@ -169,6 +193,8 @@ class InstructionsPlugin implements JsPsychPlugin { } else { show_current_page(); } + + trial.on_page_change(current_page); } function back() { @@ -177,6 +203,8 @@ class InstructionsPlugin implements JsPsychPlugin { current_page--; show_current_page(); + + trial.on_page_change(current_page); } function add_current_page_to_view_history() { @@ -197,8 +225,6 @@ class InstructionsPlugin implements JsPsychPlugin { this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboard_listener); } - display_element.innerHTML = ""; - var trial_data = { view_history: view_history, rt: Math.round(performance.now() - start_time), diff --git a/packages/plugin-maxdiff/src/index.spec.ts b/packages/plugin-maxdiff/src/index.spec.ts index 2692c7f601..3f43f7ea8d 100644 --- a/packages/plugin-maxdiff/src/index.spec.ts +++ b/packages/plugin-maxdiff/src/index.spec.ts @@ -18,7 +18,7 @@ describe("maxdiff plugin", () => { document.querySelector('input[data-name="0"][name="left"]').checked = true; document.querySelector('input[data-name="1"][name="right"]').checked = true; - clickTarget(document.querySelector("#jspsych-maxdiff-next")); + await clickTarget(document.querySelector("#jspsych-maxdiff-next")); await expectFinished(); expect(getData().values()[0].response).toEqual({ left: "a", right: "b" }); diff --git a/packages/plugin-maxdiff/src/index.ts b/packages/plugin-maxdiff/src/index.ts index 2eeb59f709..c7b822a281 100644 --- a/packages/plugin-maxdiff/src/index.ts +++ b/packages/plugin-maxdiff/src/index.ts @@ -1,58 +1,92 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "maxdiff", + version: version, parameters: { - /** Array containing the alternatives to be presented in the maxdiff table. */ + /** An array of one or more alternatives of string type to fill the rows of the maxdiff table. If `required` is true, + * then the array must contain two or more alternatives, so that at least one can be selected for both the left + * and right columns. */ alternatives: { type: ParameterType.STRING, - pretty_name: "Alternatives", array: true, default: undefined, }, - /** Array containing the labels to display for left and right response columns. */ + /** An array with exactly two labels of string type to display as column headings (to the left and right of the + * alternatives) for responses on the criteria of interest. */ labels: { type: ParameterType.STRING, array: true, - pretty_name: "Labels", default: undefined, }, - /** If true, the order of the alternatives will be randomized. */ + /** If true, the display order of `alternatives` is randomly determined at the start of the trial. */ randomize_alternative_order: { type: ParameterType.BOOL, - pretty_name: "Randomize Alternative Order", default: false, }, - /** String to display at top of the page. */ + /** HTML formatted string to display at the top of the page above the maxdiff table. */ preamble: { type: ParameterType.HTML_STRING, - pretty_name: "Preamble", default: "", }, /** Label of the button to submit response. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button Label", default: "Continue", }, - /** Makes answering the alternative required. */ + /** If true, prevents the user from submitting the response and proceeding until a radio button in both the left and right response columns has been selected. */ required: { type: ParameterType.BOOL, - pretty_name: "Required", default: false, }, }, + data: { + /** The response time in milliseconds for the participant to make a response. The time is measured from when the maxdiff table first + * appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** An object with two keys, `left` and `right`, containing the labels (strings) corresponding to the left and right response + * columns. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + labels: { + type: ParameterType.COMPLEX, + parameters: { + left: { + type: ParameterType.STRING, + }, + right: { + type: ParameterType.STRING, + }, + }, + }, + /** An object with two keys, `left` and `right`, containing the alternatives selected on the left and right columns. + * This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + response: { + type: ParameterType.COMPLEX, + parameters: { + left: { + type: ParameterType.STRING, + }, + right: { + type: ParameterType.STRING, + }, + }, + }, + }, }; type Info = typeof info; /** - * **maxdiff** - * - * jsPsych plugin for maxdiff/conjoint analysis designs + * The maxdiff plugin displays a table with rows of alternatives to be selected for two mutually-exclusive categories, + * typically as 'most' or 'least' on a particular criteria (e.g. importance, preference, similarity). The participant + * responds by selecting one radio button corresponding to an alternative in both the left and right response columns. + * The same alternative cannot be endorsed on both the left and right response columns (e.g. 'most' and 'least') simultaneously. * * @author Angus Hughes - * @see {@link https://www.jspsych.org/plugins/jspsych-maxdiff/ maxdiff plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/maxdiff/ maxdiff plugin documentation on jspsych.org} */ class MaxdiffPlugin implements JsPsychPlugin { static info = info; @@ -195,9 +229,6 @@ class MaxdiffPlugin implements JsPsychPlugin { response: { left: get_response("left"), right: get_response("right") }, }; - // clear the display - display_element.innerHTML = ""; - // next trial this.jsPsych.finishTrial(trial_data); }); diff --git a/packages/plugin-mirror-camera/src/index.ts b/packages/plugin-mirror-camera/src/index.ts index 9d96c45210..bc169f5f4f 100644 --- a/packages/plugin-mirror-camera/src/index.ts +++ b/packages/plugin-mirror-camera/src/index.ts @@ -1,45 +1,57 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "mirror-camera", + version: version, parameters: { - /** HTML to render below the video */ + /** HTML-formatted content to display below the camera feed. */ prompt: { type: ParameterType.HTML_STRING, default: null, }, - /** Label to show on continue button */ + /** The label of the button to advance to the next trial. */ button_label: { type: ParameterType.STRING, default: "Continue", }, - /** Height of the video element */ + /** The height of the video playback element. If left `null` then it will match the size of the recording. */ height: { type: ParameterType.INT, default: null, }, - /** Width of the video element */ + /** The width of the video playback element. If left `null` then it will match the size of the recording. */ width: { type: ParameterType.INT, default: null, }, - /** Whether to flip the camera */ + /** Whether to mirror the video image. */ mirror_camera: { type: ParameterType.BOOL, default: true, }, }, + data: { + /** The length of time the participant viewed the video playback. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **mirror-camera** + * This plugin shows a live feed of the participant's camera. It can be useful in experiments that need to record video in order to give the participant a chance to see what is in the view of the camera. + * + * You must initialize the camera using the [initialize-camera plugin](initialize-camera.md) prior to running this plugin. * - * jsPsych plugin for showing a live stream from a camera + * !!! warning + * When recording from a camera your experiment will need to be running over `https://` protocol. If you try to run the experiment locally using the `file://` protocol or over `http://` protocol you will not be able to access the camera because of [potential security problems](https://blog.mozilla.org/webrtc/camera-microphone-require-https-in-firefox-68/). * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-mirror-camera/ mirror-camera plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/mirror-camera/ mirror-camera plugin documentation on jspsych.org} */ class MirrorCameraPlugin implements JsPsychPlugin { static info = info; @@ -73,7 +85,6 @@ class MirrorCameraPlugin implements JsPsychPlugin { } finish(display_element: HTMLElement) { - display_element.innerHTML = ""; this.jsPsych.finishTrial({ rt: performance.now() - this.start_time, }); diff --git a/packages/plugin-preload/src/index.spec.ts b/packages/plugin-preload/src/index.spec.ts index 0ac776caeb..fd54d8f0a8 100644 --- a/packages/plugin-preload/src/index.spec.ts +++ b/packages/plugin-preload/src/index.spec.ts @@ -1,7 +1,7 @@ import audioKeyboardResponse from "@jspsych/plugin-audio-keyboard-response"; import imageKeyboardResponse from "@jspsych/plugin-image-keyboard-response"; import videoKeyboardResponse from "@jspsych/plugin-video-keyboard-response"; -import { simulateTimeline, startTimeline } from "@jspsych/test-utils"; +import { flushPromises, simulateTimeline, startTimeline } from "@jspsych/test-utils"; import { JsPsych, initJsPsych } from "jspsych"; import preloadPlugin from "."; @@ -573,6 +573,7 @@ describe("preload plugin", () => { ); jest.advanceTimersByTime(101); + await flushPromises(); expect(mockFn).toHaveBeenCalledWith("timeout"); expect(getHTML()).toMatch( diff --git a/packages/plugin-preload/src/index.ts b/packages/plugin-preload/src/index.ts index 0e61a71b39..8f3f271f83 100644 --- a/packages/plugin-preload/src/index.ts +++ b/packages/plugin-preload/src/index.ts @@ -1,28 +1,34 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "preload", + version: version, parameters: { - /** Whether or not to automatically preload any media files based on the timeline passed to jsPsych.run. */ + /** If `true`, the plugin will preload any files that can be automatically preloaded based on the main experiment + * timeline that is passed to `jsPsych.run`. If `false`, any file(s) to be preloaded should be specified by passing + * a timeline array to the `trials` parameter and/or an array of file paths to the `images`, `audio`, and/or `video` + * parameters. Setting this parameter to `false` is useful when you plan to preload your files in smaller batches + * throughout the experiment. */ auto_preload: { type: ParameterType.BOOL, - pretty_name: "Auto-preload", default: false, }, - /** A timeline of trials to automatically preload. If one or more trial objects is provided in the timeline array, then the plugin will attempt to preload the media files used in the trial(s). */ + /** An array containing one or more jsPsych trial or timeline objects. This parameter is useful when you want to + * automatically preload stimuli files from a specific subset of the experiment. See [Creating an Experiment: + * The Timeline](../overview/timeline.md) for information on constructing timelines. */ trials: { type: ParameterType.TIMELINE, - pretty_name: "Trials", default: [], }, /** - * Array with one or more image files to load. This parameter is often used in cases where media files cannot# + * Array with one or more image files to load. This parameter is often used in cases where media files cannot * be automatically preloaded based on the timeline, e.g. because the media files are passed into an image plugin/parameter with * timeline variables or dynamic parameters, or because the image is embedded in an HTML string. */ images: { type: ParameterType.STRING, - pretty_name: "Images", default: [], array: true, }, @@ -33,7 +39,6 @@ const info = { */ audio: { type: ParameterType.STRING, - pretty_name: "Audio", default: [], array: true, }, @@ -44,20 +49,17 @@ const info = { */ video: { type: ParameterType.STRING, - pretty_name: "Video", default: [], array: true, }, - /** HTML-formatted message to be shown above the progress bar while the files are loading. */ + /** HTML-formatted message to show above the progress bar while the files are loading. If `null`, then no message is shown. */ message: { type: ParameterType.HTML_STRING, - pretty_name: "Message", default: null, }, - /** Whether or not to show the loading progress bar. */ + /** If `true`, a progress bar will be shown while the files are loading. If `false`, no progress bar is shown. */ show_progress_bar: { type: ParameterType.BOOL, - pretty_name: "Show progress bar", default: true, }, /** @@ -67,13 +69,11 @@ const info = { */ continue_after_error: { type: ParameterType.BOOL, - pretty_name: "Continue after error", default: false, }, - /** Error message to show on the page in case of any loading errors. This parameter is only relevant when continue_after_error is false. */ + /** HTML-formatted message to be shown on the page after loading fails or times out. Only applies when `continue_after_error` is `false`.*/ error_message: { type: ParameterType.HTML_STRING, - pretty_name: "Error message", default: "The experiment failed to load.", }, /** @@ -82,7 +82,6 @@ const info = { */ show_detailed_errors: { type: ParameterType.BOOL, - pretty_name: "Show detailed errors", default: false, }, /** @@ -92,33 +91,67 @@ const info = { */ max_load_time: { type: ParameterType.INT, - pretty_name: "Max load time", default: null, }, /** Function to be called after a file fails to load. The function takes the file name as its only argument. */ on_error: { type: ParameterType.FUNCTION, - pretty_name: "On error", default: null, }, /** Function to be called after a file loads successfully. The function takes the file name as its only argument. */ on_success: { type: ParameterType.FUNCTION, - pretty_name: "On success", default: null, }, }, + data: { + /** If `true`, then all files loaded successfully within the `max_load_time`. If `false`, then one or + * more file requests returned a failure and/or the file loading did not complete within the `max_load_time` duration.*/ + success: { + type: ParameterType.BOOL, + }, + /** If `true`, then the files did not finish loading within the `max_load_time` duration. + * If `false`, then the file loading did not timeout. Note that when the preload trial does not timeout + * (`timeout: false`), it is still possible for loading to fail (`success: false`). This happens if + * one or more files fails to load and all file requests trigger either a success or failure event before + * the `max_load_time` duration. */ + timeout: { + type: ParameterType.BOOL, + }, + /** One or more image file paths that produced a loading failure before the trial ended. */ + failed_images: { + type: ParameterType.STRING, + array: true, + }, + /** One or more audio file paths that produced a loading failure before the trial ended. */ + failed_audio: { + type: ParameterType.STRING, + array: true, + }, + /** One or more video file paths that produced a loading failure before the trial ended. */ + failed_video: { + type: ParameterType.STRING, + array: true, + }, + }, }; type Info = typeof info; /** - * **preload** + * This plugin loads images, audio, and video files. It is used for loading files into the browser's memory before they are + * needed in the experiment, in order to improve stimulus and response timing, and avoid disruption to the experiment flow. + * We recommend using this plugin anytime you are loading media files, and especially when your experiment requires large + * and/or many media files. See the [Media Preloading page](../overview/media-preloading.md) for more information. * - * jsPsych plugin for preloading image, audio, and video files + * The preload trial will end as soon as all files have loaded successfully. The trial will end or stop with an error + * message when one of these two scenarios occurs (whichever comes first): (a) all files have not finished loading + * when the `max_load_time` duration is reached, or (b) all file requests have responded with either a load or fail + * event, and one or more files has failed to load. The `continue_after_error` parameter determines whether the trial + * will stop with an error message or end (allowing the experiment to continue) when preloading is not successful. * * @author Becky Gilbert - * @see {@link https://www.jspsych.org/plugins/jspsych-preload/ preload plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/preload/ preload plugin documentation on jspsych.org} */ class PreloadPlugin implements JsPsychPlugin { static info = info; @@ -247,8 +280,6 @@ class PreloadPlugin implements JsPsychPlugin { }; const end_trial = () => { - // clear timeout again when end_trial is called, to handle race condition with max_load_time - this.jsPsych.pluginAPI.clearAllTimeouts(); var trial_data = { success: success, timeout: timeout, @@ -256,8 +287,7 @@ class PreloadPlugin implements JsPsychPlugin { failed_audio: failed_audio, failed_video: failed_video, }; - // clear the display - display_element.innerHTML = ""; + this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-reconstruction/src/index.spec.ts b/packages/plugin-reconstruction/src/index.spec.ts index 1195612bc5..4d5e6944f1 100644 --- a/packages/plugin-reconstruction/src/index.spec.ts +++ b/packages/plugin-reconstruction/src/index.spec.ts @@ -49,9 +49,9 @@ describe("reconstruction", () => { const { getHTML } = await startTimeline(timeline); - pressKey("h"); + await pressKey("h"); expect(getHTML()).toContain("

6

"); - pressKey("h"); + await pressKey("h"); expect(getHTML()).toContain("

7

"); }); @@ -68,9 +68,9 @@ describe("reconstruction", () => { const { getHTML } = await startTimeline(timeline); - pressKey("g"); + await pressKey("g"); expect(getHTML()).toContain("

4

"); - pressKey("g"); + await pressKey("g"); expect(getHTML()).toContain("

3

"); }); @@ -88,11 +88,11 @@ describe("reconstruction", () => { const { getHTML } = await startTimeline(timeline); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toContain("

6

"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toContain("

7

"); - pressKey("h"); + await pressKey("h"); expect(getHTML()).toContain("

7

"); }); @@ -110,11 +110,11 @@ describe("reconstruction", () => { const { getHTML } = await startTimeline(timeline); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toContain("

4

"); - pressKey("a"); + await pressKey("a"); expect(getHTML()).toContain("

3

"); - pressKey("g"); + await pressKey("g"); expect(getHTML()).toContain("

3

"); }); @@ -147,7 +147,7 @@ describe("reconstruction", () => { const { displayElement, expectFinished } = await startTimeline(timeline); - clickTarget(displayElement.querySelector("button")); + await clickTarget(displayElement.querySelector("button")); await expectFinished(); }); @@ -165,9 +165,9 @@ describe("reconstruction", () => { const { displayElement, getData } = await startTimeline(timeline); - pressKey("h"); + await pressKey("h"); - clickTarget(displayElement.querySelector("button")); + await clickTarget(displayElement.querySelector("button")); expect(getData().values()[0].final_value).toEqual(0.55); }); diff --git a/packages/plugin-reconstruction/src/index.ts b/packages/plugin-reconstruction/src/index.ts index dd21288b90..4ac16efd84 100644 --- a/packages/plugin-reconstruction/src/index.ts +++ b/packages/plugin-reconstruction/src/index.ts @@ -1,56 +1,71 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { parameterPathArrayToString } from "jspsych/src/timeline/util"; + +import { version } from "../package.json"; const info = { name: "reconstruction", + version: version, parameters: { /** A function with a single parameter that returns an HTML-formatted string representing the stimulus. */ stim_function: { type: ParameterType.FUNCTION, - pretty_name: "Stimulus function", default: undefined, }, /** The starting value of the stimulus parameter. */ starting_value: { type: ParameterType.FLOAT, - pretty_name: "Starting value", default: 0.5, }, /** The change in the stimulus parameter caused by pressing one of the modification keys. */ step_size: { type: ParameterType.FLOAT, - pretty_name: "Step size", default: 0.05, }, /** The key to press for increasing the parameter value. */ key_increase: { type: ParameterType.KEY, - pretty_name: "Key increase", default: "h", }, /** The key to press for decreasing the parameter value. */ key_decrease: { type: ParameterType.KEY, - pretty_name: "Key decrease", default: "g", }, /** The text that appears on the button to finish the trial. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, }, + data: { + /** The starting value of the stimulus parameter. */ + start_value: { + type: ParameterType.INT, + }, + /** The final value of the stimulus parameter. */ + final_value: { + type: ParameterType.INT, + }, + /** The length of time, in milliseconds, that the trial lasted. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **reconstruction** + * This plugin allows a participant to interact with a stimulus by modifying a parameter of the stimulus and observing + * the change in the stimulus in real-time. * - * jsPsych plugin for a reconstruction task where the subject recreates a stimulus from memory + * The stimulus must be defined through a function that returns an HTML-formatted string. The function should take a + * single value, which is the parameter that can be modified by the participant. The value can only range from 0 to 1. + * See the example at the bottom of the page for a sample function. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-reconstruction/ reconstruction plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/reconstruction/ reconstruction plugin documentation on jspsych.org} */ class ReconstructionPlugin implements JsPsychPlugin { static info = info; @@ -76,8 +91,6 @@ class ReconstructionPlugin implements JsPsychPlugin { start_value: trial.starting_value, }; - display_element.innerHTML = ""; - // next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-resize/src/index.ts b/packages/plugin-resize/src/index.ts index aa286d40b3..b29b0a17bf 100644 --- a/packages/plugin-resize/src/index.ts +++ b/packages/plugin-resize/src/index.ts @@ -1,56 +1,67 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "resize", + version: version, parameters: { - /** The height of the item to be measured. */ + /** The height of the item to be measured. Any units can be used + * as long as you are consistent with using the same units for + * all parameters. */ item_height: { type: ParameterType.INT, - pretty_name: "Item height", default: 1, }, /** The width of the item to be measured. */ item_width: { type: ParameterType.INT, - pretty_name: "Item width", default: 1, }, /** The content displayed below the resizable box and above the button. */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, /** After the scaling factor is applied, this many pixels will equal one unit of measurement. */ pixels_per_unit: { type: ParameterType.INT, - pretty_name: "Pixels per unit", default: 100, }, - /** The initial size of the box, in pixels, along the larget dimension. */ + /** The initial size of the box, in pixels, along the largest dimension. + * The aspect ratio will be set automatically to match the item width and height. */ starting_size: { type: ParameterType.INT, - pretty_name: "Starting size", default: 100, }, /** Label to display on the button to complete calibration. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, }, + data: { + /** Final width of the resizable div container, in pixels. */ + final_width_px: { + type: ParameterType.INT, + }, + /** Scaling factor that will be applied to the div containing jsPsych content. */ + scale_factor: { + type: ParameterType.FLOAT, + }, + }, }; type Info = typeof info; /** - * **resize** * - * jsPsych plugin for controlling the real world size of the display + * This plugin displays a resizable div container that allows the user to drag until the container is the same size as the + * item being measured. Once the user measures the item as close as possible, clicking the button sets a scaling factor + * for the div containing jsPsych content. This causes the stimuli that follow to have a known size, independent of monitor resolution. * * @author Steve Chao - * @see {@link https://www.jspsych.org/plugins/jspsych-resize/ resize plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/resize/ resize plugin documentation on jspsych.org} */ class ResizePlugin implements JsPsychPlugin { static info = info; @@ -93,9 +104,6 @@ class ResizePlugin implements JsPsychPlugin { document.removeEventListener("mousemove", resizeevent); document.removeEventListener("mouseup", mouseupevent); - // clear the screen - display_element.innerHTML = ""; - // finishes trial var trial_data = { diff --git a/packages/plugin-same-different-html/src/index.spec.ts b/packages/plugin-same-different-html/src/index.spec.ts index dd8e415423..93c12be211 100644 --- a/packages/plugin-same-different-html/src/index.spec.ts +++ b/packages/plugin-same-different-html/src/index.spec.ts @@ -28,7 +28,7 @@ describe("same-different-html", () => { expect(getHTML()).toMatch("visibility: hidden"); - pressKey("q"); // same_key + await pressKey("q"); // same_key await expectFinished(); expect(getData().values()[0].correct).toBe(true); diff --git a/packages/plugin-same-different-html/src/index.ts b/packages/plugin-same-different-html/src/index.ts index 4033874a97..94a0653447 100644 --- a/packages/plugin-same-different-html/src/index.ts +++ b/packages/plugin-same-different-html/src/index.ts @@ -1,44 +1,43 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "same-different-html", + version: version, parameters: { - /** Array containing the HTML content to be displayed. */ + /** A pair of stimuli, represented as an array with two entries, one for + * each stimulus. A stimulus is a string containing valid HTML markup. + * Stimuli will be shown in the order that they are defined in the array. */ stimuli: { type: ParameterType.HTML_STRING, - pretty_name: "Stimuli", default: undefined, array: true, }, /** Correct answer: either "same" or "different". */ answer: { type: ParameterType.SELECT, - pretty_name: "Answer", options: ["same", "different"], default: undefined, }, /** The key that subjects should press to indicate that the two stimuli are the same. */ same_key: { type: ParameterType.KEY, - pretty_name: "Same key", default: "q", }, /** The key that subjects should press to indicate that the two stimuli are different. */ different_key: { type: ParameterType.KEY, - pretty_name: "Different key", default: "p", }, - /** How long to show the first stimulus for in milliseconds. If null, then the stimulus will remain on the screen until any keypress is made. */ + /** How long to show the first stimulus for in milliseconds. If the value of this parameter is null then the stimulus will be shown until the participant presses any key. */ first_stim_duration: { type: ParameterType.INT, - pretty_name: "First stimulus duration", default: 1000, }, /** How long to show a blank screen in between the two stimuli. */ gap_duration: { type: ParameterType.INT, - pretty_name: "Gap duration", default: 500, }, /** How long to show the second stimulus for in milliseconds. If null, then the stimulus will remain on the screen until a valid response is made. */ @@ -47,24 +46,47 @@ const info = { pretty_name: "Second stimulus duration", default: 1000, }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, }, + data: { + /** An array of length 2 containing the HTML-formatted content that the participant saw for each trial. This will be encoded as a JSON string + * when data is saved using the `.json()` or `.csv()` functions. */ + stimulus: { + type: ParameterType.HTML_STRING, + array: true, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the second stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** `true` if the participant's response matched the `answer` for this trial. */ + correct: { + type: ParameterType.BOOL, + }, + /** The correct answer to the trial, either `'same'` or `'different'`. */ + answer: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * **same-different-html** - * - * jsPsych plugin for showing two HTML stimuli sequentially and getting a same / different judgment via keypress + * The same-different-html plugin displays two stimuli sequentially. Stimuli are HTML objects. + * The participant responds using the keyboard, and indicates whether the stimuli were the + * same or different. Same does not necessarily mean identical; a category judgment could be made, for example. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-same-different-html/ same-different-html plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/same-different-html/ same-different-html plugin documentation on jspsych.org} */ class SameDifferentHtmlPlugin implements JsPsychPlugin { static info = info; @@ -117,9 +139,6 @@ class SameDifferentHtmlPlugin implements JsPsychPlugin { } const after_response = (info: { key: string; rt: number }) => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - var correct = false; var skey = trial.same_key; @@ -145,8 +164,6 @@ class SameDifferentHtmlPlugin implements JsPsychPlugin { trial_data["response_stim1"] = first_stim_info.key; } - display_element.innerHTML = ""; - this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-same-different-image/src/index.spec.ts b/packages/plugin-same-different-image/src/index.spec.ts index ca60777f48..a584b4ee25 100644 --- a/packages/plugin-same-different-image/src/index.spec.ts +++ b/packages/plugin-same-different-image/src/index.spec.ts @@ -28,7 +28,7 @@ describe("same-different-image", () => { expect(getHTML()).toMatch("visibility: hidden"); - pressKey("q"); // same_key + await pressKey("q"); // same_key await expectFinished(); expect(getData().values()[0].correct).toBe(true); diff --git a/packages/plugin-same-different-image/src/index.ts b/packages/plugin-same-different-image/src/index.ts index b8fc554db8..b1f621b45b 100644 --- a/packages/plugin-same-different-image/src/index.ts +++ b/packages/plugin-same-different-image/src/index.ts @@ -1,70 +1,95 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "same-different-image", + version: version, parameters: { - /** Array containing the images to be displayed. */ + /** A pair of stimuli, represented as an array with two entries, + * one for each stimulus. The stimulus is a path to an image file. + * Stimuli will be shown in the order that they are defined in the array. */ stimuli: { type: ParameterType.IMAGE, - pretty_name: "Stimuli", default: undefined, array: true, }, - /** Correct answer: either "same" or "different". */ + /** Either `'same'` or `'different'`. */ answer: { type: ParameterType.SELECT, - pretty_name: "Answer", options: ["same", "different"], default: undefined, }, /** The key that subjects should press to indicate that the two stimuli are the same. */ same_key: { type: ParameterType.KEY, - pretty_name: "Same key", default: "q", }, /** The key that subjects should press to indicate that the two stimuli are different. */ different_key: { type: ParameterType.KEY, - pretty_name: "Different key", default: "p", }, - /** How long to show the first stimulus for in milliseconds. If null, then the stimulus will remain on the screen until any keypress is made. */ + /** How long to show the first stimulus for in milliseconds. If the value of this parameter is null then the stimulus will be shown until the participant presses any key. */ first_stim_duration: { type: ParameterType.INT, - pretty_name: "First stimulus duration", default: 1000, }, /** How long to show a blank screen in between the two stimuli */ gap_duration: { type: ParameterType.INT, - pretty_name: "Gap duration", default: 500, }, /** How long to show the second stimulus for in milliseconds. If null, then the stimulus will remain on the screen until a valid response is made. */ second_stim_duration: { type: ParameterType.INT, - pretty_name: "Second stimulus duration", default: 1000, }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed + * below the stimulus. The intention is that it can be used to provide a + * reminder about the action the participant is supposed to take + * (e.g., which key to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, }, + data: { + /** An array of length 2 containing the paths to the image files that the participant saw for each trial. + * This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + stimulus: { + type: ParameterType.STRING, + array: true, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the second stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** `true` if the participant's response matched the `answer` for this trial. */ + correct: { + type: ParameterType.BOOL, + }, + /** The correct answer to the trial, either `'same'` or `'different'`. */ + answer: { + type: ParameterType.STRING, + }, + }, }; type Info = typeof info; /** - * **same-different-image** - * - * jsPsych plugin for showing two image stimuli sequentially and getting a same / different judgment via keypress + * The same-different-image plugin displays two stimuli sequentially. Stimuli are images. + * The participant responds using the keyboard, and indicates whether the stimuli were the + * same or different. Same does not necessarily mean identical; a category judgment could be + * made, for example. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-same-different-image/ same-different-image plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/same-different-image/ same-different-image plugin documentation on jspsych.org} */ class SameDifferentImagePlugin implements JsPsychPlugin { static info = info; @@ -117,9 +142,6 @@ class SameDifferentImagePlugin implements JsPsychPlugin { } const after_response = (info: { key: string; rt: number }) => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - var correct = false; var skey = trial.same_key; @@ -145,8 +167,6 @@ class SameDifferentImagePlugin implements JsPsychPlugin { trial_data["response_stim1"] = first_stim_info.key; } - display_element.innerHTML = ""; - this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-serial-reaction-time-mouse/src/index.ts b/packages/plugin-serial-reaction-time-mouse/src/index.ts index dab0ad0dc6..4cec040509 100644 --- a/packages/plugin-serial-reaction-time-mouse/src/index.ts +++ b/packages/plugin-serial-reaction-time-mouse/src/index.ts @@ -1,69 +1,91 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "serial-reaction-time-mouse", + version: version, parameters: { - /** This array represents the grid of boxes shown on the screen. */ + /** This array represents the grid of boxes shown on the screen. Each inner array represents a single row. The entries in the inner arrays represent the columns. If an entry is `1` then a square will be drawn at that location on the grid. If an entry is `0` then the corresponding location on the grid will be empty. Thus, by mixing `1`s and `0`s it is possible to create many different grid-based arrangements. */ grid: { type: ParameterType.BOOL, // TO DO: BOOL doesn't seem like the right type here. INT? Also, is this always a nested array? - pretty_name: "Grid", array: true, default: [[1, 1, 1, 1]], }, /** The location of the target. The array should be the [row, column] of the target. */ target: { type: ParameterType.INT, - pretty_name: "Target", array: true, default: undefined, }, /** The width and height in pixels of each square in the grid. */ grid_square_size: { type: ParameterType.INT, - pretty_name: "Grid square size", default: 100, }, /** The color of the target square. */ target_color: { type: ParameterType.STRING, - pretty_name: "Target color", default: "#999", }, - /** If true, the trial ends after a mouse click. */ + /** If true, the trial ends after a key press. Feedback is displayed if `show_response_feedback` is true. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** The number of milliseconds to display the grid before the target changes color. */ + /** The number of milliseconds to display the grid *before* the target changes color. */ pre_target_duration: { type: ParameterType.INT, - pretty_name: "Pre-target duration", default: 0, }, /** How long to show the trial */ + /** The maximum length of time of the trial, not including feedback. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, /** If a positive number, the target will progressively change color at the start of the trial, with the transition lasting this many milliseconds. */ fade_duration: { type: ParameterType.INT, - pretty_name: "Fade duration", default: null, }, /** If true, then user can make nontarget response. */ allow_nontarget_responses: { type: ParameterType.BOOL, - pretty_name: "Allow nontarget response", default: false, }, - /** Any content here will be displayed below the stimulus */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which keys to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, + no_function: false, + }, + }, + data: { + /** The representation of the grid. This will be encoded as a JSON string when data is saved using + * the `.json()` or `.csv()` functions. */ + grid: { + type: ParameterType.COMPLEX, + array: true, + }, + /** The representation of the target location on the grid. This will be encoded + * as a JSON string when data is saved using the `.json()` or `.csv()` functions */ + target: { + type: ParameterType.COMPLEX, + array: true, + }, + /** The `[row, column]` response location on the grid. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + response: { + type: ParameterType.INT, + array: true, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the second stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** `true` if the participant's response matched the target. */ + correct: { + type: ParameterType.BOOL, }, }, }; @@ -71,12 +93,13 @@ const info = { type Info = typeof info; /** - * **serial-reaction-time-mouse** - * - * jsPsych plugin for running a serial reaction time task with mouse responses + * The serial reaction time mouse plugin implements a generalized version of the SRT + * task [(Nissen & Bullmer, 1987)](https://doi.org/10.1016%2F0010-0285%2887%2990002-8). + * Squares are displayed in a grid-based system on the screen, and one square changes color. + * The participant must click on the square that changes color. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-serial-reaction-time-mouse/ serial-reaction-time-mouse plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/serial-reaction-time-mouse/ serial-reaction-time-mouse plugin documentation on jspsych.org} */ class SerialReactionTimeMousePlugin implements JsPsychPlugin { static info = info; @@ -154,9 +177,6 @@ class SerialReactionTimeMousePlugin implements JsPsychPlugin { } const endTrial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial var trial_data = { rt: response.rt, @@ -166,9 +186,6 @@ class SerialReactionTimeMousePlugin implements JsPsychPlugin { correct: response.row == trial.target[0] && response.column == trial.target[1], }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-serial-reaction-time/src/index.spec.ts b/packages/plugin-serial-reaction-time/src/index.spec.ts index b750a11354..88890e2d25 100644 --- a/packages/plugin-serial-reaction-time/src/index.spec.ts +++ b/packages/plugin-serial-reaction-time/src/index.spec.ts @@ -21,7 +21,7 @@ describe("serial-reaction-time plugin", () => { expect(getCellElement("0-2").style.backgroundColor).toBe(""); expect(getCellElement("0-3").style.backgroundColor).toBe(""); - pressKey("3"); + await pressKey("3"); await expectFinished(); expect(getData().last(1).values()[0].correct).toBe(true); @@ -42,7 +42,7 @@ describe("serial-reaction-time plugin", () => { expect(getCellElement("0-2").style.backgroundColor).toBe(""); expect(getCellElement("0-3").style.backgroundColor).toBe(""); - pressKey("3"); + await pressKey("3"); expect(getHTML()).not.toBe(""); @@ -69,7 +69,7 @@ describe("serial-reaction-time plugin", () => { expect(getCellElement("0-2").style.backgroundColor).toBe(""); expect(getCellElement("0-3").style.backgroundColor).toBe(""); - pressKey("3"); + await pressKey("3"); jest.runAllTimers(); @@ -78,7 +78,7 @@ describe("serial-reaction-time plugin", () => { expect(getCellElement("0-2").style.backgroundColor).toBe(""); expect(getCellElement("0-3").style.backgroundColor).toBe(""); - pressKey("3"); + await pressKey("3"); await expectFinished(); diff --git a/packages/plugin-serial-reaction-time/src/index.ts b/packages/plugin-serial-reaction-time/src/index.ts index c605832df2..8b87ef75a7 100644 --- a/packages/plugin-serial-reaction-time/src/index.ts +++ b/packages/plugin-serial-reaction-time/src/index.ts @@ -1,96 +1,116 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "serial-reaction-time", + version: version, parameters: { - /** This nested array represents the grid of boxes shown on the screen, where each inner array is a row, and each entry in the inner array is a column. */ + /** This array represents the grid of boxes shown on the screen. Each inner array represents a single row. The entries in the inner arrays represent the columns. If an entry is `1` then a square will be drawn at that location on the grid. If an entry is `0` then the corresponding location on the grid will be empty. Thus, by mixing `1`s and `0`s it is possible to create many different grid-based arrangements. */ grid: { type: ParameterType.BOOL, // TO DO: BOOL doesn't seem like the right type here. INT? Also, is this always a nested array? - pretty_name: "Grid", array: true, default: [[1, 1, 1, 1]], }, /** The location of the target. The array should be the [row, column] of the target. */ target: { type: ParameterType.INT, - pretty_name: "Target", array: true, default: undefined, }, - /** Nested array with dimensions that match the grid. Each entry in this array is the key that should be pressed for that corresponding location in the grid. */ + /** The dimensions of this array must match the dimensions of `grid`. Each entry in this array is the key that should be pressed for that corresponding location in the grid. Entries can be left blank if there is no key associated with that location of the grid. */ choices: { type: ParameterType.KEYS, // TO DO: always a nested array, so I think ParameterType.KEYS and array: true is ok here? - pretty_name: "Choices", array: true, default: [["3", "5", "7", "9"]], }, /** The width and height in pixels of each square in the grid. */ grid_square_size: { type: ParameterType.INT, - pretty_name: "Grid square size", default: 100, }, /** The color of the target square. */ target_color: { type: ParameterType.STRING, - pretty_name: "Target color", default: "#999", }, - /** If true, trial ends when user makes a response */ + /** If true, the trial ends after a key press. Feedback is displayed if `show_response_feedback` is true. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** The number of milliseconds to display the grid before the target changes color. */ + /** The number of milliseconds to display the grid *before* the target changes color. */ pre_target_duration: { type: ParameterType.INT, - pretty_name: "Pre-target duration", default: 0, }, - /** How long to show the trial. */ + /** The maximum length of time of the trial, not including feedback. */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, /** If true, show feedback indicating where the user responded and whether it was correct. */ show_response_feedback: { type: ParameterType.BOOL, - pretty_name: "Show response feedback", default: false, }, /** The length of time in milliseconds to show the feedback. */ feedback_duration: { type: ParameterType.INT, - pretty_name: "Feedback duration", default: 200, }, /** If a positive number, the target will progressively change color at the start of the trial, with the transition lasting this many milliseconds. */ fade_duration: { type: ParameterType.INT, - pretty_name: "Fade duration", default: null, }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which keys to press). */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, no_function: false, }, }, + data: { + /** The representation of the grid. This will be encoded as a JSON string when data is saved using + * the `.json()` or `.csv()` functions. */ + grid: { + type: ParameterType.COMPLEX, + array: true, + }, + /** The representation of the target location on the grid. This will be encoded + * as a JSON string when data is saved using the `.json()` or `.csv()` functions */ + target: { + type: ParameterType.COMPLEX, + array: true, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + array: true, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the second stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** `true` if the participant's response matched the target. */ + correct: { + type: ParameterType.BOOL, + }, + }, }; type Info = typeof info; /** - * **serial-reaction-time** - * - * jsPsych plugin for running a serial reaction time task with keypress responses + * The serial reaction time plugin implements a generalized version of the SRT task + * [(Nissen & Bullemer, 1987)](https://doi.org/10.1016%2F0010-0285%2887%2990002-8). + * Squares are displayed in a grid-based system on the screen, and one square changes color. + * The participant presses a key that corresponds to the darkened key. Feedback is optionally displayed, + * showing the participant which square the key they pressed matches. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-serial-reaction-time/ serial-reaction-time plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/serial-reaction-time/ serial-reaction-time plugin documentation on jspsych.org} */ class SerialReactionTimePlugin implements JsPsychPlugin { static info = info; @@ -113,9 +133,6 @@ class SerialReactionTimePlugin implements JsPsychPlugin { }; const endTrial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // kill keyboard listeners this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); @@ -128,9 +145,6 @@ class SerialReactionTimePlugin implements JsPsychPlugin { target: trial.target, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-sketchpad/src/index.spec.ts b/packages/plugin-sketchpad/src/index.spec.ts index 1bc6299e21..fc9645a8d3 100644 --- a/packages/plugin-sketchpad/src/index.spec.ts +++ b/packages/plugin-sketchpad/src/index.spec.ts @@ -19,7 +19,7 @@ describe("sketchpad", () => { expect(displayElement.querySelector("#sketchpad-undo")).not.toBeNull(); expect(displayElement.querySelector("#sketchpad-redo")).not.toBeNull(); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -36,7 +36,7 @@ describe("sketchpad", () => { expect(canvas.getAttribute("width")).toBe("800"); expect(canvas.getAttribute("height")).toBe("300"); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -54,7 +54,7 @@ describe("sketchpad", () => { expect(canvas.getAttribute("width")).toBe("300"); expect(canvas.getAttribute("height")).toBe("300"); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -75,7 +75,7 @@ describe("sketchpad", () => { display_content.indexOf("sketchpad-canvas") ); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -100,7 +100,7 @@ describe("sketchpad", () => { ); expect(display_content.indexOf("prompt")).toBeLessThan(display_content.indexOf("finish-btn")); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -127,7 +127,7 @@ describe("sketchpad", () => { display_content.indexOf("finish-btn") ); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -145,7 +145,7 @@ describe("sketchpad", () => { expect(buttons[1].getAttribute("data-color")).toBe("green"); expect(buttons[2].getAttribute("data-color")).toBe("#0000ff"); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -161,7 +161,7 @@ describe("sketchpad", () => { expect(button.innerHTML).toBe("foo"); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -177,7 +177,7 @@ describe("sketchpad", () => { expect(button.innerHTML).toBe("foo"); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -193,7 +193,7 @@ describe("sketchpad", () => { expect(button.innerHTML).toBe("foo"); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); @@ -209,7 +209,7 @@ describe("sketchpad", () => { expect(button.innerHTML).toBe("foo"); - clickTarget(displayElement.querySelector("#sketchpad-end")); + await clickTarget(displayElement.querySelector("#sketchpad-end")); await expectFinished(); }); }); diff --git a/packages/plugin-sketchpad/src/index.ts b/packages/plugin-sketchpad/src/index.ts index 09ebe1d19d..98dde01c96 100644 --- a/packages/plugin-sketchpad/src/index.ts +++ b/packages/plugin-sketchpad/src/index.ts @@ -1,7 +1,10 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "sketchpad", + version: version, parameters: { /** * The shape of the canvas element. Accepts `'rectangle'` or `'circle'` @@ -11,14 +14,14 @@ const info = { default: "rectangle", }, /** - * Width of the canvas in pixels. + * Width of the canvas in pixels when `canvas_shape` is a `"rectangle"`. */ canvas_width: { type: ParameterType.INT, default: 500, }, /** - * Width of the canvas in pixels. + * Height of the canvas in pixels when `canvas_shape` is a `"rectangle"`. */ canvas_height: { type: ParameterType.INT, @@ -53,7 +56,7 @@ const info = { default: null, }, /** - * Background color of the canvas. + * Color of the canvas background. Note that a `background_image` will render on top of the color. */ background_color: { type: ParameterType.STRING, @@ -67,14 +70,14 @@ const info = { default: 2, }, /** - * The color of the stroke on the canvas + * The color of the stroke on the canvas. */ stroke_color: { type: ParameterType.STRING, default: "#000000", }, /** - * An array of colors to render as a palette of options for stroke colors. + * Array of colors to render as a palette of choices for stroke color. Clicking on the corresponding color button will change the stroke color. */ stroke_color_palette: { type: ParameterType.STRING, @@ -96,36 +99,38 @@ const info = { default: "abovecanvas", }, /** - * Whether to save the final image in the data as dataURL + * Whether to save the final image in the data as a base64 encoded data URL. */ save_final_image: { type: ParameterType.BOOL, default: true, }, /** - * Whether to save the set of strokes that generated the image + * Whether to save the individual stroke data that generated the final image. */ save_strokes: { type: ParameterType.BOOL, default: true, }, /** - * If this key is held down then it is like the mouse button being clicked for controlling - * the flow of the "ink". + * If this key is held down then it is like the mouse button being held down. + * The "ink" will flow when the button is held and stop when it is lifted. + * Pass in the string representation of the key, e.g., `'a'` for the A key + * or `' '` for the spacebar. */ key_to_draw: { type: ParameterType.KEY, default: null, }, /** - * Whether to show the button that ends the trial + * Whether to show the button that ends the trial. */ show_finished_button: { type: ParameterType.BOOL, default: true, }, /** - * The label for the button that ends the trial + * The label for the button that ends the trial. */ finished_button_label: { type: ParameterType.STRING, @@ -175,42 +180,100 @@ const info = { default: "Redo", }, /** - * Array of keys that will end the trial when pressed. + * This array contains the key(s) that the participant is allowed to press in order to end + * the trial. Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, + * `'ArrowDown'`) - see [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) + * and [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/) + * for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"NO_KEYS"` + * means that no keys will be accepted as valid responses. Specifying `"ALL_KEYS"` will mean that all responses are allowed. */ choices: { type: ParameterType.KEYS, default: "NO_KEYS", }, /** - * Length of time before trial ends. If `null` the trial will not timeout. + * Length of time before the trial ends. If `null` the trial will continue indefinitely + * (until another way of ending the trial occurs). */ trial_duration: { type: ParameterType.INT, default: null, }, /** - * Whether to show a countdown timer for the remaining trial duration + * Whether to show a timer that counts down until the end of the trial when `trial_duration` is not `null`. */ show_countdown_trial_duration: { type: ParameterType.BOOL, default: false, }, /** - * The html for the countdown timer. + * The HTML to use for rendering the countdown timer. The element with `id="sketchpad-timer"` + * will have its content replaced by a countdown timer in the format `MM:SS`. */ countdown_timer_html: { type: ParameterType.HTML_STRING, default: ` remaining`, }, }, + data: { + /** The length of time from the start of the trial to the end of the trial. */ + rt: { + type: ParameterType.INT, + }, + /** If the trial was ended by clicking the finished button, then `"button"`. If the trial was ended by pressing a key, then the key that was pressed. If the trial timed out, then `null`. */ + response: { + type: ParameterType.STRING, + }, + /** If `save_final_image` is true, then this will contain the base64 encoded data URL for the image, in png format. */ + png: { + type: ParameterType.STRING, + }, + /** 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. */ + strokes: { + type: ParameterType.COMPLEX, + array: true, + parameters: { + action: { + type: ParameterType.STRING, + }, + x: { + type: ParameterType.INT, + optional: true, + }, + y: { + type: ParameterType.INT, + optional: true, + }, + t: { + type: ParameterType.INT, + optional: true, + }, + color: { + type: ParameterType.STRING, + optional: true, + }, + }, + }, + }, }; type Info = typeof info; /** - * **sketchpad** + * This plugin creates an interactive canvas that the participant can draw on using their mouse or touchscreen. + * It can be used for sketching tasks, like asking the participant to draw a particular object. + * It can also be used for some image segmentation or annotation tasks by setting the `background_image` parameter to render an image on the canvas. + * + * The plugin stores a [base 64 data URL representation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) of the final image. + * This can be converted to an image file using [online tools](https://www.google.com/search?q=base64+image+decoder) or short programs in [R](https://stackoverflow.com/q/58604195/3726673), [python](https://stackoverflow.com/q/2323128/3726673), or another language of your choice. + * It also records all of the individual strokes that the participant made during the trial. + * + * !!! warning + * This plugin generates **a lot** of data. Each trial can easily add 500kb+ of data to a final JSON output. + * You can reduce the amount of data generated by turning off storage of the individual stroke data (`save_strokes: false`) or storage of the final image (`save_final_image: false`) if your use case doesn't require that information. + * If you are going to be collecting a lot of data with this plugin you may want to save your data to your server after each trial and not wait until the end of the experiment to perform a single bulk upload. + * You can do this by putting data saving code inside the [`on_data_update` event handler](../overview/events.md#on_data_update). * - * jsPsych plugin for displaying a canvas stimulus and getting a slider response * * @author Josh de Leeuw * @see {@link https://www.jspsych.org/latest/plugins/sketchpad/ sketchpad plugin documentation on jspsych.org} @@ -646,7 +709,6 @@ class SketchpadPlugin implements JsPsychPlugin { } private end_trial(response = null) { - this.jsPsych.pluginAPI.clearAllTimeouts(); this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); clearInterval(this.timer_interval); @@ -663,8 +725,6 @@ class SketchpadPlugin implements JsPsychPlugin { trial_data.strokes = this.strokes; } - this.display.innerHTML = ""; - document.querySelector("#sketchpad-styles").remove(); this.jsPsych.finishTrial(trial_data); diff --git a/packages/plugin-survey-html-form/src/index.spec.ts b/packages/plugin-survey-html-form/src/index.spec.ts index 103af42764..7f8ec0cd5c 100644 --- a/packages/plugin-survey-html-form/src/index.spec.ts +++ b/packages/plugin-survey-html-form/src/index.spec.ts @@ -22,7 +22,7 @@ describe("survey-html-form plugin", () => { '#jspsych-survey-html-form input[name="second"]' )[0].value = TEST_VALUE; - clickTarget(document.querySelector("#jspsych-survey-html-form-next")); + await clickTarget(document.querySelector("#jspsych-survey-html-form-next")); await expectFinished(); diff --git a/packages/plugin-survey-html-form/src/index.ts b/packages/plugin-survey-html-form/src/index.ts index 5487fba8e4..72871d0156 100644 --- a/packages/plugin-survey-html-form/src/index.ts +++ b/packages/plugin-survey-html-form/src/index.ts @@ -1,56 +1,76 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "survey-html-form", + version: version, parameters: { /** HTML formatted string containing all the input elements to display. Every element has to have its own distinctive name attribute. The
tag must not be included and is generated by the plugin. */ html: { type: ParameterType.HTML_STRING, - pretty_name: "HTML", default: null, }, /** HTML formatted string to display at the top of the page above all the questions. */ preamble: { type: ParameterType.HTML_STRING, - pretty_name: "Preamble", default: null, }, /** The text that appears on the button to finish the trial. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, /** The HTML element ID of a form field to autofocus on. */ autofocus: { type: ParameterType.STRING, - pretty_name: "Element ID to focus", default: "", }, /** Retrieve the data as an array e.g. [{name: "INPUT_NAME", value: "INPUT_VALUE"}, ...] instead of an object e.g. {INPUT_NAME: INPUT_VALUE, ...}. */ dataAsArray: { type: ParameterType.BOOL, - pretty_name: "Data As Array", default: false, }, /** Setting this to true will enable browser auto-complete or auto-fill for the form. */ autocomplete: { type: ParameterType.BOOL, - pretty_name: "Allow autocomplete", default: false, }, }, + data: { + /** 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 participant answered for this particular input. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + response: { + type: ParameterType.COMPLEX, + nested: { + identifier: { + type: ParameterType.STRING, + }, + response: { + type: + ParameterType.STRING | + ParameterType.INT | + ParameterType.FLOAT | + ParameterType.BOOL | + ParameterType.OBJECT, + }, + }, + }, + /** The response time in milliseconds for the participant to make a response. */ + rt: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **survey-html-form** - * - * jsPsych plugin for displaying free HTML forms and collecting responses from all input elements * + * The survey-html-form plugin displays a set of `` from a HTML string. The type of input can be freely + * chosen, for a list of possible input types see the [MDN page on inputs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + * The participant provides answers to the input fields. * @author Jan Simson - * @see {@link https://www.jspsych.org/plugins/jspsych-survey-html-form/ survey-html-form plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/survey-html-form/ survey-html-form plugin documentation on jspsych.org} */ class SurveyHtmlFormPlugin implements JsPsychPlugin { static info = info; @@ -121,8 +141,6 @@ class SurveyHtmlFormPlugin implements JsPsychPlugin { response: question_data, }; - display_element.innerHTML = ""; - // next trial this.jsPsych.finishTrial(trialdata); }); diff --git a/packages/plugin-survey-likert/src/index.spec.ts b/packages/plugin-survey-likert/src/index.spec.ts index b23f7c85e6..e9a5573e17 100644 --- a/packages/plugin-survey-likert/src/index.spec.ts +++ b/packages/plugin-survey-likert/src/index.spec.ts @@ -30,7 +30,7 @@ describe("survey-likert plugin", () => { selectInput("Q3", "3").checked = true; selectInput("Q4", "4").checked = true; - clickTarget(document.querySelector("#jspsych-survey-likert-next")); + await clickTarget(document.querySelector("#jspsych-survey-likert-next")); await expectFinished(); diff --git a/packages/plugin-survey-likert/src/index.ts b/packages/plugin-survey-likert/src/index.ts index 9d92ac3894..b5c7551433 100644 --- a/packages/plugin-survey-likert/src/index.ts +++ b/packages/plugin-survey-likert/src/index.ts @@ -1,37 +1,35 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "survey-likert", + version: version, parameters: { /** Array containing one or more objects with parameters for the question(s) that should be shown on the page. */ questions: { type: ParameterType.COMPLEX, array: true, - pretty_name: "Questions", nested: { /** Question prompt. */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: undefined, }, /** Array of likert labels to display for this question. */ labels: { type: ParameterType.STRING, array: true, - pretty_name: "Labels", default: undefined, }, /** Whether or not a response to this question must be given in order to continue. */ required: { type: ParameterType.BOOL, - pretty_name: "Required", default: false, }, /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */ name: { type: ParameterType.STRING, - pretty_name: "Question Name", default: "", }, }, @@ -39,45 +37,67 @@ const info = { /** If true, the order of the questions in the 'questions' array will be randomized. */ randomize_question_order: { type: ParameterType.BOOL, - pretty_name: "Randomize Question Order", default: false, }, /** HTML-formatted string to display at top of the page above all of the questions. */ preamble: { type: ParameterType.HTML_STRING, - pretty_name: "Preamble", default: null, }, /** Width of the likert scales in pixels. */ scale_width: { type: ParameterType.INT, - pretty_name: "Scale width", default: null, }, /** Label of the button to submit responses. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, /** Setting this to true will enable browser auto-complete or auto-fill for the form. */ autocomplete: { type: ParameterType.BOOL, - pretty_name: "Allow autocomplete", default: false, }, }, + data: { + /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + response: { + type: ParameterType.COMPLEX, + nested: { + identifier: { + type: ParameterType.STRING, + }, + response: { + type: + ParameterType.STRING | + ParameterType.INT | + ParameterType.FLOAT | + ParameterType.BOOL | + ParameterType.OBJECT, + }, + }, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */ + rt: { + type: ParameterType.INT, + }, + /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + question_order: { + type: ParameterType.INT, + array: true, + }, + }, }; type Info = typeof info; /** - * **survey-likert** - * - * jsPsych plugin for gathering responses to questions on a likert scale + * The survey-likert plugin displays a set of questions with Likert scale responses. The participant responds + * by selecting a radio button. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-survey-likert/ survey-likert plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/survey-likert/ survey-likert plugin documentation on jspsych.org} */ class SurveyLikertPlugin implements JsPsychPlugin { static info = info; @@ -209,8 +229,6 @@ class SurveyLikertPlugin implements JsPsychPlugin { question_order: question_order, }; - display_element.innerHTML = ""; - // next trial this.jsPsych.finishTrial(trial_data); }); diff --git a/packages/plugin-survey-multi-choice/src/index.spec.ts b/packages/plugin-survey-multi-choice/src/index.spec.ts index 74177f1542..7d2b42c6d3 100644 --- a/packages/plugin-survey-multi-choice/src/index.spec.ts +++ b/packages/plugin-survey-multi-choice/src/index.spec.ts @@ -32,7 +32,7 @@ describe("survey-multi-choice plugin", () => { getInputElement(3, "d").checked = true; getInputElement(4, "e").checked = true; - clickTarget(document.querySelector("#jspsych-survey-multi-choice-next")); + await clickTarget(document.querySelector("#jspsych-survey-multi-choice-next")); await expectFinished(); diff --git a/packages/plugin-survey-multi-choice/src/index.ts b/packages/plugin-survey-multi-choice/src/index.ts index 1b19bb4a52..69c35f99b2 100644 --- a/packages/plugin-survey-multi-choice/src/index.ts +++ b/packages/plugin-survey-multi-choice/src/index.ts @@ -1,72 +1,110 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "survey-multi-choice", + version: version, parameters: { - /** Array containing one or more objects with parameters for the question(s) that should be shown on the page. */ + /** + * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt, + * options, required, and horizontal parameter that will be applied to the question. See examples below for further + * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be + * associated with a group of options (radio buttons). All questions will get presented on the same page (trial). + * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to + * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates + * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is + * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the + * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing + * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions. + */ questions: { type: ParameterType.COMPLEX, array: true, - pretty_name: "Questions", nested: { /** Question prompt. */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: undefined, }, /** Array of multiple choice options for this question. */ options: { type: ParameterType.STRING, - pretty_name: "Options", array: true, default: undefined, }, /** Whether or not a response to this question must be given in order to continue. */ required: { type: ParameterType.BOOL, - pretty_name: "Required", default: false, }, /** If true, then the question will be centered and options will be displayed horizontally. */ horizontal: { type: ParameterType.BOOL, - pretty_name: "Horizontal", default: false, }, /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */ name: { type: ParameterType.STRING, - pretty_name: "Question Name", default: "", }, }, }, - /** If true, the order of the questions in the 'questions' array will be randomized. */ + /** + * 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. + */ randomize_question_order: { type: ParameterType.BOOL, - pretty_name: "Randomize Question Order", default: false, }, - /** HTML-formatted string to display at top of the page above all of the questions. */ + /** HTML formatted string to display at the top of the page above all the questions. */ preamble: { type: ParameterType.HTML_STRING, - pretty_name: "Preamble", default: null, }, - /** Label of the button to submit responses. */ + /** Label of the button. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, - /** Setting this to true will enable browser auto-complete or auto-fill for the form. */ + /** + * This determines whether or not all of the input elements on the page should allow autocomplete. Setting + * this to true will enable autocomplete or auto-fill for the form. + */ autocomplete: { type: ParameterType.BOOL, - pretty_name: "Allow autocomplete", default: false, }, }, + data: { + /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + response: { + type: ParameterType.COMPLEX, + nested: { + identifier: { + type: ParameterType.STRING, + }, + response: { + type: + ParameterType.STRING | + ParameterType.INT | + ParameterType.FLOAT | + ParameterType.BOOL | + ParameterType.OBJECT, + }, + }, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */ + rt: { + type: ParameterType.INT, + }, + /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + question_order: { + type: ParameterType.INT, + array: true, + }, + }, }; type Info = typeof info; @@ -74,10 +112,10 @@ type Info = typeof info; /** * **survey-multi-choice** * - * jsPsych plugin for presenting multiple choice survey questions + * The survey-multi-choice plugin displays a set of questions with multiple choice response fields. The participant selects a single answer. * * @author Shane Martin - * @see {@link https://www.jspsych.org/plugins/jspsych-survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-choice/ survey-multi-choice plugin documentation on jspsych.org} */ class SurveyMultiChoicePlugin implements JsPsychPlugin { static info = info; @@ -226,7 +264,6 @@ class SurveyMultiChoicePlugin implements JsPsychPlugin { response: question_data, question_order: question_order, }; - display_element.innerHTML = ""; // next trial this.jsPsych.finishTrial(trial_data); diff --git a/packages/plugin-survey-multi-select/src/index.spec.ts b/packages/plugin-survey-multi-select/src/index.spec.ts index fe548904f0..8e5c1894ca 100644 --- a/packages/plugin-survey-multi-select/src/index.spec.ts +++ b/packages/plugin-survey-multi-select/src/index.spec.ts @@ -63,7 +63,7 @@ describe("survey-multi-select plugin", () => { getInputElement(3, "d").checked = true; getInputElement(4, "e").checked = true; - clickTarget(document.querySelector("#jspsych-survey-multi-select-next")); + await clickTarget(document.querySelector("#jspsych-survey-multi-select-next")); await expectFinished(); diff --git a/packages/plugin-survey-multi-select/src/index.ts b/packages/plugin-survey-multi-select/src/index.ts index 403c6a7980..b327543cbe 100644 --- a/packages/plugin-survey-multi-select/src/index.ts +++ b/packages/plugin-survey-multi-select/src/index.ts @@ -1,88 +1,124 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "survey-multi-select", + version: version, parameters: { - /** Array containing one or more objects with parameters for the question(s) that should be shown on the page. */ + /** + * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt, + * options, required, and horizontal parameter that will be applied to the question. See examples below for further + * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be + * associated with a group of options (radio buttons). All questions will get presented on the same page (trial). + * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to + * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates + * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is + * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the + * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing + * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions. + */ questions: { type: ParameterType.COMPLEX, array: true, - pretty_name: "Questions", nested: { /** Question prompt. */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: undefined, }, /** Array of multiple select options for this question. */ options: { type: ParameterType.STRING, - pretty_name: "Options", array: true, default: undefined, }, /** If true, then the question will be centered and options will be displayed horizontally. */ horizontal: { type: ParameterType.BOOL, - pretty_name: "Horizontal", default: false, }, /** Whether or not a response to this question must be given in order to continue. */ required: { type: ParameterType.BOOL, - pretty_name: "Required", default: false, }, /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */ name: { type: ParameterType.STRING, - pretty_name: "Question Name", default: "", }, }, }, - /** If true, the order of the questions in the 'questions' array will be randomized. */ + /** + * 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. + */ randomize_question_order: { type: ParameterType.BOOL, - pretty_name: "Randomize Question Order", default: false, }, - /** HTML-formatted string to display at top of the page above all of the questions. */ + /** HTML formatted string to display at the top of the page above all the questions. */ preamble: { type: ParameterType.HTML_STRING, - pretty_name: "Preamble", default: null, }, /** Label of the button to submit responses. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, - /** Message that will be displayed if one or more required questions is not answered. */ + /** 'You must choose at least one response for this question' | Message to display if required response is not given. */ required_message: { type: ParameterType.STRING, - pretty_name: "Required message", default: "You must choose at least one response for this question", }, - /** Setting this to true will enable browser auto-complete or auto-fill for the form. */ + /** This determines whether or not all of the input elements on the page should allow autocomplete. + * Setting this to true will enable autocomplete or auto-fill for the form. */ autocomplete: { type: ParameterType.BOOL, - pretty_name: "Allow autocomplete", default: false, }, }, + + data: { + /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + response: { + type: ParameterType.COMPLEX, + nested: { + identifier: { + type: ParameterType.STRING, + }, + response: { + type: + ParameterType.STRING | + ParameterType.INT | + ParameterType.FLOAT | + ParameterType.BOOL | + ParameterType.OBJECT, + }, + }, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */ + rt: { + type: ParameterType.INT, + }, + /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + question_order: { + type: ParameterType.INT, + array: true, + }, + }, }; type Info = typeof info; /** - * **survey-multi-select** - * - * jsPsych plugin for presenting multiple choice survey questions with the ability to respond with more than one option + * The survey-multi-select plugin displays a set of questions with multiple select response fields. The participant can + * select multiple answers. * - * @see {@link https://www.jspsych.org/plugins/jspsych-survey-multi-select/ survey-multi-select plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/survey-multi-select/ survey-multi-select plugin documentation on jspsych.org} */ class SurveyMultiSelectPlugin implements JsPsychPlugin { static info = info; @@ -263,7 +299,6 @@ class SurveyMultiSelectPlugin implements JsPsychPlugin { response: question_data, question_order: question_order, }; - display_element.innerHTML = ""; // next trial this.jsPsych.finishTrial(trial_data); diff --git a/packages/plugin-survey-text/src/index.spec.ts b/packages/plugin-survey-text/src/index.spec.ts index ca1274a33a..775e81a835 100644 --- a/packages/plugin-survey-text/src/index.spec.ts +++ b/packages/plugin-survey-text/src/index.spec.ts @@ -2,7 +2,8 @@ import { clickTarget, simulateTimeline, startTimeline } from "@jspsych/test-util import surveyText from "."; -const selectInput = (selector: string) => document.querySelector(selector); +const selectInput = (inputId: number) => + document.querySelector(`#input-${inputId}`); jest.useFakeTimers(); @@ -16,10 +17,10 @@ describe("survey-text plugin", () => { ]); expect(displayElement.querySelectorAll("p.jspsych-survey-text").length).toBe(2); - expect(selectInput("#input-0").size).toBe(40); - expect(selectInput("#input-1").size).toBe(40); + expect(selectInput(0).size).toBe(40); + expect(selectInput(1).size).toBe(40); - clickTarget(document.querySelector("#jspsych-survey-text-next")); + await clickTarget(document.querySelector("#jspsych-survey-text-next")); await expectFinished(); }); @@ -36,16 +37,16 @@ describe("survey-text plugin", () => { ]); expect(displayElement.querySelectorAll("p.jspsych-survey-text").length).toBe(2); - expect(selectInput("#input-0").size).toBe(50); - expect(selectInput("#input-1").size).toBe(20); + expect(selectInput(0).size).toBe(50); + expect(selectInput(1).size).toBe(20); - clickTarget(document.querySelector("#jspsych-survey-text-next")); + await clickTarget(document.querySelector("#jspsych-survey-text-next")); await expectFinished(); }); test("required parameter works", async () => { - const { displayElement } = await startTimeline([ + const { displayElement, expectFinished } = await startTimeline([ { type: surveyText, questions: [ @@ -56,8 +57,12 @@ describe("survey-text plugin", () => { ]); expect(displayElement.querySelectorAll("p.jspsych-survey-text").length).toBe(2); - expect(selectInput("#input-0").required).toBe(true); - expect(selectInput("#input-1").required).toBe(false); + expect(selectInput(0).required).toBe(true); + expect(selectInput(1).required).toBe(false); + + selectInput(0).value = "42"; + await clickTarget(document.querySelector("#jspsych-survey-text-next")); + await expectFinished(); }); test("data are logged with the right question when randomize order is true", async () => { @@ -75,13 +80,13 @@ describe("survey-text plugin", () => { }, ]); - selectInput("#input-0").value = "a0"; - selectInput("#input-1").value = "a1"; - selectInput("#input-2").value = "a2"; - selectInput("#input-3").value = "a3"; - selectInput("#input-4").value = "a4"; + selectInput(0).value = "a0"; + selectInput(1).value = "a1"; + selectInput(2).value = "a2"; + selectInput(3).value = "a3"; + selectInput(4).value = "a4"; - clickTarget(document.querySelector("#jspsych-survey-text-next")); + await clickTarget(document.querySelector("#jspsych-survey-text-next")); await expectFinished(); diff --git a/packages/plugin-survey-text/src/index.ts b/packages/plugin-survey-text/src/index.ts index e5445a5161..112dd0945e 100644 --- a/packages/plugin-survey-text/src/index.ts +++ b/packages/plugin-survey-text/src/index.ts @@ -1,88 +1,123 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "survey-text", + version: version, parameters: { + /** + * An array of objects, each object represents a question that appears on the screen. Each object contains a prompt, + * options, required, and horizontal parameter that will be applied to the question. See examples below for further + * clarification.`prompt`: Type string, default value is *undefined*. The string is prompt/question that will be + * associated with a group of options (radio buttons). All questions will get presented on the same page (trial). + * `options`: Type array, defualt value is *undefined*. An array of strings. The array contains a set of options to + * display for an individual question.`required`: Type boolean, default value is null. The boolean value indicates + * if a question is required('true') or not ('false'), using the HTML5 `required` attribute. If this parameter is + * undefined, the question will be optional. `horizontal`:Type boolean, default value is false. If true, then the + * question is centered and the options are displayed horizontally. `name`: Name of the question. Used for storing + * data. If left undefined then default names (`Q0`, `Q1`, `...`) will be used for the questions. + */ questions: { type: ParameterType.COMPLEX, array: true, - pretty_name: "Questions", default: undefined, nested: { /** Question prompt. */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: undefined, }, /** Placeholder text in the response text box. */ placeholder: { type: ParameterType.STRING, - pretty_name: "Placeholder", default: "", }, /** The number of rows for the response text box. */ rows: { type: ParameterType.INT, - pretty_name: "Rows", default: 1, }, /** The number of columns for the response text box. */ columns: { type: ParameterType.INT, - pretty_name: "Columns", default: 40, }, /** Whether or not a response to this question must be given in order to continue. */ required: { type: ParameterType.BOOL, - pretty_name: "Required", default: false, }, /** Name of the question in the trial data. If no name is given, the questions are named Q0, Q1, etc. */ name: { type: ParameterType.STRING, - pretty_name: "Question Name", default: "", }, }, }, - /** If true, the order of the questions in the 'questions' array will be randomized. */ + /** + * 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. + */ randomize_question_order: { type: ParameterType.BOOL, - pretty_name: "Randomize Question Order", default: false, }, - /** HTML-formatted string to display at top of the page above all of the questions. */ + /** HTML formatted string to display at the top of the page above all the questions. */ preamble: { type: ParameterType.HTML_STRING, - pretty_name: "Preamble", default: null, }, /** Label of the button to submit responses. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, /** Setting this to true will enable browser auto-complete or auto-fill for the form. */ autocomplete: { type: ParameterType.BOOL, - pretty_name: "Allow autocomplete", default: false, }, }, + data: { + /** An object containing the response for each question. The object will have a separate key (variable) for each question, with the first question in the trial being recorded in `Q0`, the second in `Q1`, and so on. The responses are recorded as integers, representing the position selected on the likert scale for that question. If the `name` parameter is defined for the question, then the response object will use the value of `name` as the key for each question. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + response: { + type: ParameterType.COMPLEX, + nested: { + identifier: { + type: ParameterType.STRING, + }, + response: { + type: + ParameterType.STRING | + ParameterType.INT | + ParameterType.FLOAT | + ParameterType.BOOL | + ParameterType.OBJECT, + }, + }, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */ + rt: { + type: ParameterType.INT, + }, + /** An array with the order of questions. For example `[2,0,1]` would indicate that the first question was `trial.questions[2]` (the third item in the `questions` parameter), the second question was `trial.questions[0]`, and the final question was `trial.questions[1]`. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + question_order: { + type: ParameterType.INT, + array: true, + }, + }, }; type Info = typeof info; /** - * **survey-text** * - * jsPsych plugin for free text response survey questions + * The survey-text plugin displays a set of questions with free response text fields. The participant types in answers. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-survey-text/ survey-text plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/survey-text/ survey-text plugin documentation on jspsych.org} */ class SurveyTextPlugin implements JsPsychPlugin { static info = info; @@ -221,8 +256,6 @@ class SurveyTextPlugin implements JsPsychPlugin { response: question_data, }; - display_element.innerHTML = ""; - // next trial this.jsPsych.finishTrial(trialdata); }); diff --git a/packages/plugin-survey/src/index.spec.ts b/packages/plugin-survey/src/index.spec.ts index 149e1db361..0d08d9b8f8 100644 --- a/packages/plugin-survey/src/index.spec.ts +++ b/packages/plugin-survey/src/index.spec.ts @@ -60,7 +60,7 @@ describe("survey plugin", () => { 'input[type="button"].jspsych-nav-complete' ); expect(complete_button).not.toBeNull(); - clickTarget(complete_button); + await clickTarget(complete_button); await expectFinished(); }); @@ -105,7 +105,7 @@ describe("survey plugin", () => { 'input[type="button"].jspsych-nav-complete' ); expect(complete_button).not.toBeNull(); - clickTarget(complete_button); + await clickTarget(complete_button); await expectFinished(); }); @@ -140,7 +140,7 @@ describe("survey plugin", () => { 'input[type="button"].jspsych-nav-complete' ); expect(complete_button).not.toBeNull(); - clickTarget(complete_button); + await clickTarget(complete_button); await expectFinished(); }); diff --git a/packages/plugin-survey/src/index.ts b/packages/plugin-survey/src/index.ts index 71579151be..9b724ae6c7 100644 --- a/packages/plugin-survey/src/index.ts +++ b/packages/plugin-survey/src/index.ts @@ -2,35 +2,68 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; import * as SurveyJS from "survey-knockout-ui"; +import { version } from "../package.json"; + const info = { name: "survey", + version: version, parameters: { /** - * A SurveyJS survey model defined as a JavaScript object. - * See: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#define-a-static-survey-model-in-json + * + * A SurveyJS-compatible JavaScript object that defines the survey (we refer to this as the survey 'JSON' + * for consistency with the SurveyJS documentation, but this parameter should be a JSON-compatible + * JavaScript object rather than a string). If used with the `survey_function` parameter, the survey + * will initially be constructed with this object and then passed to the `survey_function`. See + * the [SurveyJS JSON documentation](https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#define-a-static-survey-model-in-json) for more information. + * */ survey_json: { type: ParameterType.OBJECT, default: {}, - pretty_name: "Survey JSON object", }, /** - * A SurveyJS survey model defined as a function. The function receives an empty SurveyJS survey object as an argument. - * See: https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#create-or-change-a-survey-model-dynamically + * + * A function that receives a SurveyJS survey object as an argument. If no `survey_json` is specified, then + * the function receives an empty survey model and must add all pages/elements to it. If a `survey_json` + * object is provided, then this object forms the basis of the survey model that is passed into the `survey_function`. + * See the [SurveyJS JavaScript documentation](https://surveyjs.io/form-library/documentation/design-survey/create-a-simple-survey#create-or-change-a-survey-model-dynamically) for more information. + * */ survey_function: { type: ParameterType.FUNCTION, default: null, - pretty_name: "Survey function", }, /** - * A function that can be used to validate responses. This function is called whenever the SurveyJS onValidateQuestion event occurs. - * See: https://surveyjs.io/form-library/documentation/data-validation#implement-custom-client-side-validation + * A function that can be used to validate responses. This function is called whenever the SurveyJS `onValidateQuestion` + * event occurs. (Note: it is also possible to add this function to the survey using the `survey_function` parameter - + * we've just added it as a parameter for convenience). */ validation_function: { type: ParameterType.FUNCTION, default: null, - pretty_name: "Validation function", + }, + }, + data: { + /** An object containing the response to each question. The object will have a separate key (identifier) for each question. If the `name` parameter is defined for the question (recommended), then the response object will use the value of `name` as the key for each question. If any questions do not have a name parameter, their keys will named automatically, with the first unnamed question recorded as `question1`, the second as `question2`, and so on. The response type will depend on the question type. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + response: { + type: ParameterType.COMPLEX, + nested: { + identifier: { + type: ParameterType.STRING, + }, + response: { + type: + ParameterType.STRING | + ParameterType.INT | + ParameterType.FLOAT | + ParameterType.BOOL | + ParameterType.OBJECT, + }, + }, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the questions first appear on the screen until the participant's response(s) are submitted. */ + rt: { + type: ParameterType.INT, }, }, }; @@ -63,12 +96,27 @@ const jsPsychSurveyCssClassMap = { }; /** - * **survey** + * SurveyJS version: 1.9.138 + * + * This plugin is a wrapper for the [**SurveyJS form library**](https://surveyjs.io/form-library/documentation/overview). It displays survey-style questions across one or more pages. You can mix different question types on the same page, and participants can navigate back and forth through multiple survey pages without losing responses. SurveyJS provides a large number of built-in question types, response validation options, conditional display options, special response options ("None", "Select all", "Other"), and other useful features for building complex surveys. See the [Building Surveys in jsPsych](../overview/building-surveys.md) page for a more detailed list of all options and features. + * + * With SurveyJS, surveys can be defined using a JavaScript/JSON object, a JavaScript function, or a combination of both. The jsPsych `survey` plugin provides parameters that accept these methods of constructing a SurveyJS survey, and passes them into SurveyJS. The fact that this plugin just acts as a wrapper means you can take advantage of all of the SurveyJS features, and copy/paste directly from SurveyJS examples into the plugin's `survey_json` parameter (for JSON object configuration) or `survey_function` parameter (for JavaScript code). + * + * This page contains the plugin's reference information and examples. The [Building Surveys in jsPsych](../overview/building-surveys.md) page contains a more detailed guide for using this plugin. + * + * For the most comprehensive guides on survey configuration and features, please see the [SurveyJS form library documentation](https://surveyjs.io/form-library/documentation/overview) and [examples](https://surveyjs.io/form-library/examples/overview). + * + * !!! warning "Limitations" + * + * The jsPsych `survey` plugin is not compatible with certain jsPsych and SurveyJS features. Specifically: + * + * - **It is not always well-suited for use with jsPsych's [timeline variables](../overview/timeline.md#timeline-variables) feature.** This is because the timeline variables array must store the entire `survey_json` object for each trial, rather than just the parameters that change across trials, which are nested within the `survey_json` object. We offer some alternative methods for dynamically constructing questions/trials in [this section](../overview/building-surveys.md#defining-survey-trialsquestions-programmatically) of the Building Surveys in jsPsych documentation page. + * - **It does not support the SurveyJS "[complete page](https://surveyjs.io/form-library/documentation/design-survey/create-a-multi-page-survey#complete-page)" parameter.** This is a parameter for HTML formatted content that should appear after the participant clicks the 'submit' button. Instead of using this parameter, you should create another jsPsych trial that comes after the survey trial to serve the same purpose. + * - **It does not support the SurveyJS question's `correctAnswer` property**, which is used for SurveyJS quizzes and automatic response scoring. SurveyJS does not store this value or the response score in the data - instead this is only used to display scores on the survey's 'complete page'. Since the complete page is not supported, this 'correctAnswer' property also does not work as intended in the jsPsych plugin. * - * jsPsych plugin for presenting complex questionnaires using the SurveyJS library * * @author Becky Gilbert - * @see {@link https://www.jspsych.org/plugins/survey/ survey plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/survey/ survey plugin documentation on jspsych.org} */ class SurveyPlugin implements JsPsychPlugin { static info = info; @@ -146,7 +194,6 @@ class SurveyPlugin implements JsPsychPlugin { } // clear display and reset flex on jspsych-content-wrapper - display_element.innerHTML = ""; document.querySelector(".jspsych-content-wrapper").style.display = "flex"; // finish trial and save data diff --git a/packages/plugin-video-button-response/src/index.ts b/packages/plugin-video-button-response/src/index.ts index d5780c4d9a..07997d907a 100644 --- a/packages/plugin-video-button-response/src/index.ts +++ b/packages/plugin-video-button-response/src/index.ts @@ -1,45 +1,58 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "video-button-response", + version: version, parameters: { - /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */ + /** + * An array of file paths to the video. You can specify multiple formats of the same video (e.g., .mp4, .ogg, .webm) + * to maximize the [cross-browser compatibility](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats). + * Usually .mp4 is a safe cross-browser option. The plugin does not reliably support .mov files. The player will use the + * first source file in the array that is compatible with the browser, so specify the files in order of preference. + */ stimulus: { type: ParameterType.VIDEO, - pretty_name: "Video", default: undefined, array: true, }, - /** Array containing the label(s) for the button(s). */ + /** + * Labels for the buttons. Each different string in the array will generate a different button. + */ choices: { type: ParameterType.STRING, - pretty_name: "Choices", default: undefined, array: true, }, - /** The HTML for creating button. Can create own style. Use the "%choice%" string to indicate where the label from the choices parameter should be inserted. */ + /** + * A function that generates the HTML for each button in the `choices` array. The function gets the string and index + * of the item in the `choices` array and should return valid HTML. If you want to use different markup for each + * button, you can do that by using a conditional on either parameter. The default parameter returns a button element + * with the text label of the choice. + */ button_html: { - type: ParameterType.HTML_STRING, - pretty_name: "Button HTML", - default: '', - array: true, + type: ParameterType.FUNCTION, + default: function (choice: string, choice_index: number) { + return ``; + }, }, - /** Any content here will be displayed below the buttons. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is + * that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which + * key to press). + */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, - /** The width of the video in pixels. */ + /** The width of the video display in pixels. */ width: { type: ParameterType.INT, - pretty_name: "Width", default: "", }, /** The height of the video display in pixels. */ height: { type: ParameterType.INT, - pretty_name: "Height", default: "", }, /** If true, the video will begin playing as soon as it has loaded. */ @@ -48,84 +61,125 @@ const info = { pretty_name: "Autoplay", default: true, }, - /** If true, the subject will be able to pause the video or move the playback to any point in the video. */ + /** If true, controls for the video player will be available to the participant. They will be able to pause + * the video or move the playback to any point in the video. + */ controls: { type: ParameterType.BOOL, - pretty_name: "Controls", default: false, }, /** Time to start the clip. If null (default), video will start at the beginning of the file. */ start: { type: ParameterType.FLOAT, - pretty_name: "Start", default: null, }, /** Time to stop the clip. If null (default), video will stop at the end of the file. */ stop: { type: ParameterType.FLOAT, - pretty_name: "Stop", default: null, }, /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */ rate: { type: ParameterType.FLOAT, - pretty_name: "Rate", default: 1, }, /** If true, the trial will end immediately after the video finishes playing. */ trial_ends_after_video: { type: ParameterType.BOOL, - pretty_name: "End trial after video finishes", default: false, }, - /** How long to show trial before it ends. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the + * participant fails to make a response before this timer is reached, the participant's response will be + * recorded as null for the trial and the trial will end. If the value of this parameter is null, then the + * trial will wait for a response indefinitely. + */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** The vertical margin of the button. */ - margin_vertical: { + /** Setting to `'grid'` will make the container element have the CSS property `display: grid` and enable the + * use of `grid_rows` and `grid_columns`. Setting to `'flex'` will make the container element have the CSS + * property `display: flex`. You can customize how the buttons are laid out by adding inline CSS in the + * `button_html` parameter. + */ + button_layout: { type: ParameterType.STRING, - pretty_name: "Margin vertical", - default: "0px", + default: "grid", }, - /** The horizontal margin of the button. */ - margin_horizontal: { - type: ParameterType.STRING, - pretty_name: "Margin horizontal", - default: "8px", + /** + * The number of rows in the button grid. Only applicable when `button_layout` is set to `'grid'`. If null, + * the number of rows will be determined automatically based on the number of buttons and the number of columns. + */ + grid_rows: { + type: ParameterType.INT, + default: 1, }, - /** If true, the trial will end when subject makes a response. */ + /** The number of grid columns when `button_layout` is "grid". + * Setting to `null` (default value) will infer the number of columns + * based on the number of rows and buttons. */ + grid_columns: { + type: ParameterType.INT, + default: null, + }, + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response + * before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until + * the value for `trial_duration` is reached. You can set this parameter to `false` to force the participant + * to view a stimulus for a fixed amount of time, even if they respond before the time is complete. + */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */ + /** If true, then responses are allowed while the video is playing. If false, then the video must finish + * playing before the button choices are enabled and a response is accepted. Once the video has played + * all the way through, the buttons are enabled and a response is allowed (including while the video is + * being re-played via on-screen playback controls). + */ response_allowed_while_playing: { type: ParameterType.BOOL, - pretty_name: "Response allowed while playing", default: true, }, - /** The delay of enabling button */ + /** How long the button will delay enabling in milliseconds. If `response_allowed_while_playing` is `true`, + * the timer will start immediately. If it is `false`, the timer will start at the end of the video. + */ enable_button_after: { type: ParameterType.INT, - pretty_name: "Enable button after", default: 0, }, }, + data: { + /** Indicates which button the participant pressed. The first button in the `choices` array is 0, the second is 1, and so on. */ + response: { + type: ParameterType.INT, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + stimulus: { + type: ParameterType.STRING, + array: true, + }, + }, }; type Info = typeof info; /** - * **video-button-response** + * This plugin plays a video and records responses generated by button click. The stimulus can be displayed until a response is given, + * or for a pre-determined amount of time. The trial can be ended automatically when the participant responds, when the video file has + * finished playing, or if the participant has failed to respond within a fixed length of time. You can also prevent a button response + * from being made before the video has finished playing. The button itself can be customized using HTML formatting. * - * jsPsych plugin for playing a video file and getting a button response + * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using + * timeline variables or another dynamic method to specify the video stimulus, you will need to + * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. + * Also note that video preloading is disabled when the experiment is running as a file (i.e. opened directly in the browser, + * rather than through a server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md) for more information. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-video-button-response/ video-button-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/video-button-response/ video-button-response plugin documentation on jspsych.org} */ class VideoButtonResponsePlugin implements JsPsychPlugin { static info = info; @@ -133,109 +187,100 @@ class VideoButtonResponsePlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { - if (!Array.isArray(trial.stimulus)) { - throw new Error(` - The stimulus property for the video-button-response plugin must be an array - of files. See https://www.jspsych.org/latest/plugins/video-button-response/#parameters - `); - } + // Setup stimulus + const stimulusWrapper = document.createElement("div"); + display_element.appendChild(stimulusWrapper); - // setup stimulus - var video_html = "
"; - video_html += '"; - video_html += "
"; - - //display buttons - var buttons = []; - if (Array.isArray(trial.button_html)) { - if (trial.button_html.length == trial.choices.length) { - buttons = trial.button_html; - } else { - console.error( - "Error in video-button-response plugin. The length of the button_html array does not equal the length of the choices array" + + // Display buttons + const buttonGroupElement = document.createElement("div"); + buttonGroupElement.id = "jspsych-video-button-response-btngroup"; + if (trial.button_layout === "grid") { + buttonGroupElement.classList.add("jspsych-btn-group-grid"); + if (trial.grid_rows === null && trial.grid_columns === null) { + throw new Error( + "You cannot set `grid_rows` to `null` without providing a value for `grid_columns`." ); } - } else { - for (var i = 0; i < trial.choices.length; i++) { - buttons.push(trial.button_html); - } + const n_cols = + trial.grid_columns === null + ? Math.ceil(trial.choices.length / trial.grid_rows) + : trial.grid_columns; + const n_rows = + trial.grid_rows === null + ? Math.ceil(trial.choices.length / trial.grid_columns) + : trial.grid_rows; + buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`; + buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`; + } else if (trial.button_layout === "flex") { + buttonGroupElement.classList.add("jspsych-btn-group-flex"); } - video_html += '
'; - for (var i = 0; i < trial.choices.length; i++) { - var str = buttons[i].replace(/%choice%/g, trial.choices[i]); - video_html += - '
' + - str + - "
"; + + for (const [choiceIndex, choice] of trial.choices.entries()) { + buttonGroupElement.insertAdjacentHTML("beforeend", trial.button_html(choice, choiceIndex)); + const buttonElement = buttonGroupElement.lastChild as HTMLElement; + buttonElement.dataset.choice = choiceIndex.toString(); + buttonElement.addEventListener("click", () => { + after_response(choiceIndex); + }); } - video_html += "
"; - // add prompt if there is one + display_element.appendChild(buttonGroupElement); + + // Show prompt if there is one if (trial.prompt !== null) { - video_html += trial.prompt; + display_element.insertAdjacentHTML("beforeend", trial.prompt); } - display_element.innerHTML = video_html; - var start_time = performance.now(); - var video_element = display_element.querySelector( - "#jspsych-video-button-response-stimulus" - ); - - if (video_preload_blob) { - video_element.src = video_preload_blob; + if (videoPreloadBlob) { + videoElement.src = videoPreloadBlob; } - video_element.onended = () => { + videoElement.onended = () => { if (trial.trial_ends_after_video) { end_trial(); } else if (!trial.response_allowed_while_playing) { @@ -243,37 +288,36 @@ class VideoButtonResponsePlugin implements JsPsychPlugin { } }; - video_element.playbackRate = trial.rate; + videoElement.playbackRate = trial.rate; // if video start time is specified, hide the video and set the starting time // before showing and playing, so that the video doesn't automatically show the first frame if (trial.start !== null) { - video_element.pause(); - video_element.onseeked = () => { - video_element.style.visibility = "visible"; - video_element.muted = false; + videoElement.pause(); + videoElement.onseeked = () => { + videoElement.style.visibility = "visible"; + videoElement.muted = false; if (trial.autoplay) { - video_element.play(); + videoElement.play(); } else { - video_element.pause(); + videoElement.pause(); } - video_element.onseeked = () => {}; + videoElement.onseeked = () => {}; }; - video_element.onplaying = () => { - video_element.currentTime = trial.start; - video_element.onplaying = () => {}; + videoElement.onplaying = () => { + videoElement.currentTime = trial.start; + videoElement.onplaying = () => {}; }; // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play, // change current time, then show/unmute - video_element.muted = true; - video_element.play(); + videoElement.muted = true; + videoElement.play(); } let stopped = false; if (trial.stop !== null) { - video_element.addEventListener("timeupdate", (e) => { - var currenttime = video_element.currentTime; - if (currenttime >= trial.stop) { + videoElement.addEventListener("timeupdate", (e) => { + if (videoElement.currentTime >= trial.stop) { if (!trial.response_allowed_while_playing) { if (trial.enable_button_after > 0) { enable_buttons_delayed(trial.enable_button_after); @@ -281,7 +325,7 @@ class VideoButtonResponsePlugin implements JsPsychPlugin { enable_buttons(); } } - video_element.pause(); + videoElement.pause(); if (trial.trial_ends_after_video && !stopped) { // this is to prevent end_trial from being called twice, because the timeupdate event // can fire in quick succession @@ -298,7 +342,7 @@ class VideoButtonResponsePlugin implements JsPsychPlugin { if (trial.response_allowed_while_playing) { disable_buttons(); - if (trial.enable_button_after !== null) { + if (trial.enable_button_after > 0) { enable_buttons_delayed(trial.enable_button_after); } else { enable_buttons(); @@ -315,43 +359,33 @@ class VideoButtonResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // stop the video file if it is playing // remove any remaining end event handlers - display_element - .querySelector("#jspsych-video-button-response-stimulus") - .pause(); - display_element.querySelector( - "#jspsych-video-button-response-stimulus" - ).onended = () => {}; + videoElement.pause(); + videoElement.onended = () => {}; // gather the data to store for the trial - var trial_data = { + const trial_data = { rt: response.rt, stimulus: trial.stimulus, response: response.button, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; // function to handle responses by the subject - function after_response(choice: string) { + function after_response(choice: number) { // measure rt var end_time = performance.now(); var rt = Math.round(end_time - start_time); - response.button = parseInt(choice); + response.button = choice; response.rt = rt; // after a valid response, the stimulus will have the CSS class 'responded' // which can be used to provide visual feedback that a response was recorded - video_element.className += " responded"; + videoElement.classList.add("responded"); // disable all the buttons after a response disable_buttons(); @@ -361,30 +395,15 @@ class VideoButtonResponsePlugin implements JsPsychPlugin { } } - function button_response(e) { - var choice = e.currentTarget.getAttribute("data-choice"); // don't use dataset for jsdom compatibility - after_response(choice); - } - function disable_buttons() { - var btns = document.querySelectorAll(".jspsych-video-button-response-button"); - for (var i = 0; i < btns.length; i++) { - var btn_el = btns[i].querySelector("button"); - if (btn_el) { - btn_el.disabled = true; - } - btns[i].removeEventListener("click", button_response); + for (const button of buttonGroupElement.children) { + button.setAttribute("disabled", "disabled"); } } function enable_buttons() { - var btns = document.querySelectorAll(".jspsych-video-button-response-button"); - for (var i = 0; i < btns.length; i++) { - var btn_el = btns[i].querySelector("button"); - if (btn_el) { - btn_el.disabled = false; - } - btns[i].addEventListener("click", button_response); + for (const button of buttonGroupElement.children) { + button.removeAttribute("disabled"); } } @@ -446,7 +465,9 @@ class VideoButtonResponsePlugin implements JsPsychPlugin { const respond = () => { if (data.rt !== null) { this.jsPsych.pluginAPI.clickTarget( - display_element.querySelector(`div[data-choice="${data.response}"] button`), + display_element.querySelector( + `#jspsych-video-button-response-btngroup [data-choice="${data.response}"]` + ), data.rt ); } diff --git a/packages/plugin-video-keyboard-response/src/index.ts b/packages/plugin-video-keyboard-response/src/index.ts index 16a5664fd7..d98156e928 100644 --- a/packages/plugin-video-keyboard-response/src/index.ts +++ b/packages/plugin-video-keyboard-response/src/index.ts @@ -1,7 +1,10 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "video-keyboard-response", + version: version, parameters: { /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */ stimulus: { @@ -89,17 +92,42 @@ const info = { default: true, }, }, + data: { + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the + * stimulus first appears on the screen until the participant's response. + * */ + rt: { + type: ParameterType.INT, + }, + /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + stimulus: { + type: ParameterType.STRING, + array: true, + }, + }, }; type Info = typeof info; /** - * **video-keyboard-response** + * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is + * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds, + * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time. + * You can also prevent a keyboard response from being recorded before the video has finished playing. * - * jsPsych plugin for playing a video file and getting a keyboard response + * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are + * using timeline variables or another dynamic method to specify the video stimulus, you will need to + * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading + * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a + * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md) + * for more information. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org} */ class VideoKeyboardResponsePlugin implements JsPsychPlugin { static info = info; @@ -250,9 +278,6 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // kill keyboard listeners this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); @@ -272,9 +297,6 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin { response: response.key, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-video-slider-response/src/index.ts b/packages/plugin-video-slider-response/src/index.ts index 605f019a67..1e8466b050 100644 --- a/packages/plugin-video-slider-response/src/index.ts +++ b/packages/plugin-video-slider-response/src/index.ts @@ -1,148 +1,187 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "video-slider-response", + version: version, parameters: { - /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */ + /** An array of file paths to the video. You can specify multiple formats of the same video (e.g., .mp4, .ogg, .webm) + * to maximize the [cross-browser compatibility](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats). + * Usually .mp4 is a safe cross-browser option. The plugin does not reliably support .mov files. The player will use + * the first source file in the array that is compatible with the browser, so specify the files in order of preference. + */ stimulus: { type: ParameterType.VIDEO, - pretty_name: "Video", default: undefined, array: true, }, - /** Any content here will be displayed below the stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that + * it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). + */ prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Prompt", default: null, }, /** The width of the video in pixels. */ width: { type: ParameterType.INT, - pretty_name: "Width", default: "", }, /** The height of the video display in pixels. */ height: { type: ParameterType.INT, - pretty_name: "Height", default: "", }, /** If true, the video will begin playing as soon as it has loaded. */ autoplay: { type: ParameterType.BOOL, - pretty_name: "Autoplay", default: true, }, - /** If true, the subject will be able to pause the video or move the playback to any point in the video. */ + /** If true, controls for the video player will be available to the participant. They will be able to pause the + * video or move the playback to any point in the video. + */ controls: { type: ParameterType.BOOL, - pretty_name: "Controls", default: false, }, /** Time to start the clip. If null (default), video will start at the beginning of the file. */ start: { type: ParameterType.FLOAT, - pretty_name: "Start", default: null, }, /** Time to stop the clip. If null (default), video will stop at the end of the file. */ stop: { type: ParameterType.FLOAT, - pretty_name: "Stop", default: null, }, /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */ rate: { type: ParameterType.FLOAT, - pretty_name: "Rate", default: 1, }, /** Sets the minimum value of the slider. */ min: { type: ParameterType.INT, - pretty_name: "Min slider", default: 0, }, /** Sets the maximum value of the slider. */ max: { type: ParameterType.INT, - pretty_name: "Max slider", default: 100, }, /** Sets the starting value of the slider. */ slider_start: { type: ParameterType.INT, - pretty_name: "Slider starting value", default: 50, }, - /** Sets the step of the slider. */ + /** Sets the step of the slider. This is the smallest amount by which the slider can change. */ step: { type: ParameterType.INT, - pretty_name: "Step", default: 1, }, - /** Array containing the labels for the slider. Labels will be displayed at equidistant locations along the slider. */ + /** + * Labels displayed at equidistant locations on the slider. For example, two labels will be placed at the ends + * of the slider. Three labels would place two at the ends and one in the middle. Four will place two at the + * ends, and the other two will be at 33% and 67% of the slider width. + */ labels: { type: ParameterType.HTML_STRING, - pretty_name: "Labels", default: [], array: true, }, - /** Width of the slider in pixels. */ + /** Set the width of the slider in pixels. If left null, then the width will be equal to the widest element in + * the display. + */ slider_width: { type: ParameterType.INT, - pretty_name: "Slider width", default: null, }, - /** Label of the button to advance. */ + /** Label of the button to end the trial. */ button_label: { type: ParameterType.STRING, - pretty_name: "Button label", default: "Continue", }, - /** If true, the participant will have to move the slider before continuing. */ + /** If true, the participant must move the slider before clicking the continue button. */ require_movement: { type: ParameterType.BOOL, - pretty_name: "Require movement", default: false, }, /** If true, the trial will end immediately after the video finishes playing. */ trial_ends_after_video: { type: ParameterType.BOOL, - pretty_name: "End trial after video finishes", default: false, }, - /** How long to show trial before it ends. */ + /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the + * participant fails to make a response before this timer is reached, the participant's response will be + * recorded as null for the trial and the trial will end. If the value of this parameter is null, then the + * trial will wait for a response indefinitely. + */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, - /** If true, the trial will end when subject makes a response. */ + /** If true, then the trial will end whenever the participant makes a response (assuming they make their response + * before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until + * the value for `trial_duration` is reached. You can set this parameter to `false` to force the participant + * to view a stimulus for a fixed amount of time, even if they respond before the time is complete. + */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, - /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */ + /** + * If true, then responses are allowed while the video is playing. If false, then the video must finish playing + * before the slider is enabled and the trial can end via the next button click. Once the video has played all + * the way through, the slider is enabled and a response is allowed (including while the video is being re-played + * via on-screen playback controls). + */ response_allowed_while_playing: { type: ParameterType.BOOL, - pretty_name: "Response allowed while playing", default: true, }, }, + data: { + /** The numeric value of the slider. */ + response: { + type: ParameterType.INT, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + stimulus: { + type: ParameterType.STRING, + array: true, + }, + /** The starting value of the slider. */ + slider_start: { + type: ParameterType.INT, + }, + /** The start time of the video clip. */ + start: { + type: ParameterType.FLOAT, + }, + }, }; type Info = typeof info; /** - * **video-slider-response** + * This plugin plays a video and allows the participant to respond by dragging a slider. The stimulus can be displayed + * until a response is given, or for a pre-determined amount of time. The trial can be ended automatically when the + * participant responds, when the video file has finished playing, or if the participant has failed to respond within + * a fixed length of time. You can also prevent the slider response from being made before the video has finished playing. * - * jsPsych plugin for playing a video file and getting a slider response + * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are + * using timeline variables or another dynamic method to specify the video stimulus, you will need to + * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading + * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a + * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md) for more information. * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-video-slider-response/ video-slider-response plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/video-slider-response/ video-slider-response plugin documentation on jspsych.org} */ class VideoSliderResponsePlugin implements JsPsychPlugin { static info = info; @@ -361,9 +400,6 @@ class VideoSliderResponsePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - // stop the video file if it is playing // remove any remaining end event handlers display_element @@ -382,9 +418,6 @@ class VideoSliderResponsePlugin implements JsPsychPlugin { response: response.response, }; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-virtual-chinrest/src/index.ts b/packages/plugin-virtual-chinrest/src/index.ts index c29cfe057a..44bfab4a83 100644 --- a/packages/plugin-virtual-chinrest/src/index.ts +++ b/packages/plugin-virtual-chinrest/src/index.ts @@ -1,19 +1,27 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "virtual-chinrest", + version: version, parameters: { - /** What units to resize to? ["none"/"cm"/"inch"/"deg"]. If "none", no resizing will be done to the jsPsych content after this trial. */ + /** + * Units to resize the jsPsych content to after the trial is over: `"none"` `"cm"` `"inch"` or `"deg"`. + * If `"none"`, no resizing will be done to the jsPsych content after the virtual-chinrest trial ends. + */ resize_units: { type: ParameterType.SELECT, - pretty_name: "Resize units", options: ["none", "cm", "inch", "deg"], default: "none", }, - /** After the scaling factor is applied, this many pixels will equal one unit of measurement. */ + /** + * After the scaling factor is applied, this many pixels will equal one unit of measurement, where + * the units are indicated by `resize_units`. This is only used when resizing is done after the + * trial ends (i.e. the `resize_units` parameter is not "none"). + */ pixels_per_unit: { type: ParameterType.INT, - pretty_name: "Pixels per unit", default: 100, }, // mouse_adjustment: { @@ -21,10 +29,11 @@ const info = { // pretty_name: "Adjust Using Mouse?", // default: true, // }, - /** Any content here will be displayed above the card stimulus. */ + /** This string can contain HTML markup. Any content here will be displayed + * **below the card stimulus** during the resizing phase. + */ adjustment_prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Adjustment prompt", default: `

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

@@ -32,44 +41,41 @@ const info = {

If you do not have access to a real card you can use a ruler to measure the image width to 3.37 inches or 85.6 mm.

`, }, - /** Content of the button displayed below the card stimulus. */ + /** Content of the button displayed below the card stimulus during the resizing phase. */ adjustment_button_prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Adjustment button prompt", default: "Click here when the image is the correct size", }, - /** Path to an image to be shown in the resizable item div. */ + /** Path of the item to be presented in the card stimulus during the resizing phase. If `null` then no + * image is shown, and a solid color background is used instead. _An example image is available in + * `/examples/img/card.png`_ + */ item_path: { type: ParameterType.IMAGE, - pretty_name: "Item path", default: null, preload: false, }, - /** The height of the item to be measured, in mm. */ + /** The known height of the physical item (e.g. credit card) to be measured, in mm. */ item_height_mm: { type: ParameterType.FLOAT, - pretty_name: "Item height (mm)", default: 53.98, }, - /** The width of the item to be measured, in mm. */ + /** The known width of the physical item (e.g. credit card) to be measured, in mm. */ item_width_mm: { type: ParameterType.FLOAT, - pretty_name: "Item width (mm)", default: 85.6, }, - /** The initial size of the card, in pixels, along the largest dimension. */ + /** The initial size of the card stimulus, in pixels, along its largest dimension. */ item_init_size: { type: ParameterType.INT, - pretty_name: "Initial Size", default: 250, }, - /** How many times to measure the blindspot location? If 0, blindspot will not be detected, and viewing distance and degree data not computed. */ + /** How many times to measure the blindspot location. If `0`, blindspot will not be detected, and viewing distance and degree data will not be computed. */ blindspot_reps: { type: ParameterType.INT, - pretty_name: "Blindspot measurement repetitions", default: 5, }, - /** HTML-formatted prompt to be shown on the screen during blindspot estimates. */ + /** This string can contain HTML markup. Any content here will be displayed **above the blindspot task**. */ blindspot_prompt: { type: ParameterType.HTML_STRING, pretty_name: "Blindspot prompt", @@ -92,32 +98,78 @@ const info = { // pretty_name: "Blindspot start prompt", // default: "Start" // }, - /** Text accompanying the remaining measurements counter. */ + /** Text accompanying the remaining measurements counter that appears below the blindspot task. */ blindspot_measurements_prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Blindspot measurements prompt", default: "Remaining measurements: ", }, - /** HTML-formatted string for reporting the distance estimate. It can contain a span with ID 'distance-estimate', which will be replaced with the distance estimate. If "none" is given, viewing distance will not be reported to the participant. */ + /** Estimated viewing distance data displayed after blindspot task. If `"none"` is given, viewing distance will not be reported to the participant. The HTML `span` element with `id = distance-estimate` returns the distance. */ viewing_distance_report: { type: ParameterType.HTML_STRING, - pretty_name: "Viewing distance report", default: "

Based on your responses, you are sitting about from the screen.

Does that seem about right?

", }, - /** Label for the button that can be clicked on the viewing distance report screen to re-do the blindspot estimate(s). */ + /** Text for the button on the viewing distance report page to re-do the viewing distance estimate. If the participant click this button, the blindspot task starts again. */ redo_measurement_button_label: { type: ParameterType.HTML_STRING, - pretty_name: "Re-do measurement button label", default: "No, that is not close. Try again.", }, - /** Label for the button that can be clicked on the viewing distance report screen to accept the viewing distance estimate. */ + /** Text for the button on the viewing distance report page that can be clicked to accept the viewing distance estimate. */ blindspot_done_prompt: { type: ParameterType.HTML_STRING, - pretty_name: "Blindspot done prompt", default: "Yes", }, }, + data: { + /** The response time in milliseconds. */ + rt: { + type: ParameterType.INT, + }, + /** The height in millimeters of the item to be measured. */ + item_height_mm: { + type: ParameterType.FLOAT, + }, + /** The width in millimeters of the item to be measured. */ + item_width_mm: { + type: ParameterType.FLOAT, + }, + /** Final height of the resizable div container, in degrees. */ + item_height_deg: { + type: ParameterType.FLOAT, + }, + /** Final width of the resizable div container, in degrees. */ + item_width_deg: { + type: ParameterType.FLOAT, + }, + /** Final width of the resizable div container, in pixels. */ + item_width_px: { + type: ParameterType.FLOAT, + }, + /** Pixels to degrees conversion factor. */ + px2deg: { + type: ParameterType.INT, + }, + /** Pixels to millimeters conversion factor. */ + px2mm: { + type: ParameterType.FLOAT, + }, + /** Scaling factor that will be applied to the div containing jsPsych content. */ + scale_factor: { + type: ParameterType.FLOAT, + }, + /** The interior width of the window in degrees. */ + win_width_deg: { + type: ParameterType.FLOAT, + }, + /** The interior height of the window in degrees. */ + win_height_deg: { + type: ParameterType.FLOAT, + }, + /** Estimated distance to the screen in millimeters. */ + view_dist_mm: { + type: ParameterType.FLOAT, + }, + }, }; type Info = typeof info; @@ -134,14 +186,30 @@ declare global { } /** - * **virtual-chinrest** * - * jsPsych plugin for estimating physical distance from monitor and optionally resizing experiment content, based on Qisheng Li 11/2019. /// https://github.com/QishengLi/virtual_chinrest + * This plugin provides a "virtual chinrest" that can measure the distance between the participant and the screen. It + * can also standardize the jsPsych page content to a known physical dimension (e.g., ensuring that a 200px wide stimulus + * is 2.2cm wide on the participant's monitor). This is based on the work of [Li, Joo, Yeatman, and Reinecke + * (2020)](https://doi.org/10.1038/s41598-019-57204-1), and the plugin code is a modified version of + * [their implementation](https://github.com/QishengLi/virtual_chinrest). We recommend citing their work in any paper + * that makes use of this plugin. + * + * !!! note "Citation" + * Li, Q., Joo, S. J., Yeatman, J. D., & Reinecke, K. (2020). Controlling for Participants’ Viewing Distance in Large-Scale, Psychophysical Online Experiments Using a Virtual Chinrest. _Scientific Reports, 10_(1), 1-11. doi: [10.1038/s41598-019-57204-1](https://doi.org/10.1038/s41598-019-57204-1) + * + * The plugin works in two phases. + * + * **Phase 1**. To calculate the pixel-to-cm conversion rate for a participant’s display, participants are asked to place + * a credit card or other item of the same size on the screen and resize an image until it is the same size as the credit + * card. Since we know the physical dimensions of the card, we can find the conversion rate for the participant's display. + * + * **Phase 2**. To measure the participant's viewing distance from their screen we use a [blind spot]() task. Participants are asked to focus on a black square on the screen with their right eye closed, while a red dot repeatedly sweeps from right to left. They press the spacebar on their keyboard whenever they perceive that the red dot has disappeared. This part allows the plugin to use the distance between the black square and the red dot when it disappears from eyesight to estimate how far the participant is from the monitor. This estimation assumes that the blind spot is located at 13.5° temporally. + * * * @author Gustavo Juantorena * 08/2020 // https://github.com/GEJ1 * Contributions from Peter J. Kohler: https://github.com/pjkohler - * @see {@link https://www.jspsych.org/plugins/jspsych-virtual-chinrest/ virtual-chinrest plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/virtual-chinrest/ virtual-chinrest plugin documentation on jspsych.org} */ class VirtualChinrestPlugin implements JsPsychPlugin { static info = info; @@ -430,9 +498,6 @@ class VirtualChinrestPlugin implements JsPsychPlugin { // compute final data computeTransformation(); - // clear the display - display_element.innerHTML = ""; - // finish the trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-visual-search-circle/src/index.spec.ts b/packages/plugin-visual-search-circle/src/index.spec.ts index bd530e49b5..655b896d89 100644 --- a/packages/plugin-visual-search-circle/src/index.spec.ts +++ b/packages/plugin-visual-search-circle/src/index.spec.ts @@ -1,4 +1,4 @@ -import { pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils"; +import { flushPromises, pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils"; import visualSearchCircle from "."; @@ -22,9 +22,10 @@ describe("visual-search-circle", () => { expect(displayElement.querySelectorAll("img").length).toBe(1); jest.advanceTimersByTime(1000); // fixation duration + await flushPromises(); expect(displayElement.querySelectorAll("img").length).toBe(5); - pressKey("a"); + await pressKey("a"); await expectFinished(); expect(displayElement.querySelectorAll("img").length).toBe(0); diff --git a/packages/plugin-visual-search-circle/src/index.ts b/packages/plugin-visual-search-circle/src/index.ts index e329632d7a..0024e8178b 100644 --- a/packages/plugin-visual-search-circle/src/index.ts +++ b/packages/plugin-visual-search-circle/src/index.ts @@ -1,114 +1,149 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "visual-search-circle", + version: version, parameters: { - /** The target image to be displayed. This must specified when using the target, foil and set_size parameters to define the stimuli set, rather than the stimuli parameter. */ + /** + * Path to image file that is the search target. This parameter must specified when the stimuli set is + * defined using the `target`, `foil` and `set_size` parameters, but should NOT be specified when using + * the `stimuli` parameter. + */ target: { type: ParameterType.IMAGE, - pretty_name: "Target", default: null, }, - /** The image to use as the foil/distractor. This must specified when using the target, foil and set_size parameters to define the stimuli set, rather than the stimuli parameter. */ + /** + * Path to image file that is the foil/distractor. This image will be repeated for all distractors up to + * the `set_size` value. This parameter must specified when the stimuli set is defined using the `target`, + * `foil` and `set_size` parameters, but should NOT be specified when using the `stimuli` parameter. + */ foil: { type: ParameterType.IMAGE, - pretty_name: "Foil", default: null, }, - /** How many items should be displayed, including the target when target_present is true? This must specified when using the target, foil and set_size parameters to define the stimuli set, rather than the stimuli parameter. */ + /** + * How many items should be displayed, including the target when `target_present` is `true`. The foil + * image will be repeated up to this value when `target_present` is `false`, or up to `set_size - 1` + * when `target_present` is `true`. This parameter must specified when using the `target`, `foil` and + * `set_size` parameters to define the stimuli set, but should NOT be specified when using the `stimuli` + * parameter. + */ set_size: { type: ParameterType.INT, - pretty_name: "Set size", default: null, }, - /** Array containing one or more image files to be displayed. This only needs to be specified when NOT using the target, foil, and set_size parameters to define the stimuli set. */ + /** + * Array containing all of the image files to be displayed. This parameter must be specified when NOT + * using the `target`, `foil`, and `set_size` parameters to define the stimuli set. + */ stimuli: { type: ParameterType.IMAGE, - pretty_name: "Stimuli", - default: null, + default: [], array: true, }, /** - * Is the target present? - * When using the target, foil and set_size parameters, false means that the foil image will be repeated up to the set_size, - * and if true, then the target will be presented along with the foil image repeated up to set_size - 1. - * When using the stimuli parameter, this parameter is only used to determine the response accuracy. + * Is the target present? This parameter must always be specified. When using the `target`, `foil` and + * `set_size` parameters, `false` means that the foil image will be repeated up to the set_size, and + * `true` means that the target will be presented along with the foil image repeated up to set_size - 1. + * When using the `stimuli` parameter, this parameter is only used to determine the response accuracy. */ target_present: { type: ParameterType.BOOL, - pretty_name: "Target present", default: undefined, }, - /** Path to image file that is a fixation target. */ + /** + * Path to image file that is a fixation target. This parameter must always be specified. + */ fixation_image: { type: ParameterType.IMAGE, - pretty_name: "Fixation image", default: undefined, }, /** Two element array indicating the height and width of the search array element images. */ target_size: { type: ParameterType.INT, - pretty_name: "Target size", array: true, default: [50, 50], }, /** Two element array indicating the height and width of the fixation image. */ fixation_size: { type: ParameterType.INT, - pretty_name: "Fixation size", array: true, default: [16, 16], }, /** The diameter of the search array circle in pixels. */ circle_diameter: { type: ParameterType.INT, - pretty_name: "Circle diameter", default: 250, }, /** The key to press if the target is present in the search array. */ target_present_key: { type: ParameterType.KEY, - pretty_name: "Target present key", default: "j", }, /** The key to press if the target is not present in the search array. */ target_absent_key: { type: ParameterType.KEY, - pretty_name: "Target absent key", default: "f", }, - /** The maximum duration to wait for a response. */ + /** The maximum amount of time the participant is allowed to search before the trial will continue. A value + * of null will allow the participant to search indefinitely. + */ trial_duration: { type: ParameterType.INT, - pretty_name: "Trial duration", default: null, }, /** How long to show the fixation image for before the search array (in milliseconds). */ fixation_duration: { type: ParameterType.INT, - pretty_name: "Fixation duration", default: 1000, }, - /** Whether a keyboard response ends the trial early */ + /** If true, the trial will end when the participant makes a response. */ response_ends_trial: { type: ParameterType.BOOL, - pretty_name: "Response ends trial", default: true, }, }, + data: { + /** True if the participant gave the correct response. */ + correct: { + type: ParameterType.BOOL, + }, + /** Indicates which key the participant pressed. */ + response: { + type: ParameterType.STRING, + }, + /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */ + rt: { + type: ParameterType.INT, + }, + /** The number of items in the search array. */ + set_size: { + type: ParameterType.INT, + }, + /** True if the target is present in the search array. */ + target_present: { + type: ParameterType.BOOL, + }, + /** Array where each element is the pixel value of the center of an image in the search array. If the target is present, then the first element will represent the location of the target. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */ + locations: { + type: ParameterType.INT, + array: true, + }, + }, }; type Info = typeof info; /** - * **visual-search-circle** - * - * jsPsych plugin to display a set of objects, with or without a target, equidistant from fixation. - * Subject responds with key press to whether or not the target is present. - * Based on code written for psychtoolbox by Ben Motz. + * This plugin presents a customizable visual-search task modelled after [Wang, Cavanagh, & Green (1994)](http://dx.doi.org/10.3758/BF03206946). + * The participant indicates whether or not a target is present among a set of distractors. The stimuli are displayed in a circle, evenly-spaced, + * equidistant from a fixation point. Here is an example using normal and backward Ns: * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-visual-search-circle/ visual-search-circle plugin documentation on jspsych.org} + * @see {@link https://www.jspsych.org/latest/plugins/visual-search-circle/ visual-search-circle plugin documentation on jspsych.org} **/ class VisualSearchCirclePlugin implements JsPsychPlugin { static info = info; @@ -166,9 +201,6 @@ class VisualSearchCirclePlugin implements JsPsychPlugin { }; const end_trial = () => { - display_element.innerHTML = ""; - - this.jsPsych.pluginAPI.clearAllTimeouts(); this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); // data saving @@ -258,7 +290,7 @@ class VisualSearchCirclePlugin implements JsPsychPlugin { ]; } - private generateDisplayLocs(n_locs, trial) { + private generateDisplayLocs(n_locs: number, trial: TrialType) { // circle params var diam = trial.circle_diameter; // pixels var radi = diam / 2; @@ -281,7 +313,7 @@ class VisualSearchCirclePlugin implements JsPsychPlugin { return display_locs; } - private generatePresentationSet(trial) { + private generatePresentationSet(trial: TrialType) { var to_present = []; if (trial.target !== null && trial.foil !== null && trial.set_size !== null) { if (trial.target_present) { @@ -294,7 +326,7 @@ class VisualSearchCirclePlugin implements JsPsychPlugin { to_present.push(trial.foil); } } - } else if (trial.stimuli !== null) { + } else if (trial.stimuli.length > 0) { to_present = trial.stimuli; } else { console.error( diff --git a/packages/plugin-webgazer-calibrate/package.json b/packages/plugin-webgazer-calibrate/package.json index fff5959047..09c6885b4d 100644 --- a/packages/plugin-webgazer-calibrate/package.json +++ b/packages/plugin-webgazer-calibrate/package.json @@ -34,10 +34,12 @@ }, "homepage": "https://www.jspsych.org/latest/plugins/webgazer-calibrate", "peerDependencies": { - "jspsych": ">=7.0.0" + "jspsych": ">=7.0.0", + "@jspsych/extension-webgazer": ">=1.0.0" }, "devDependencies": { "@jspsych/config": "^2.0.0", + "@jspsych/extension-webgazer": "^1.0.2", "@jspsych/test-utils": "^1.1.2" } } diff --git a/packages/plugin-webgazer-calibrate/src/index.ts b/packages/plugin-webgazer-calibrate/src/index.ts index 28d6c8a619..1718a48b59 100644 --- a/packages/plugin-webgazer-calibrate/src/index.ts +++ b/packages/plugin-webgazer-calibrate/src/index.ts @@ -1,9 +1,13 @@ +import type WebGazerExtension from "@jspsych/extension-webgazer"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "webgazer-calibrate", + version: version, parameters: { - /** An array of calibration points, where each element is an array cointaining the coordinates for one calibration point: [x,y] */ + /** Array of points in `[x,y]` coordinates. Specified as a percentage of the screen width and height, from the left and top edge. The default grid is 9 points. */ calibration_points: { type: ParameterType.INT, // TO DO: nested array, so different type? default: [ @@ -19,51 +23,57 @@ const info = { ], array: true, }, - /** What should the subject do in response to the calibration point presentation? Options are 'click' and 'view'. */ + /** Can specify `click` to have participants click on calibration points or `view` to have participants passively watch calibration points. */ calibration_mode: { type: ParameterType.SELECT, options: ["click", "view"], default: "click", }, - /** Size of the calibration points, in pixels */ + /** Diameter of the calibration points in pixels. */ point_size: { type: ParameterType.INT, default: 20, }, - /** Number of repetitions per calibration point */ + /** The number of times to repeat the sequence of calibration points. */ repetitions_per_point: { type: ParameterType.INT, default: 1, }, - /** Whether or not to randomize the calibration point order */ + /** Whether to randomize the order of the calibration points. */ randomize_calibration_order: { type: ParameterType.BOOL, default: false, }, - /** If calibration_mode is view, then this is the delay before calibration after the point is shown */ + /** If `calibration_mode` is set to `view`, then this is the delay before calibrating after showing a point. + * Gives the participant time to fixate on the new target before assuming that the participant is looking at the target. */ time_to_saccade: { type: ParameterType.INT, default: 1000, }, - /** If calibration_mode is view, then this is the length of time to show the point while calibrating */ + /** + * If `calibration_mode` is set to `view`, then this is the length of time to show a point while calibrating. Note + * that if `click` calibration is used then the point will remain on the screen until clicked. + */ time_per_point: { type: ParameterType.INT, default: 1000, }, }, + data: { + // no data collected + }, }; type Info = typeof info; /** - * **webgazer-calibrate** * - * jsPsych plugin for calibrating webcam eye gaze location estimation. - * Intended for use with the WebGazer eye-tracking extension, after the webcam has been initialized with the `webgazer-init-camera` plugin. + * This plugin can be used to calibrate the [WebGazer extension](../extensions/webgazer.md). For a narrative + * description of eye tracking with jsPsych, see the [eye tracking overview](../overview/eye-tracking.md). * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-webgazer-calibrate/ webgazer-calibrate plugin} and - * {@link https://www.jspsych.org/overview/eye-tracking/ eye-tracking overview} documentation on jspsych.org + * @see {@link https://www.jspsych.org/latest/plugins/webgazer-calibrate/ webgazer-calibrate plugin} and + * {@link https://www.jspsych.org/latest/overview/eye-tracking/ eye-tracking overview} documentation on jspsych.org */ class WebgazerCalibratePlugin implements JsPsychPlugin { static info = info; @@ -71,6 +81,8 @@ class WebgazerCalibratePlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { + const extension = this.jsPsych.extensions.webgazer as WebGazerExtension; + var html = `
`; @@ -94,9 +106,9 @@ class WebgazerCalibratePlugin implements JsPsychPlugin { }; const calibrate = () => { - this.jsPsych.extensions["webgazer"].resume(); + extension.resume(); if (trial.calibration_mode == "click") { - this.jsPsych.extensions["webgazer"].startMouseCalibration(); + extension.startMouseCalibration(); } next_calibration_round(); }; @@ -139,7 +151,7 @@ class WebgazerCalibratePlugin implements JsPsychPlugin { const watch_dot = () => { if (performance.now() > pt_start_cal) { - this.jsPsych.extensions["webgazer"].calibratePoint(x, y, "click"); + extension.calibratePoint(x, y); } if (performance.now() < pt_finish) { requestAnimationFrame(watch_dot); @@ -154,7 +166,7 @@ class WebgazerCalibratePlugin implements JsPsychPlugin { const calibration_done = () => { if (trial.calibration_mode == "click") { - this.jsPsych.extensions["webgazer"].stopMouseCalibration(); + extension.stopMouseCalibration(); } wg_container.innerHTML = ""; end_trial(); @@ -162,19 +174,13 @@ class WebgazerCalibratePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - this.jsPsych.extensions["webgazer"].pause(); - this.jsPsych.extensions["webgazer"].hidePredictions(); - this.jsPsych.extensions["webgazer"].hideVideo(); - - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); + extension.pause(); + extension.hidePredictions(); + extension.hideVideo(); // gather the data to store for the trial var trial_data = {}; - // clear the display - display_element.innerHTML = ""; - // move on to the next trial this.jsPsych.finishTrial(trial_data); }; diff --git a/packages/plugin-webgazer-init-camera/package.json b/packages/plugin-webgazer-init-camera/package.json index 420c4d3292..9bfaf90205 100644 --- a/packages/plugin-webgazer-init-camera/package.json +++ b/packages/plugin-webgazer-init-camera/package.json @@ -34,10 +34,12 @@ }, "homepage": "https://www.jspsych.org/latest/plugins/webgazer-init-camera", "peerDependencies": { - "jspsych": ">=7.0.0" + "jspsych": ">=7.0.0", + "@jspsych/extension-webgazer": ">=1.0.0" }, "devDependencies": { "@jspsych/config": "^2.0.0", + "@jspsych/extension-webgazer": "^1.0.2", "@jspsych/test-utils": "^1.1.2" } } diff --git a/packages/plugin-webgazer-init-camera/src/index.ts b/packages/plugin-webgazer-init-camera/src/index.ts index 1560d1f70c..d9c0c56341 100644 --- a/packages/plugin-webgazer-init-camera/src/index.ts +++ b/packages/plugin-webgazer-init-camera/src/index.ts @@ -1,9 +1,13 @@ +import type WebGazerExtension from "@jspsych/extension-webgazer"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "webgazer-init-camera", + version: version, parameters: { - /** Instruction text */ + /** Instructions for the participant to follow. */ instructions: { type: ParameterType.HTML_STRING, default: ` @@ -12,25 +16,32 @@ const info = {

It is important that you try and keep your head reasonably still throughout the experiment, so please take a moment to adjust your setup to be comfortable.

When your face is centered in the box and the box is green, you can click to continue.

`, }, - /** Text for the button that participants click to end the trial. */ + /** The text for the button that participants click to end the trial. */ button_text: { type: ParameterType.STRING, default: "Continue", }, }, + data: { + /** 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. + */ + load_time: { + type: ParameterType.INT, + }, + }, }; type Info = typeof info; /** - * **webgazer-init-camera** - * - * jsPsych plugin for initializing the webcam and helping the participant center their face in the camera view. - * Intended for use with the WebGazer eye-tracking extension. + * This plugin initializes the camera and helps the participant center their face in the camera view for + * using the the [WebGazer extension](../extensions/webgazer.md). For a narrative description of eye + * tracking with jsPsych, see the [eye tracking overview](../overview/eye-tracking.md). * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-webgazer-init-camera/ webgazer-init-camera plugin} and - * {@link https://www.jspsych.org/overview/eye-tracking/ eye-tracking overview} documentation on jspsych.org + * @see {@link https://www.jspsych.org/latest/plugins/webgazer-init-camera/ webgazer-init-camera plugin} and + * {@link https://www.jspsych.org/latest/overview/eye-tracking/ eye-tracking overview} documentation on jspsych.org */ class WebgazerInitCameraPlugin implements JsPsychPlugin { static info = info; @@ -38,6 +49,8 @@ class WebgazerInitCameraPlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { + const extension = this.jsPsych.extensions.webgazer as WebGazerExtension; + let trial_complete; var start_time = performance.now(); @@ -45,20 +58,14 @@ class WebgazerInitCameraPlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - this.jsPsych.extensions["webgazer"].pause(); - this.jsPsych.extensions["webgazer"].hideVideo(); - - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); + extension.pause(); + extension.hideVideo(); // gather the data to store for the trial var trial_data = { load_time: load_time, }; - // clear the display - display_element.innerHTML = ""; - document.querySelector("#webgazer-center-style").remove(); // move on to the next trial @@ -85,8 +92,8 @@ class WebgazerInitCameraPlugin implements JsPsychPlugin { display_element.innerHTML = html; - this.jsPsych.extensions["webgazer"].showVideo(); - this.jsPsych.extensions["webgazer"].resume(); + extension.showVideo(); + extension.resume(); var wg_container = display_element.querySelector("#webgazer-init-container"); @@ -115,8 +122,8 @@ class WebgazerInitCameraPlugin implements JsPsychPlugin { }); }; - if (!this.jsPsych.extensions.webgazer.isInitialized()) { - this.jsPsych.extensions.webgazer + if (!extension.isInitialized()) { + extension .start() .then(() => { showTrial(); diff --git a/packages/plugin-webgazer-validate/package.json b/packages/plugin-webgazer-validate/package.json index 42f947c655..5004038b87 100644 --- a/packages/plugin-webgazer-validate/package.json +++ b/packages/plugin-webgazer-validate/package.json @@ -34,10 +34,12 @@ }, "homepage": "https://www.jspsych.org/latest/plugins/webgazer-validate", "peerDependencies": { - "jspsych": ">=7.0.0" + "jspsych": ">=7.0.0", + "@jspsych/extension-webgazer": ">=1.0.0" }, "devDependencies": { "@jspsych/config": "^2.0.0", + "@jspsych/extension-webgazer": "^1.0.2", "@jspsych/test-utils": "^1.1.2" } } diff --git a/packages/plugin-webgazer-validate/src/index.ts b/packages/plugin-webgazer-validate/src/index.ts index 66579025d3..78a1f26874 100644 --- a/packages/plugin-webgazer-validate/src/index.ts +++ b/packages/plugin-webgazer-validate/src/index.ts @@ -1,7 +1,11 @@ +import type WebGazerExtension from "@jspsych/extension-webgazer"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { version } from "../package.json"; + const info = { name: "webgazer-validate", + version: version, parameters: { /** Array of points in [x,y] coordinates */ validation_points: { @@ -59,20 +63,58 @@ const info = { default: false, }, }, + data: { + /** Raw gaze data for the trial. The array will contain a nested array for each validation point. Within each nested array will be a list of `{x,y,dx,dy}` values specifying the absolute x and y pixels, as well as the distance from the target for that gaze point. */ + raw_gaze: { + type: ParameterType.COMPLEX, + array: true, + nested: { + x: { + type: ParameterType.INT, + }, + y: { + type: ParameterType.INT, + }, + dx: { + type: ParameterType.INT, + }, + dy: { + type: ParameterType.INT, + }, + }, + }, + /** The percentage of samples within the `roi_radius` for each validation point. */ + percent_in_roi: { + type: ParameterType.FLOAT, + array: true, + }, + /** The average `x` and `y` distance from each validation point, plus the median distance `r` of the points from this average offset. */ + average_offset: { + type: ParameterType.FLOAT, + array: true, + }, + /** The average number of samples per second. Calculated by finding samples per second for each point and then averaging these estimates together. */ + samples_per_sec: { + type: ParameterType.FLOAT, + }, + /** The list of validation points, in the order that they appeared. */ + validation_points: { + type: ParameterType.INT, + array: true, + }, + }, }; type Info = typeof info; /** - * **webgazer-validate** - * - * jsPsych plugin for measuring the accuracy and precision of eye gaze predictions. - * Intended for use with the Webgazer eye-tracking extension, after the webcam has been initialized with the - * `webgazer-init-camera` plugin and calibrated with the `webgazer-calibrate` plugin. + * This plugin can be used to measure the accuracy and precision of gaze predictions made by the + * [WebGazer extension](../extensions/webgazer.md). For a narrative description of eye tracking with jsPsych, + * see the [eye tracking overview](../overview/eye-tracking.md). * * @author Josh de Leeuw - * @see {@link https://www.jspsych.org/plugins/jspsych-webgazer-validate/ webgazer-validate plugin} and - * {@link https://www.jspsych.org/overview/eye-tracking/ eye-tracking overview} documentation on jspsych.org + * @see {@link https://www.jspsych.org/latest/plugins/webgazer-validate/ webgazer-validate plugin} and + * {@link https://www.jspsych.org/latest/overview/eye-tracking/ eye-tracking overview} documentation on jspsych.org */ class WebgazerValidatePlugin implements JsPsychPlugin { static info = info; @@ -80,6 +122,8 @@ class WebgazerValidatePlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { + const extension = this.jsPsych.extensions.webgazer as WebGazerExtension; + var trial_data = {}; trial_data.raw_gaze = []; trial_data.percent_in_roi = []; @@ -100,13 +144,7 @@ class WebgazerValidatePlugin implements JsPsychPlugin { // function to end trial when it is time const end_trial = () => { - this.jsPsych.extensions.webgazer.stopSampleInterval(); - - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - - // clear the display - display_element.innerHTML = ""; + extension.stopSampleInterval(); // move on to the next trial this.jsPsych.finishTrial(trial_data); @@ -127,7 +165,7 @@ class WebgazerValidatePlugin implements JsPsychPlugin { var pt_data = []; - var cancelGazeUpdate = this.jsPsych.extensions["webgazer"].onGazeUpdate((prediction) => { + var cancelGazeUpdate = extension.onGazeUpdate((prediction) => { if (performance.now() > pt_start_val) { pt_data.push({ x: prediction.x, @@ -169,9 +207,9 @@ class WebgazerValidatePlugin implements JsPsychPlugin { } trial_data.validation_points = val_points; points_completed = -1; - //jsPsych.extensions['webgazer'].resume(); - this.jsPsych.extensions.webgazer.startSampleInterval(); - //jsPsych.extensions.webgazer.showPredictions(); + //extension.resume(); + extension.startSampleInterval(); + //extension.showPredictions(); next_validation_point(); }; @@ -200,13 +238,13 @@ class WebgazerValidatePlugin implements JsPsychPlugin { '