mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 11:10:54 +00:00
Merge pull request #2209 from jspsych/plugin-exclusions
New Plugin: Browser Check
This commit is contained in:
commit
2922bc5dad
5
.changeset/khaki-rice-retire.md
Normal file
5
.changeset/khaki-rice-retire.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jspsych/plugin-browser-check": major
|
||||||
|
---
|
||||||
|
|
||||||
|
Initial release of the browser-check plugin. The plugin can measure various features and properties of the participant's browser and optionally exclude participants from the study based on these features and properties.
|
5
.changeset/new-llamas-remember.md
Normal file
5
.changeset/new-llamas-remember.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"jspsych": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
`jsPsych.endExperiment()` has a new, optional second parameter for saving data. Passing in an object of key-value pairs will store the pairs in the data for the final trial of the experiment.
|
49
docs/demos/jspsych-browser-check-demo1.html
Normal file
49
docs/demos/jspsych-browser-check-demo1.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://unpkg.com/jspsych@7.0.0"></script>
|
||||||
|
<script src="https://unpkg.com/@jspsych/plugin-browser-check@1.0.0"></script>
|
||||||
|
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.0.0"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.0.0/css/jspsych.css">
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var jsPsych = initJsPsych();
|
||||||
|
|
||||||
|
var start = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: '',
|
||||||
|
choices: ['Run demo']
|
||||||
|
};
|
||||||
|
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychBrowserCheck
|
||||||
|
};
|
||||||
|
|
||||||
|
var show_data = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: function() {
|
||||||
|
var trial_data = jsPsych.data.getLastTrialData().values();
|
||||||
|
var trial_json = JSON.stringify(trial_data, null, 2);
|
||||||
|
return '<p style="margin-bottom:0px;"><strong>Trial data:</strong></p>'+
|
||||||
|
'<pre style="margin-top:0px;text-align:left;">'+trial_json+'</pre>';
|
||||||
|
},
|
||||||
|
choices: ['Repeat demo']
|
||||||
|
};
|
||||||
|
|
||||||
|
var trial_data_loop = {
|
||||||
|
timeline: [trial, show_data],
|
||||||
|
loop_function: function() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof jsPsych !== "undefined") {
|
||||||
|
jsPsych.run([start, trial_data_loop]);
|
||||||
|
} else {
|
||||||
|
document.body.innerHTML = '<div style="text-align:center; margin-top:50%; transform:translate(0,-50%);">You must be online to view the plugin demo.</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
53
docs/demos/jspsych-browser-check-demo2.html
Normal file
53
docs/demos/jspsych-browser-check-demo2.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://unpkg.com/jspsych@7.0.0"></script>
|
||||||
|
<script src="https://unpkg.com/@jspsych/plugin-browser-check@1.0.0"></script>
|
||||||
|
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.0.0"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.0.0/css/jspsych.css">
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var jsPsych = initJsPsych();
|
||||||
|
|
||||||
|
var start = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: '',
|
||||||
|
choices: ['Run demo']
|
||||||
|
};
|
||||||
|
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychBrowserCheck,
|
||||||
|
inclusion_function: (data) => {
|
||||||
|
return ['chrome', 'firefox'].contains(data.browser);
|
||||||
|
},
|
||||||
|
exclusion_message: `<p>You must use Chrome or Firefox to complete this experiment.</p>`
|
||||||
|
};
|
||||||
|
|
||||||
|
var show_data = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: function() {
|
||||||
|
var trial_data = jsPsych.data.getLastTrialData().values();
|
||||||
|
var trial_json = JSON.stringify(trial_data, null, 2);
|
||||||
|
return '<p style="margin-bottom:0px;"><strong>Trial data:</strong></p>'+
|
||||||
|
'<pre style="margin-top:0px;text-align:left;">'+trial_json+'</pre>';
|
||||||
|
},
|
||||||
|
choices: ['Repeat demo']
|
||||||
|
};
|
||||||
|
|
||||||
|
var trial_data_loop = {
|
||||||
|
timeline: [trial, show_data],
|
||||||
|
loop_function: function() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof jsPsych !== "undefined") {
|
||||||
|
jsPsych.run([start, trial_data_loop]);
|
||||||
|
} else {
|
||||||
|
document.body.innerHTML = '<div style="text-align:center; margin-top:50%; transform:translate(0,-50%);">You must be online to view the plugin demo.</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
51
docs/demos/jspsych-browser-check-demo3.html
Normal file
51
docs/demos/jspsych-browser-check-demo3.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://unpkg.com/jspsych@7.0.0"></script>
|
||||||
|
<script src="https://unpkg.com/@jspsych/plugin-browser-check@1.0.0"></script>
|
||||||
|
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.0.0"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.0.0/css/jspsych.css">
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var jsPsych = initJsPsych();
|
||||||
|
|
||||||
|
var start = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: '<p>If you want to test the interactive resize, make your browser window small before continuing.</p>',
|
||||||
|
choices: ['Run demo']
|
||||||
|
};
|
||||||
|
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychBrowserCheck,
|
||||||
|
minimum_width: 1000,
|
||||||
|
minimum_height: 600
|
||||||
|
};
|
||||||
|
|
||||||
|
var show_data = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: function() {
|
||||||
|
var trial_data = jsPsych.data.getLastTrialData().values();
|
||||||
|
var trial_json = JSON.stringify(trial_data, null, 2);
|
||||||
|
return '<p style="margin-bottom:0px;"><strong>Trial data:</strong></p>'+
|
||||||
|
'<pre style="margin-top:0px;text-align:left;">'+trial_json+'</pre>';
|
||||||
|
},
|
||||||
|
choices: ['Repeat demo']
|
||||||
|
};
|
||||||
|
|
||||||
|
var trial_data_loop = {
|
||||||
|
timeline: [trial, show_data],
|
||||||
|
loop_function: function() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof jsPsych !== "undefined") {
|
||||||
|
jsPsych.run([start, trial_data_loop]);
|
||||||
|
} else {
|
||||||
|
document.body.innerHTML = '<div style="text-align:center; margin-top:50%; transform:translate(0,-50%);">You must be online to view the plugin demo.</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
59
docs/demos/jspsych-browser-check-demo4.html
Normal file
59
docs/demos/jspsych-browser-check-demo4.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://unpkg.com/jspsych@7.0.0"></script>
|
||||||
|
<script src="https://unpkg.com/@jspsych/plugin-browser-check@1.0.0"></script>
|
||||||
|
<script src="https://unpkg.com/@jspsych/plugin-html-button-response@1.0.0"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jspsych@7.0.0/css/jspsych.css">
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var jsPsych = initJsPsych();
|
||||||
|
|
||||||
|
var start = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: '',
|
||||||
|
choices: ['Run demo']
|
||||||
|
};
|
||||||
|
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychBrowserCheck,
|
||||||
|
inclusion_function: (data) => {
|
||||||
|
return data.browser == 'chrome' && data.mobile === false
|
||||||
|
},
|
||||||
|
exclusion_message: (data) => {
|
||||||
|
if(data.mobile){
|
||||||
|
return '<p>You must use a desktop/laptop computer to participate in this experiment.</p>';
|
||||||
|
} else if(data.browser !== 'chrome'){
|
||||||
|
return '<p>You must use Chrome as your browser to complete this experiment.</p>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var show_data = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: function() {
|
||||||
|
var trial_data = jsPsych.data.getLastTrialData().values();
|
||||||
|
var trial_json = JSON.stringify(trial_data, null, 2);
|
||||||
|
return '<p style="margin-bottom:0px;"><strong>Trial data:</strong></p>'+
|
||||||
|
'<pre style="margin-top:0px;text-align:left;">'+trial_json+'</pre>';
|
||||||
|
},
|
||||||
|
choices: ['Repeat demo']
|
||||||
|
};
|
||||||
|
|
||||||
|
var trial_data_loop = {
|
||||||
|
timeline: [trial, show_data],
|
||||||
|
loop_function: function() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof jsPsych !== "undefined") {
|
||||||
|
jsPsych.run([start, trial_data_loop]);
|
||||||
|
} else {
|
||||||
|
document.body.innerHTML = '<div style="text-align:center; margin-top:50%; transform:translate(0,-50%);">You must be online to view the plugin demo.</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
@ -1,30 +1,12 @@
|
|||||||
# Exclude Participants Based on Browser Features
|
# Exclude Participants Based on Browser Features
|
||||||
|
*Changed in 7.1*
|
||||||
|
|
||||||
Online subjects will use many different kinds of browsers. Depending on the experiment, it may be important to specify a minimum feature set of the browser. jsPsych makes this straightforward. Simply specify certain exclusion criteria in the `initJsPsych` method call. If a subject's browser doesn't meet the criteria the experiment will not start and the subject will see a message explaining the problem. For size restrictions the subject will see a message that displays the current size of their browser window and the minimum size needed to start the experiment, giving the subject an opportunity to enlarge the browser window to continue.
|
Online subjects will use many different kinds of browsers.
|
||||||
|
Depending on the experiment, it may be important to specify a minimum feature set of the browser.
|
||||||
|
|
||||||
Current exclusion options:
|
As of v7.1 of jsPsych, the recommended way to do this is using the [browser-check plugin](../plugins/browser-check.md).
|
||||||
* Minimum browser width & height
|
This plugin can record many features of the subject's browser and exclude subjects who do not meet a defined set of inclusion criteria.
|
||||||
* Support for the WebAudio API
|
Please see the [browser-check plugin documentation](../plugins/browser-check.md) for more details.
|
||||||
|
|
||||||
## Examples
|
The prior approach of using the `exclusions` parameter in `initJsPsych()` is deprecated and will be removed in `v8.0`.
|
||||||
|
You can find the documentation for it in the [7.0 docs](https://www.jspsych.org/7.0/overview/exclude-browser).
|
||||||
#### Exclude browsers that are not at least 800x600 pixels
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
initJsPsych({
|
|
||||||
exclusions: {
|
|
||||||
min_width: 800,
|
|
||||||
min_height: 600
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Exclude browsers that do not have access to the WebAudio API
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
initJsPsych({
|
|
||||||
exclusions: {
|
|
||||||
audio: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
142
docs/plugins/browser-check.md
Normal file
142
docs/plugins/browser-check.md
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
In addition to the [parameters available in all plugins](../overview/plugins.md#parameters-available-in-all-plugins), this plugin accepts the following parameters. Parameters with a default value of *undefined* must be specified. Other parameters can be left unspecified if the default value is acceptable.
|
||||||
|
|
||||||
|
| Parameter | Type | Default Value | Description |
|
||||||
|
| ------------------------------ | ---------------- | ------------- | ---------------------------------------- |
|
||||||
|
| features | array of strings | `["width", "height", "webaudio", "browser", "browser_version", "mobile", "os", "fullscreen", "vsync_rate", "webcam", "microphone"]` | The list of browser features to record. The default value includes all of the available options. |
|
||||||
|
| skip_features | array of strings | `[]` | Any features listed here will be skipped, even if they appear in `features`. Use this when you want to run most of the defaults.
|
||||||
|
| vsync_frame_count | int | 60 | 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.
|
||||||
|
| allow_window_resize | bool | true | 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`.
|
||||||
|
| minimum_height | int | 0 | If `allow_window_resize` is `true`, then this is the minimum height of the window (in pixels) that must be met before continuing.
|
||||||
|
| minimum_width | int | 0 | If `allow_window_resize` is `true`, then this is the minimum width of the window (in pixels) that must be met before continuing.
|
||||||
|
| window_resize_message | string | see description | 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: `<p>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.</p><p>The minimum window width is <span id="browser-check-min-width"></span> px.</p><p>Your current window width is <span id="browser-check-actual-width"></span> px.</p><p>The minimum window height is <span id="browser-check-min-height"></span> px.</p><p>Your current window height is <span id="browser-check-actual-height"></span> px.</p>`.
|
||||||
|
resize_fail_button_text | string | `"I cannot make the window any larger"` | During the interactive resize, a button with this text will be displayed below the `window_resize_message` for the participant to click if the window cannot meet the minimum size needed. When the button is clicked, the experiment will end and `exclusion_message` will be displayed.
|
||||||
|
inclusion_function | function | `() => { return true; }` | A function that evaluates to `true` if the browser meets all of the inclusion criteria for the experiment, and `false` otherwise. 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 | function | `() => { return <p>Your browser does not meet the requirements to participate in this experiment.</p> }` | 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.
|
||||||
|
|
||||||
|
## Data Generated
|
||||||
|
|
||||||
|
In addition to the [default data collected by all plugins](../overview/plugins.md#data-collected-by-all-plugins), this plugin collects the following data for each trial.
|
||||||
|
|
||||||
|
| Name | Type | Value |
|
||||||
|
| ------------ | ------- | ---------------------------------------- |
|
||||||
|
| width | int | The width of the browser window in pixels. If interactive resizing happens, this is the width *after* resizing.
|
||||||
|
| height | int | The height of the browser window in pixels. If interactive resizing happens, this is the height *after* resizing.
|
||||||
|
| browser | string | The browser used.
|
||||||
|
| browser_version | string | The version number of the browser.
|
||||||
|
| os | string | The operating system used.
|
||||||
|
| mobile | bool | Whether the browser is a mobile device.
|
||||||
|
| webaudio | bool | Whether the browser supports the WebAudio API.
|
||||||
|
| fullscreen | bool | Whether the browser supports the Fullscreen API.
|
||||||
|
| vsync_rate | number | An estimate of the refresh rate of the screen, in frames per second.
|
||||||
|
| webcam | bool | Whether there is a webcam device available. Note that the participant still must grant permission to access the device before it can be used.
|
||||||
|
| microphone | 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.
|
||||||
|
|
||||||
|
Note that all of these values are only recorded when the corresponding key is included in the `features` parameter for the trial.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
???+ example "Recording all of the available features, no exclusions"
|
||||||
|
=== "Code"
|
||||||
|
```javascript
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychBrowserCheck
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Demo"
|
||||||
|
<div style="text-align:center;">
|
||||||
|
<iframe src="../../demos/jspsych-browser-check-demo1.html" width="90%;" height="500px;" frameBorder="0"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="../../demos/jspsych-browser-check-demo1.html">Open demo in new tab</a>
|
||||||
|
|
||||||
|
???+ example "Using the inclusion function to mandate the use of Chrome or Firefox as the browser"
|
||||||
|
=== "Code"
|
||||||
|
```javascript
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychBrowserCheck,
|
||||||
|
inclusion_function: (data) => {
|
||||||
|
return ['chrome', 'firefox'].contains(data.browser);
|
||||||
|
},
|
||||||
|
exclusion_message: `<p>You must use Chrome or Firefox to complete this experiment.</p>`
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Demo"
|
||||||
|
<div style="text-align:center;">
|
||||||
|
<iframe src="../../demos/jspsych-browser-check-demo2.html" width="90%;" height="500px;" frameBorder="0"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="../../demos/jspsych-browser-check-demo2.html">Open demo in new tab</a>
|
||||||
|
|
||||||
|
???+ example "Setting a minimum window height & width, with the option to resize the window"
|
||||||
|
=== "Code"
|
||||||
|
```javascript
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychBrowserCheck,
|
||||||
|
minimum_width: 1000,
|
||||||
|
minimum_height: 600
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Demo"
|
||||||
|
<div style="text-align:center;">
|
||||||
|
<p>This demo only works in a resizable window. Please <a target="_blank" rel="noopener noreferrer" href="../../demos/jspsych-browser-check-demo3.html"> open it in new tab</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="../../demos/jspsych-browser-check-demo3.html">Open demo in new tab</a>
|
||||||
|
|
||||||
|
???+ example "Custom exclusion message based on measured features"
|
||||||
|
=== "Code"
|
||||||
|
```javascript
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychBrowserCheck,
|
||||||
|
inclusion_function: (data) => {
|
||||||
|
return data.browser == 'chrome' && data.mobile === false
|
||||||
|
},
|
||||||
|
exclusion_message: (data) => {
|
||||||
|
if(data.mobile){
|
||||||
|
return '<p>You must use a desktop/laptop computer to participate in this experiment.</p>';
|
||||||
|
} else if(data.browser !== 'chrome'){
|
||||||
|
return '<p>You must use Chrome as your browser to complete this experiment.</p>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Demo"
|
||||||
|
<div style="text-align:center;">
|
||||||
|
<iframe src="../../demos/jspsych-browser-check-demo4.html" width="90%;" height="500px;" frameBorder="0"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="../../demos/jspsych-browser-check-demo4.html">Open demo in new tab</a>
|
@ -12,6 +12,7 @@ Plugin | Description
|
|||||||
[audio‑button‑response](audio-button-response.md) | Play an audio file and allow the subject to respond by choosing a button to click. The button can be customized extensively, e.g., using images in place of standard buttons.
|
[audio‑button‑response](audio-button-response.md) | Play an audio file and allow the subject to respond by choosing a button to click. The button can be customized extensively, e.g., using images in place of standard buttons.
|
||||||
[audio‑keyboard‑response](audio-keyboard-response.md) | Play an audio file and allow the subject to respond by pressing a key.
|
[audio‑keyboard‑response](audio-keyboard-response.md) | Play an audio file and allow the subject to respond by pressing a key.
|
||||||
[audio‑slider‑response](audio-slider-response.md) | Play an audio file and allow the subject to respond by moving a slider to indicate a value.
|
[audio‑slider‑response](audio-slider-response.md) | Play an audio file and allow the subject to respond by moving a slider to indicate a value.
|
||||||
|
[browser‑check](browser-check.md) | Measures various features of the participant's browser and runs an inclusion check to see if the browser meets a custom set of criteria for running the study.
|
||||||
[call‑function](call-function.md) | Executes an arbitrary function call. Doesn't display anything to the subject, and the subject is usually unaware that this plugin has even executed. It's useful for performing tasks at specified times in the experiment, such as saving data.
|
[call‑function](call-function.md) | Executes an arbitrary function call. Doesn't display anything to the subject, and the subject is usually unaware that this plugin has even executed. It's useful for performing tasks at specified times in the experiment, such as saving data.
|
||||||
[canvas‑button‑response](canvas-button-response.md) | Draw a stimulus on a [HTML canvas element](https://www.w3schools.com/html/html5_canvas.asp), and record a button click response. Useful for displaying dynamic, parametrically-defined graphics, and for controlling the positioning of multiple graphical elements (shapes, text, images).
|
[canvas‑button‑response](canvas-button-response.md) | Draw a stimulus on a [HTML canvas element](https://www.w3schools.com/html/html5_canvas.asp), and record a button click response. Useful for displaying dynamic, parametrically-defined graphics, and for controlling the positioning of multiple graphical elements (shapes, text, images).
|
||||||
[canvas‑keyboard‑response](canvas-keyboard-response) | Draw a stimulus on a [HTML canvas element](https://www.w3schools.com/html/html5_canvas.asp), and record a key press response. Useful for displaying dynamic, parametrically-defined graphics, and for controlling the positioning of multiple graphical elements (shapes, text, images).
|
[canvas‑keyboard‑response](canvas-keyboard-response) | Draw a stimulus on a [HTML canvas element](https://www.w3schools.com/html/html5_canvas.asp), and record a key press response. Useful for displaying dynamic, parametrically-defined graphics, and for controlling the positioning of multiple graphical elements (shapes, text, images).
|
||||||
|
@ -24,7 +24,7 @@ The settings object can contain several parameters. None of the parameters are r
|
|||||||
| on_data_update | function | Function to execute every time data is stored using the `jsPsych.data.write` method. All plugins use this method to save data (via a call to `jsPsych.finishTrial`, so this function runs every time a plugin stores new data. |
|
| on_data_update | function | Function to execute every time data is stored using the `jsPsych.data.write` method. All plugins use this method to save data (via a call to `jsPsych.finishTrial`, so this function runs every time a plugin stores new data. |
|
||||||
| on_interaction_data_update | function | Function to execute every time a new interaction event occurs. Interaction events include clicking on a different window (blur), returning to the experiment window (focus), entering full screen mode (fullscreenenter), and exiting full screen mode (fullscreenexit). |
|
| on_interaction_data_update | function | Function to execute every time a new interaction event occurs. Interaction events include clicking on a different window (blur), returning to the experiment window (focus), entering full screen mode (fullscreenenter), and exiting full screen mode (fullscreenexit). |
|
||||||
| on_close | function | Function to execute when the user leaves the page. Can be used, for example, to save data before the page is closed. |
|
| on_close | function | Function to execute when the user leaves the page. Can be used, for example, to save data before the page is closed. |
|
||||||
| exclusions | object | Specifies restrictions on the browser the subject can use to complete the experiment. See list of options below. |
|
| exclusions | object | Specifies restrictions on the browser the subject can use to complete the experiment. See list of options below. *This feature is deprecated as of v7.1 and will be removed in v8.0. The [browser-check plugin](../plugins/browser-check.md) is an improved way to handle exclusions.* |
|
||||||
| show_progress_bar | boolean | If `true`, then [a progress bar](../overview/progress-bar.md) is shown at the top of the page. Default is `false`. |
|
| show_progress_bar | boolean | If `true`, then [a progress bar](../overview/progress-bar.md) is shown at the top of the page. Default is `false`. |
|
||||||
| message_progress_bar | string | Message to display next to the progress bar. The default is 'Completion Progress'. |
|
| message_progress_bar | string | Message to display next to the progress bar. The default is 'Completion Progress'. |
|
||||||
| auto_update_progress_bar | boolean | If true, then the progress bar at the top of the page will automatically update as every top-level timeline or trial is completed. |
|
| auto_update_progress_bar | boolean | If true, then the progress bar at the top of the page will automatically update as every top-level timeline or trial is completed. |
|
||||||
@ -169,14 +169,15 @@ jsPsych.run([block, after_block]);
|
|||||||
## jsPsych.endExperiment
|
## jsPsych.endExperiment
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
jsPsych.endExperiment(end_message)
|
jsPsych.endExperiment(end_message, data)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parameters
|
### Parameters
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| ----------- | ------ | ---------------------------------------- |
|
| ----------- | ------ | ---------------------------------------- |
|
||||||
| end_message | string | A message to display on the screen after the experiment is over. |
|
| end_message | string | A message to display on the screen after the experiment is over. Can include HTML formatting. |
|
||||||
|
| data | object | An optional object of key-value pairs to store as data in the final trial of the experiment.
|
||||||
|
|
||||||
### Return value
|
### Return value
|
||||||
|
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script src="../packages/jspsych/dist/index.browser.js"></script>
|
|
||||||
<script src="../packages/plugin-html-keyboard-response/dist/index.browser.js"></script>
|
|
||||||
<link rel="stylesheet" href="../packages/jspsych/css/jspsych.css">
|
|
||||||
</head>
|
|
||||||
<body></body>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
var jsPsych = initJsPsych({
|
|
||||||
on_finish: function() {
|
|
||||||
jsPsych.data.displayData();
|
|
||||||
},
|
|
||||||
exclusions: {
|
|
||||||
min_width: 1000,
|
|
||||||
min_height: 500,
|
|
||||||
audio: true
|
|
||||||
},
|
|
||||||
default_iti: 250
|
|
||||||
});
|
|
||||||
|
|
||||||
var trial_1 = {
|
|
||||||
type: jsPsychHtmlKeyboardResponse,
|
|
||||||
stimulus: '<p>The experiment started because your viewing window is at least 1000x500 pixels and you are running a browser that supports the WebAudio API.</p><p>To see the exclusions in action, shrink this window and refresh the page.</p>',
|
|
||||||
};
|
|
||||||
|
|
||||||
jsPsych.run([trial_1]);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</html>
|
|
57
examples/jspsych-browser-check.html
Normal file
57
examples/jspsych-browser-check.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="../packages/jspsych/dist/index.browser.js"></script>
|
||||||
|
<script src="../packages/plugin-browser-check/dist/index.browser.js"></script>
|
||||||
|
<script src="../packages/plugin-html-button-response/dist/index.browser.js"></script>
|
||||||
|
<link rel="stylesheet" href="../packages/jspsych/css/jspsych.css">
|
||||||
|
<style>
|
||||||
|
img {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var jsPsych = initJsPsych();
|
||||||
|
|
||||||
|
var instructions_trial = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: `<p>The next trial will be the browser-check plugin.</p>
|
||||||
|
<p>The minimum width is set to 1000px and the minimum height to 600px.</p>
|
||||||
|
<p>Make your window too small now if you want to test the minimum size features.</p>`,
|
||||||
|
choices: ['Continue']
|
||||||
|
}
|
||||||
|
|
||||||
|
var browser_check_trial = {
|
||||||
|
type: jsPsychBrowserCheck,
|
||||||
|
minimum_width: 1000,
|
||||||
|
minimum_height: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
var results_trial = {
|
||||||
|
type: jsPsychHtmlButtonResponse,
|
||||||
|
stimulus: function(){
|
||||||
|
var browser_check_data = jsPsych.data.get().last(1).values()[0];
|
||||||
|
var html = `<div style="width:700px; text-align: left">
|
||||||
|
<p>Browser check results</p>
|
||||||
|
<p>Window size: ${browser_check_data.width}px x ${browser_check_data.height}px.</p>
|
||||||
|
<p>Browser: ${browser_check_data.browser} version ${browser_check_data.browser_version}</p>
|
||||||
|
<p>OS: ${browser_check_data.os}</p>
|
||||||
|
<p>Mobile device: ${browser_check_data.mobile}</p>
|
||||||
|
<p>Frame rate: ${browser_check_data.vsync_rate}</p>
|
||||||
|
<p>WebAudio API support: ${browser_check_data.webaudio}</p>
|
||||||
|
<p>Fullscreen API support: ${browser_check_data.fullscreen}</p>
|
||||||
|
<p>Webcam support: ${browser_check_data.webcam}</p>
|
||||||
|
<p>Microphone support: ${browser_check_data.microphone}</p>
|
||||||
|
`
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
choices: ['Done']
|
||||||
|
}
|
||||||
|
|
||||||
|
jsPsych.run([instructions_trial, browser_check_trial, results_trial]);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</html>
|
@ -74,6 +74,7 @@ nav:
|
|||||||
- 'audio-button-response': 'plugins/audio-button-response.md'
|
- 'audio-button-response': 'plugins/audio-button-response.md'
|
||||||
- 'audio-keyboard-response': 'plugins/audio-keyboard-response.md'
|
- 'audio-keyboard-response': 'plugins/audio-keyboard-response.md'
|
||||||
- 'audio-slider-response': 'plugins/audio-slider-response.md'
|
- 'audio-slider-response': 'plugins/audio-slider-response.md'
|
||||||
|
- 'browser-check': 'plugins/browser-check.md'
|
||||||
- 'call-function': 'plugins/call-function.md'
|
- 'call-function': 'plugins/call-function.md'
|
||||||
- 'canvas-button-response': 'plugins/canvas-button-response.md'
|
- 'canvas-button-response': 'plugins/canvas-button-response.md'
|
||||||
- 'canvas-keyboard-response': 'plugins/canvas-keyboard-response.md'
|
- 'canvas-keyboard-response': 'plugins/canvas-keyboard-response.md'
|
||||||
|
37
package-lock.json
generated
37
package-lock.json
generated
@ -2454,6 +2454,10 @@
|
|||||||
"resolved": "packages/plugin-audio-slider-response",
|
"resolved": "packages/plugin-audio-slider-response",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@jspsych/plugin-browser-check": {
|
||||||
|
"resolved": "packages/plugin-browser-check",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@jspsych/plugin-call-function": {
|
"node_modules/@jspsych/plugin-call-function": {
|
||||||
"resolved": "packages/plugin-call-function",
|
"resolved": "packages/plugin-call-function",
|
||||||
"link": true
|
"link": true
|
||||||
@ -5182,6 +5186,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-browser": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-eAcRiEPTs7utXWPaAgu/OX1HRJpxW7xSHpw4LTDrGFaeWnJ37HRlqpUkKsDm0AoTbtrvHQhH+5U2Cd87EGhJTg=="
|
||||||
|
},
|
||||||
"node_modules/detect-file": {
|
"node_modules/detect-file": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
|
||||||
@ -14631,6 +14640,21 @@
|
|||||||
"jspsych": ">=7.0.0"
|
"jspsych": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/plugin-browser-check": {
|
||||||
|
"name": "@jspsych/plugin-browser-check",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-browser": "^5.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@jspsych/config": "^1.0.0",
|
||||||
|
"@jspsych/test-utils": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"jspsych": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/plugin-call-function": {
|
"packages/plugin-call-function": {
|
||||||
"name": "@jspsych/plugin-call-function",
|
"name": "@jspsych/plugin-call-function",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -16981,6 +17005,14 @@
|
|||||||
"@jspsych/test-utils": "^1.0.0"
|
"@jspsych/test-utils": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@jspsych/plugin-browser-check": {
|
||||||
|
"version": "file:packages/plugin-browser-check",
|
||||||
|
"requires": {
|
||||||
|
"@jspsych/config": "^1.0.0",
|
||||||
|
"@jspsych/test-utils": "^1.0.0",
|
||||||
|
"detect-browser": "^5.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@jspsych/plugin-call-function": {
|
"@jspsych/plugin-call-function": {
|
||||||
"version": "file:packages/plugin-call-function",
|
"version": "file:packages/plugin-call-function",
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -19356,6 +19388,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
||||||
},
|
},
|
||||||
|
"detect-browser": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-eAcRiEPTs7utXWPaAgu/OX1HRJpxW7xSHpw4LTDrGFaeWnJ37HRlqpUkKsDm0AoTbtrvHQhH+5U2Cd87EGhJTg=="
|
||||||
|
},
|
||||||
"detect-file": {
|
"detect-file": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
|
||||||
|
@ -300,12 +300,12 @@ export class JsPsych {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
endExperiment(end_message: string) {
|
endExperiment(end_message = "", data = {}) {
|
||||||
this.timeline.end_message = end_message;
|
this.timeline.end_message = end_message;
|
||||||
this.timeline.end();
|
this.timeline.end();
|
||||||
this.pluginAPI.cancelAllKeyboardResponses();
|
this.pluginAPI.cancelAllKeyboardResponses();
|
||||||
this.pluginAPI.clearAllTimeouts();
|
this.pluginAPI.clearAllTimeouts();
|
||||||
this.finishTrial();
|
this.finishTrial(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
endCurrentTimeline() {
|
endCurrentTimeline() {
|
||||||
@ -734,6 +734,11 @@ export class JsPsych {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkExclusions(exclusions) {
|
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
|
// MINIMUM SIZE
|
||||||
if (exclusions.min_width || exclusions.min_height) {
|
if (exclusions.min_width || exclusions.min_height) {
|
||||||
const mw = exclusions.min_width || 0;
|
const mw = exclusions.min_width || 0;
|
||||||
|
1
packages/plugin-browser-check/jest.config.cjs
Normal file
1
packages/plugin-browser-check/jest.config.cjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname);
|
46
packages/plugin-browser-check/package.json
Normal file
46
packages/plugin-browser-check/package.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "@jspsych/plugin-browser-check",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "jsPsych plugin for checking browser features",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.cjs",
|
||||||
|
"exports": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
},
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"unpkg": "dist/index.browser.min.js",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "npm test -- --watch",
|
||||||
|
"tsc": "tsc",
|
||||||
|
"build": "rollup --config",
|
||||||
|
"build:watch": "npm run build -- --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/jspsych/jsPsych.git",
|
||||||
|
"directory": "packages/plugin-html-keyboard-response"
|
||||||
|
},
|
||||||
|
"author": "Josh de Leeuw",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/jspsych/jsPsych/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://www.jspsych.org/latest/plugins/html-keyboard-response",
|
||||||
|
"peerDependencies": {
|
||||||
|
"jspsych": ">=7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@jspsych/config": "^1.0.0",
|
||||||
|
"@jspsych/test-utils": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"detect-browser": "^5.2.1"
|
||||||
|
}
|
||||||
|
}
|
3
packages/plugin-browser-check/rollup.config.mjs
Normal file
3
packages/plugin-browser-check/rollup.config.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { makeRollupConfig } from "@jspsych/config/rollup";
|
||||||
|
|
||||||
|
export default makeRollupConfig("jsPsychBrowserCheck");
|
213
packages/plugin-browser-check/src/index.spec.ts
Normal file
213
packages/plugin-browser-check/src/index.spec.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { clickTarget, pressKey, startTimeline } from "@jspsych/test-utils";
|
||||||
|
|
||||||
|
import browserCheck from ".";
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
describe("browser-check", () => {
|
||||||
|
test("contains data on window size", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, getData } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
skip_features: ["vsync_rate"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
|
||||||
|
expect(getData().values()[0].width).not.toBeUndefined();
|
||||||
|
expect(getData().values()[0].height).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("contains browser data from userAgent", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, getData } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
skip_features: ["vsync_rate"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
|
||||||
|
expect(getData().values()[0].browser).toBe("chrome");
|
||||||
|
expect(getData().values()[0].browser_version).toBe("18.0.1025");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("contains OS data", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, getData } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
skip_features: ["vsync_rate"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
|
||||||
|
expect(getData().values()[0].os).toBe("Android OS");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("exclusion message displayed if inclusion_function is false", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, getHTML } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
skip_features: ["vsync_rate"],
|
||||||
|
inclusion_function: (data) => {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
|
||||||
|
expect(getHTML()).toMatch(browserCheck.info.parameters.exclusion_message.default());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("inclusion_function gets data from checks", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, getHTML } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
skip_features: ["vsync_rate"],
|
||||||
|
inclusion_function: (data) => {
|
||||||
|
expect(data.browser).toBe("chrome");
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resize message is displayed when allowed", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, getHTML, displayElement } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
skip_features: ["vsync_rate"],
|
||||||
|
minimum_width: 1200,
|
||||||
|
minimum_height: 1000,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(getHTML()).toMatch("1200");
|
||||||
|
expect(getHTML()).toMatch("1000");
|
||||||
|
|
||||||
|
clickTarget(displayElement.querySelector("button"));
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can change button text on interactive resize", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, getHTML, displayElement } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
skip_features: ["vsync_rate"],
|
||||||
|
minimum_width: 1200,
|
||||||
|
minimum_height: 1000,
|
||||||
|
resize_fail_button_text: "foo",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(displayElement.querySelector("button").innerHTML).toMatch("foo");
|
||||||
|
|
||||||
|
clickTarget(displayElement.querySelector("button"));
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resizing to meet minimum vals will finish trial", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, expectRunning } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
skip_features: ["vsync_rate"],
|
||||||
|
minimum_width: 1200,
|
||||||
|
minimum_height: 1000,
|
||||||
|
resize_fail_button_text: "foo",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectRunning();
|
||||||
|
|
||||||
|
// @ts-ignore jsdom window innerWidth is settable
|
||||||
|
window.innerWidth = 2000;
|
||||||
|
// @ts-ignore jsdom window innerHeight is settable
|
||||||
|
window.innerHeight = 2000;
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("vsync rate", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(navigator, "userAgent", "get")
|
||||||
|
.mockReturnValue(
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { expectFinished, expectRunning, getData } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: browserCheck,
|
||||||
|
features: ["vsync_rate"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// this will simulate the requestAnimationFrame() calls that are needed for vsync_rate.
|
||||||
|
// each one will fake execute 16ms after the previous.
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
|
||||||
|
expect(getData().values()[0].vsync_rate).toBe(1000 / 16);
|
||||||
|
});
|
||||||
|
});
|
355
packages/plugin-browser-check/src/index.ts
Normal file
355
packages/plugin-browser-check/src/index.ts
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
import { detect } from "detect-browser";
|
||||||
|
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
|
||||||
|
|
||||||
|
const info = <const>{
|
||||||
|
name: "browser-check",
|
||||||
|
parameters: {
|
||||||
|
/**
|
||||||
|
* List of features to check and record in the data
|
||||||
|
*/
|
||||||
|
features: {
|
||||||
|
type: ParameterType.STRING,
|
||||||
|
array: true,
|
||||||
|
default: [
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"webaudio",
|
||||||
|
"browser",
|
||||||
|
"browser_version",
|
||||||
|
"mobile",
|
||||||
|
"os",
|
||||||
|
"fullscreen",
|
||||||
|
"vsync_rate",
|
||||||
|
"webcam",
|
||||||
|
"microphone",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Any features listed here will be skipped, even if they appear in `features`. Useful for
|
||||||
|
* when you want to run most of the defaults.
|
||||||
|
*/
|
||||||
|
skip_features: {
|
||||||
|
type: ParameterType.STRING,
|
||||||
|
array: true,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The number of animation frames to sample when calculating vsync_rate.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
minimum_height: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Message to display during interactive window resizing.
|
||||||
|
*/
|
||||||
|
window_resize_message: {
|
||||||
|
type: ParameterType.HTML_STRING,
|
||||||
|
default: `<p>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.</p>
|
||||||
|
<p>The minimum window width is <span id="browser-check-min-width"></span> px.</p>
|
||||||
|
<p>Your current window width is <span id="browser-check-actual-width"></span> px.</p>
|
||||||
|
<p>The minimum window height is <span id="browser-check-min-height"></span> px.</p>
|
||||||
|
<p>Your current window height is <span id="browser-check-actual-height"></span> px.</p>`,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* During the interactive resize, a button with this text will be displayed below the
|
||||||
|
* `window_resize_message` for the participant to click if the window cannot meet the
|
||||||
|
* minimum size needed. When the button is clicked, the experiment will end and
|
||||||
|
* `exclusion_message` will be displayed.
|
||||||
|
*/
|
||||||
|
resize_fail_button_text: {
|
||||||
|
type: ParameterType.STRING,
|
||||||
|
default: "I cannot make the window any larger",
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* A function that evaluates to `true` if the browser meets all of the inclusion criteria
|
||||||
|
* for the experiment, and `false` otherwise. 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`.
|
||||||
|
*/
|
||||||
|
inclusion_function: {
|
||||||
|
type: ParameterType.FUNCTION,
|
||||||
|
default: () => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The message to display if `inclusion_function` returns `false`
|
||||||
|
*/
|
||||||
|
exclusion_message: {
|
||||||
|
type: ParameterType.FUNCTION,
|
||||||
|
default: () => {
|
||||||
|
return `<p>Your browser does not meet the requirements to participate in this experiment.</p>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Info = typeof info;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **browser-check**
|
||||||
|
*
|
||||||
|
* jsPsych plugin for checking features of the browser and validating against a set of inclusion criteria.
|
||||||
|
*
|
||||||
|
* @author Josh de Leeuw
|
||||||
|
* @see {@link https://www.jspsych.org/plugins/jspsych-browser-check/ browser-check plugin documentation on jspsych.org}
|
||||||
|
*/
|
||||||
|
class BrowserCheckPlugin implements JsPsychPlugin<Info> {
|
||||||
|
static info = info;
|
||||||
|
private end_flag = false;
|
||||||
|
|
||||||
|
constructor(private jsPsych: JsPsych) {}
|
||||||
|
|
||||||
|
private delay(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
trial(display_element: HTMLElement, trial: TrialType<Info>) {
|
||||||
|
const featureCheckFunctionsMap = new Map<string, () => any>(
|
||||||
|
Object.entries({
|
||||||
|
width: () => {
|
||||||
|
return window.innerWidth;
|
||||||
|
},
|
||||||
|
height: () => {
|
||||||
|
return window.innerHeight;
|
||||||
|
},
|
||||||
|
webaudio: () => {
|
||||||
|
if (
|
||||||
|
window.AudioContext ||
|
||||||
|
// @ts-ignore because prefixed not in document type
|
||||||
|
window.webkitAudioContext ||
|
||||||
|
// @ts-ignore because prefixed not in document type
|
||||||
|
window.mozAudioContext ||
|
||||||
|
// @ts-ignore because prefixed not in document type
|
||||||
|
window.oAudioContext ||
|
||||||
|
// @ts-ignore because prefixed not in document type
|
||||||
|
window.msAudioContext
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
browser: () => {
|
||||||
|
return detect().name;
|
||||||
|
},
|
||||||
|
browser_version: () => {
|
||||||
|
return detect().version;
|
||||||
|
},
|
||||||
|
mobile: () => {
|
||||||
|
return /Mobi/i.test(window.navigator.userAgent);
|
||||||
|
},
|
||||||
|
os: () => {
|
||||||
|
return detect().os;
|
||||||
|
},
|
||||||
|
fullscreen: () => {
|
||||||
|
if (
|
||||||
|
document.exitFullscreen ||
|
||||||
|
// @ts-ignore because prefixed not in document type
|
||||||
|
document.webkitExitFullscreen ||
|
||||||
|
// @ts-ignore because prefixed not in document type
|
||||||
|
document.msExitFullscreen
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vsync_rate: () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let t0 = performance.now();
|
||||||
|
let deltas = [];
|
||||||
|
let framesToRun = trial.vsync_frame_count;
|
||||||
|
const finish = () => {
|
||||||
|
let sum = 0;
|
||||||
|
for (const v of deltas) {
|
||||||
|
sum += v;
|
||||||
|
}
|
||||||
|
const frame_rate = 1000.0 / (sum / deltas.length);
|
||||||
|
const frame_rate_two_sig_dig = Math.round(frame_rate * 100) / 100;
|
||||||
|
resolve(frame_rate_two_sig_dig);
|
||||||
|
};
|
||||||
|
const nextFrame = () => {
|
||||||
|
let t1 = performance.now();
|
||||||
|
deltas.push(t1 - t0);
|
||||||
|
t0 = t1;
|
||||||
|
framesToRun--;
|
||||||
|
if (framesToRun > 0) {
|
||||||
|
requestAnimationFrame(nextFrame);
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const start = () => {
|
||||||
|
t0 = performance.now();
|
||||||
|
requestAnimationFrame(nextFrame);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(start);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
webcam: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||||
|
const webcams = devices.filter((d) => {
|
||||||
|
return d.kind == "videoinput";
|
||||||
|
});
|
||||||
|
if (webcams.length > 0) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
microphone: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||||
|
const microphones = devices.filter((d) => {
|
||||||
|
return d.kind == "audioinput";
|
||||||
|
});
|
||||||
|
if (microphones.length > 0) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const feature_data = new Map<string, any>();
|
||||||
|
const feature_checks: Promise<void>[] = [];
|
||||||
|
const features_to_check = trial.features.filter((x) => !trial.skip_features.includes(x));
|
||||||
|
|
||||||
|
for (const feature of features_to_check) {
|
||||||
|
// this allows for feature check functions to be sync or async
|
||||||
|
feature_checks.push(Promise.resolve(featureCheckFunctionsMap.get(feature)()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.allSettled(feature_checks).then((results) => {
|
||||||
|
for (let i = 0; i < features_to_check.length; i++) {
|
||||||
|
if (results[i].status === "fulfilled") {
|
||||||
|
// @ts-expect-error because .value isn't recognized for some reason
|
||||||
|
feature_data.set(features_to_check[i], results[i].value);
|
||||||
|
} else {
|
||||||
|
feature_data.set(features_to_check[i], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inclusion_check();
|
||||||
|
});
|
||||||
|
|
||||||
|
const inclusion_check = async () => {
|
||||||
|
await check_allow_resize();
|
||||||
|
|
||||||
|
if (!this.end_flag && trial.inclusion_function(Object.fromEntries(feature_data))) {
|
||||||
|
end_trial();
|
||||||
|
} else {
|
||||||
|
end_experiment();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const check_allow_resize = async () => {
|
||||||
|
const w = feature_data.get("width");
|
||||||
|
const h = feature_data.get("height");
|
||||||
|
|
||||||
|
if (
|
||||||
|
trial.allow_window_resize &&
|
||||||
|
(w || h) &&
|
||||||
|
(trial.minimum_width > 0 || trial.minimum_height > 0)
|
||||||
|
) {
|
||||||
|
display_element.innerHTML =
|
||||||
|
trial.window_resize_message +
|
||||||
|
`<p><button id="browser-check-max-size-btn" class="jspsych-btn">${trial.resize_fail_button_text}</button></p>`;
|
||||||
|
|
||||||
|
display_element
|
||||||
|
.querySelector("#browser-check-max-size-btn")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
display_element.innerHTML = "";
|
||||||
|
this.end_flag = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const min_width_el = display_element.querySelector("#browser-check-min-width");
|
||||||
|
const min_height_el = display_element.querySelector("#browser-check-min-height");
|
||||||
|
const actual_height_el = display_element.querySelector("#browser-check-actual-height");
|
||||||
|
const actual_width_el = display_element.querySelector("#browser-check-actual-width");
|
||||||
|
|
||||||
|
while (
|
||||||
|
!this.end_flag &&
|
||||||
|
(window.innerWidth < trial.minimum_width || window.innerHeight < trial.minimum_height)
|
||||||
|
) {
|
||||||
|
if (min_width_el) {
|
||||||
|
min_width_el.innerHTML = trial.minimum_width.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min_height_el) {
|
||||||
|
min_height_el.innerHTML = trial.minimum_height.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actual_height_el) {
|
||||||
|
actual_height_el.innerHTML = window.innerHeight.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actual_width_el) {
|
||||||
|
actual_width_el.innerHTML = window.innerWidth.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.delay(100);
|
||||||
|
|
||||||
|
feature_data.set("width", window.innerWidth);
|
||||||
|
feature_data.set("height", window.innerHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const end_trial = () => {
|
||||||
|
display_element.innerHTML = "";
|
||||||
|
|
||||||
|
const trial_data = { ...Object.fromEntries(feature_data) };
|
||||||
|
|
||||||
|
this.jsPsych.finishTrial(trial_data);
|
||||||
|
};
|
||||||
|
|
||||||
|
var end_experiment = () => {
|
||||||
|
display_element.innerHTML = "";
|
||||||
|
|
||||||
|
const trial_data = { ...Object.fromEntries(feature_data) };
|
||||||
|
|
||||||
|
this.jsPsych.endExperiment(trial.exclusion_message(trial_data), trial_data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BrowserCheckPlugin;
|
7
packages/plugin-browser-check/tsconfig.json
Normal file
7
packages/plugin-browser-check/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "@jspsych/config/tsconfig.core.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "."
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user