From 6aa4e5c441c4cb334e2b9a754cf5693804c6f33d Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Wed, 13 Oct 2021 18:58:43 -0400 Subject: [PATCH 01/18] initial commit of mouse tracking plugin --- .../extension-mouse-tracking/jest.config.cjs | 1 + .../extension-mouse-tracking/package.json | 43 ++++++++ .../rollup.config.mjs | 3 + .../extension-mouse-tracking/src/index.ts | 104 ++++++++++++++++++ .../extension-mouse-tracking/tsconfig.json | 7 ++ 5 files changed, 158 insertions(+) create mode 100644 packages/extension-mouse-tracking/jest.config.cjs create mode 100644 packages/extension-mouse-tracking/package.json create mode 100644 packages/extension-mouse-tracking/rollup.config.mjs create mode 100644 packages/extension-mouse-tracking/src/index.ts create mode 100644 packages/extension-mouse-tracking/tsconfig.json diff --git a/packages/extension-mouse-tracking/jest.config.cjs b/packages/extension-mouse-tracking/jest.config.cjs new file mode 100644 index 00000000..6ac19d5c --- /dev/null +++ b/packages/extension-mouse-tracking/jest.config.cjs @@ -0,0 +1 @@ +module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname); diff --git a/packages/extension-mouse-tracking/package.json b/packages/extension-mouse-tracking/package.json new file mode 100644 index 00000000..0dd7bd0d --- /dev/null +++ b/packages/extension-mouse-tracking/package.json @@ -0,0 +1,43 @@ +{ + "name": "@jspsych/extension-mouse-tracking", + "version": "0.1.0", + "description": "jsPsych extension for mouse tracking", + "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 --passWithNoTests", + "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/extension-webgazer" + }, + "author": "Josh de Leeuw", + "license": "MIT", + "bugs": { + "url": "https://github.com/jspsych/jsPsych/issues" + }, + "homepage": "https://www.jspsych.org/latest/extensions/mouse-tracking", + "peerDependencies": { + "jspsych": ">=7.0.0" + }, + "devDependencies": { + "@jspsych/config": "^1.0.0", + "@jspsych/test-utils": "^1.0.0" + } +} diff --git a/packages/extension-mouse-tracking/rollup.config.mjs b/packages/extension-mouse-tracking/rollup.config.mjs new file mode 100644 index 00000000..eb265276 --- /dev/null +++ b/packages/extension-mouse-tracking/rollup.config.mjs @@ -0,0 +1,3 @@ +import { makeRollupConfig } from "@jspsych/config/rollup"; + +export default makeRollupConfig("jsPsychExtensionMouseTracking"); diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts new file mode 100644 index 00000000..b293131c --- /dev/null +++ b/packages/extension-mouse-tracking/src/index.ts @@ -0,0 +1,104 @@ +import { JsPsych, JsPsychExtension, JsPsychExtensionInfo } from "jspsych"; + +interface InitializeParameters { + /** + * The minimum time between mouse samples. If mouse events occur more rapidly than this limit, they will + * not be recorded. Use this if you want to keep the data files smaller and don't need high resolution + * tracking data. The default value of 0 means that all events will be recorded. + * @default 0 + */ + minimum_sample_time: number; +} + +interface OnStartParameters { + /** + * An array of string selectors. The selectors should identify one unique element on the page. + * The DOMRect of the element will be stored in the data. + */ + targets: Array; +} + +class MouseTrackingExtension implements JsPsychExtension { + static info: JsPsychExtensionInfo = { + name: "mouse-tracking", + }; + + constructor(private jsPsych: JsPsych) {} + + private domObserver: MutationObserver; + private currentTrialData: Array; + private currentTrialTargets: Map; + private currentTrialSelectors: Array; + private currentTrialStartTime: number; + private minimumSampleTime: number; + private lastSampleTime: number; + + initialize = ({ minimum_sample_time = 0 }: InitializeParameters): Promise => { + this.domObserver = new MutationObserver(this.mutationObserverCallback); + this.minimumSampleTime = minimum_sample_time; + + return new Promise((resolve, reject) => { + resolve(); + }); + }; + + on_start = (params: OnStartParameters): void => { + this.currentTrialData = []; + this.currentTrialTargets = new Map(); + this.currentTrialSelectors = params.targets; + this.lastSampleTime = null; + + this.domObserver.observe(this.jsPsych.getDisplayElement(), { childList: true }); + }; + + on_load = () => { + // set current trial start time + this.currentTrialStartTime = performance.now(); + + // start data collection + window.addEventListener("mousemove", this.mouseEventHandler); + }; + + on_finish = () => { + this.domObserver.disconnect(); + + window.removeEventListener("mousemove", this.mouseEventHandler); + + return { + mouse_tracking_data: this.currentTrialData, + mouse_tracking_targets: this.currentTrialTargets, + }; + }; + + private mouseEventHandler = (e) => { + const x = e.x; + const y = e.y; + + const event_time = performance.now(); + const t = Math.round(event_time - this.currentTrialStartTime); + + if ( + this.lastSampleTime === null || + event_time - this.lastSampleTime >= this.minimumSampleTime + ) { + this.lastSampleTime = event_time; + this.currentTrialData.push({ x, y, t }); + } + }; + + private mutationObserverCallback = (mutationsList, observer) => { + for (const selector of this.currentTrialSelectors) { + if (!this.currentTrialTargets[selector]) { + if (this.jsPsych.getDisplayElement().querySelector(selector)) { + var coords = this.jsPsych + .getDisplayElement() + .querySelector(selector) + .getBoundingClientRect(); + this.currentTrialTargets[selector] = coords; + } + } + } + }; +} + +export default MouseTrackingExtension; diff --git a/packages/extension-mouse-tracking/tsconfig.json b/packages/extension-mouse-tracking/tsconfig.json new file mode 100644 index 00000000..588f0448 --- /dev/null +++ b/packages/extension-mouse-tracking/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@jspsych/config/tsconfig.core.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src"] +} From 9eb0a0646ecfec18b85eca7ab1f43b08fb9ca785 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 12:16:40 -0400 Subject: [PATCH 02/18] add mouseMove utility to test-utils package --- .changeset/quick-mangos-vanish.md | 5 +++++ packages/test-utils/src/index.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .changeset/quick-mangos-vanish.md diff --git a/.changeset/quick-mangos-vanish.md b/.changeset/quick-mangos-vanish.md new file mode 100644 index 00000000..d3f40d75 --- /dev/null +++ b/.changeset/quick-mangos-vanish.md @@ -0,0 +1,5 @@ +--- +"@jspsych/test-utils": minor +--- + +Added mouseMove utility function to dispatch "mousemove" events with location relative to a target element. diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index d9ee0b53..7f5779a8 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -28,6 +28,24 @@ export function clickTarget(target: Element) { target.dispatchEvent(new MouseEvent("click", { bubbles: true })); } +/** + * Dispatch a `mousemove` event, with x and y defined relative to the container element. + * @param x The x location of the event, relative to the x location of `container`. + * @param y The y location of the event, relative to the y location of `container`. + * @param container The DOM element for relative location of the event. + */ +export function mouseMove(x: number, y: number, container: Element) { + const containerRect = container.getBoundingClientRect(); + + const eventInit = { + clientX: containerRect.x + x, + clientY: containerRect.y + y, + bubbles: true, + }; + + container.dispatchEvent(new MouseEvent("mousemove", eventInit)); +} + /** * https://github.com/facebook/jest/issues/2157#issuecomment-279171856 */ From 04a133b3c38c430c3acb08f06d300d95bd746990 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 12:17:23 -0400 Subject: [PATCH 03/18] add testing suite for mouse tracking extension, fix a few bugs --- package-lock.json | 22 +++++ .../extension-mouse-tracking/package.json | 2 +- .../src/index.spec.ts | 84 +++++++++++++++++++ .../extension-mouse-tracking/src/index.ts | 6 +- 4 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 packages/extension-mouse-tracking/src/index.spec.ts diff --git a/package-lock.json b/package-lock.json index 041b444e..40981803 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2430,6 +2430,10 @@ "resolved": "packages/config", "link": true }, + "node_modules/@jspsych/extension-mouse-tracking": { + "resolved": "packages/extension-mouse-tracking", + "link": true + }, "node_modules/@jspsych/extension-webgazer": { "resolved": "packages/extension-webgazer", "link": true @@ -14540,6 +14544,17 @@ "node": ">=4.2.0" } }, + "packages/extension-mouse-tracking": { + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@jspsych/config": "^1.0.0", + "@jspsych/test-utils": "^1.0.0" + }, + "peerDependencies": { + "jspsych": ">=7.0.0" + } + }, "packages/extension-webgazer": { "name": "@jspsych/extension-webgazer", "version": "1.0.0", @@ -16907,6 +16922,13 @@ } } }, + "@jspsych/extension-mouse-tracking": { + "version": "file:packages/extension-mouse-tracking", + "requires": { + "@jspsych/config": "^1.0.0", + "@jspsych/test-utils": "^1.0.0" + } + }, "@jspsych/extension-webgazer": { "version": "file:packages/extension-webgazer", "requires": { diff --git a/packages/extension-mouse-tracking/package.json b/packages/extension-mouse-tracking/package.json index 0dd7bd0d..ef1dd678 100644 --- a/packages/extension-mouse-tracking/package.json +++ b/packages/extension-mouse-tracking/package.json @@ -16,7 +16,7 @@ ], "source": "src/index.ts", "scripts": { - "test": "jest --passWithNoTests", + "test": "jest", "test:watch": "npm test -- --watch", "tsc": "tsc", "build": "rollup --config", diff --git a/packages/extension-mouse-tracking/src/index.spec.ts b/packages/extension-mouse-tracking/src/index.spec.ts new file mode 100644 index 00000000..dfedd0f5 --- /dev/null +++ b/packages/extension-mouse-tracking/src/index.spec.ts @@ -0,0 +1,84 @@ +import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response"; +import { clickTarget, mouseMove, pressKey, startTimeline } from "@jspsych/test-utils"; +import { initJsPsych } from "jspsych"; + +import MouseTrackingExtension from "."; + +jest.useFakeTimers(); + +describe("Mouse Tracking Extension", () => { + test("adds mouse move data to trial", async () => { + const jsPsych = initJsPsych({ + extensions: [{ type: MouseTrackingExtension }], + }); + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "
", + extensions: [{ type: MouseTrackingExtension }], + }, + ]; + + const { displayElement, getHTML, 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 expectFinished(); + + expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ + x: targetRect.x + 50, + y: targetRect.y + 50, + }); + expect(getData().values()[0].mouse_tracking_data[1]).toMatchObject({ + x: targetRect.x + 55, + y: targetRect.y + 50, + }); + expect(getData().values()[0].mouse_tracking_data[2]).toMatchObject({ + x: targetRect.x + 60, + y: targetRect.y + 50, + }); + }); + + test("records bounding rect of targets in data", async () => { + const jsPsych = initJsPsych({ + extensions: [{ type: MouseTrackingExtension }], + }); + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: ` +
+
+ `, + extensions: [ + { type: MouseTrackingExtension, params: { targets: ["#target", "#target2"] } }, + ], + }, + ]; + + const { displayElement, getHTML, getData, expectFinished } = await startTimeline( + timeline, + jsPsych + ); + + const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); + const target2Rect = displayElement.querySelector("target2").getBoundingClientRect(); + + pressKey("a"); + + await expectFinished(); + + expect(getData().values()[0].mouse_tracking_targets["#target"]).toEqual(targetRect); + }); +}); diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts index b293131c..550cdc6e 100644 --- a/packages/extension-mouse-tracking/src/index.ts +++ b/packages/extension-mouse-tracking/src/index.ts @@ -45,7 +45,7 @@ class MouseTrackingExtension implements JsPsychExtension { on_start = (params: OnStartParameters): void => { this.currentTrialData = []; this.currentTrialTargets = new Map(); - this.currentTrialSelectors = params.targets; + this.currentTrialSelectors = typeof params !== "undefined" ? params.targets : []; this.lastSampleTime = null; this.domObserver.observe(this.jsPsych.getDisplayElement(), { childList: true }); @@ -71,8 +71,8 @@ class MouseTrackingExtension implements JsPsychExtension { }; private mouseEventHandler = (e) => { - const x = e.x; - const y = e.y; + const x = e.clientX; + const y = e.clientY; const event_time = performance.now(); const t = Math.round(event_time - this.currentTrialStartTime); From 02a510f5d3a9c78cdf9af7d506ebdb04a8fbe78b Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 12:18:01 -0400 Subject: [PATCH 04/18] add to test case --- packages/extension-mouse-tracking/src/index.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/extension-mouse-tracking/src/index.spec.ts b/packages/extension-mouse-tracking/src/index.spec.ts index dfedd0f5..54ea8086 100644 --- a/packages/extension-mouse-tracking/src/index.spec.ts +++ b/packages/extension-mouse-tracking/src/index.spec.ts @@ -80,5 +80,6 @@ describe("Mouse Tracking Extension", () => { await expectFinished(); expect(getData().values()[0].mouse_tracking_targets["#target"]).toEqual(targetRect); + expect(getData().values()[0].mouse_tracking_targets["#target2"]).toEqual(target2Rect); }); }); From 554408cb462740fa83512c382c4750c4595c5c6a Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 12:18:47 -0400 Subject: [PATCH 05/18] fix bug in test file --- packages/extension-mouse-tracking/src/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension-mouse-tracking/src/index.spec.ts b/packages/extension-mouse-tracking/src/index.spec.ts index 54ea8086..b054b33e 100644 --- a/packages/extension-mouse-tracking/src/index.spec.ts +++ b/packages/extension-mouse-tracking/src/index.spec.ts @@ -73,7 +73,7 @@ describe("Mouse Tracking Extension", () => { ); const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); - const target2Rect = displayElement.querySelector("target2").getBoundingClientRect(); + const target2Rect = displayElement.querySelector("#target2").getBoundingClientRect(); pressKey("a"); From 78df74d1863f302da9bdc2c27f45c5972abe7467 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 13:58:52 -0400 Subject: [PATCH 06/18] update docs and add demo --- ...spsych-extension-mouse-tracking-demo1.html | 99 +++++++++++++++++ docs/extensions/list-of-extensions.md | 3 +- docs/extensions/mouse-tracking.md | 105 ++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 docs/demos/jspsych-extension-mouse-tracking-demo1.html create mode 100644 docs/extensions/mouse-tracking.md diff --git a/docs/demos/jspsych-extension-mouse-tracking-demo1.html b/docs/demos/jspsych-extension-mouse-tracking-demo1.html new file mode 100644 index 00000000..f424980c --- /dev/null +++ b/docs/demos/jspsych-extension-mouse-tracking-demo1.html @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + diff --git a/docs/extensions/list-of-extensions.md b/docs/extensions/list-of-extensions.md index 92cae8e9..3ad9b412 100644 --- a/docs/extensions/list-of-extensions.md +++ b/docs/extensions/list-of-extensions.md @@ -9,4 +9,5 @@ For an overview of what extensions are and how they work, see our [extensions ov Extension | Description ------ | ----------- -[jspsych‑ext‑webgazer](../extensions/webgazer.md) | Enables eye tracking using the [WebGazer](https://webgazer.cs.brown.edu/) library. \ No newline at end of file +[mouse‑tracking](../extensions/mouse-tracking.md) | Enables tracking of mouse movements and recording location of objects on screen. +[webgazer](../extensions/webgazer.md) | Enables eye tracking using the [WebGazer](https://webgazer.cs.brown.edu/) library. \ No newline at end of file diff --git a/docs/extensions/mouse-tracking.md b/docs/extensions/mouse-tracking.md new file mode 100644 index 00000000..4566374d --- /dev/null +++ b/docs/extensions/mouse-tracking.md @@ -0,0 +1,105 @@ +# mouse-tracking + +This extension supports mouse tracking. +Specifically, it can record the `x` `y` coordinates and time of [mousemove events](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event). +It also allows recording of the [bounding rectangle](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) of elements on the screen to support the calculation of mouse events relative to different elements. + +## Parameters + +### Initialization Parameters + +Initialization parameters can be set when calling `initJsPsych()` + +```js +initJsPsych({ + extensions: [ + {type: jsPsychExtensionMouseTracking, params: {...}} + ] +}) +``` + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ +minimum_sample_time | number | 0 | The minimum time between mouse samples. If mouse events occur more rapidly than this limit, they will not be recorded. Use this if you want to keep the data files smaller and don't need high resolution tracking data. The default value of 0 means that all events will be recorded. + +### Trial Parameters + +Trial parameters can be set when adding the extension to a trial object. + +```js +var trial = { + type: jsPsych..., + extensions: [ + {type: jsPsychExtensionWebgazer, params: {...}} + ] +} +``` + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ +targets | array | [] | A list of elements on the page that you would like to record the coordinates of for comparison with the WebGazer data. Each entry in the array should be a valid [CSS selector string](https://www.w3schools.com/cssref/css_selectors.asp) that identifies the element. The selector string should be valid for exactly one element on the page. If the selector is valid for more than one element then only the first matching element will be recorded. + +## Data Generated + +Name | Type | Value +-----|------|------ +mouse_tracking_data | array | An array of objects containing mouse movement data for the trial. Each object has an `x`, a `y`, and a `t` 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. +mouse_tracking_targets | object | 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. + +## Examples + +???+ example "Record mouse movement data and play it back" + === "Code" + ```javascript + var trial = { + type: jsPsychHtmlButtonResponse, + stimulus: '
', + choices: ['Done'], + prompt: "

Move your mouse around inside the square.

", + extensions: [ + {type: jsPsychExtensionMouseTracking, params: {targets: ['#target']}} + ], + data: { + task: 'draw' + } + }; + + var replay = { + type: jsPsychHtmlButtonResponse, + stimulus: '
', + choices: ['Done'], + prompt: "

Here's the recording of your mouse movements

", + on_load: function(){ + var mouseMovements = jsPsych.data.get().last(1).values()[0].mouse_tracking_data; + var targetRect = jsPsych.data.get().last(1).values()[0].mouse_tracking_targets['#target']; + + var startTime = performance.now(); + + function draw_frame() { + var timeElapsed = performance.now() - startTime; + var points = mouseMovements.filter((x) => x.t <= timeElapsed); + var html = ``; + for(var p of points){ + html += `
` + } + document.querySelector('#target').innerHTML = html; + if(points.length < mouseMovements.length) { + requestAnimationFrame(draw_frame); + } + } + + requestAnimationFrame(draw_frame); + + }, + data: { + task: 'replay' + } + } + ``` + + === "Demo" +
+ +
+ + Open demo in new tab \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 4a955bd4..0b9baf4e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -117,6 +117,7 @@ nav: - 'webgazer-validate': 'plugins/webgazer-validate.md' - Extensions: - 'List of Extensions': 'extensions/list-of-extensions.md' + - 'mouse-tracking': 'extensions/mouse-tracking.md' - 'webgazer': 'extensions/webgazer.md' - Developers: - 'Configuration': 'developers/configuration.md' From 2f84f5b04df5d2ed8ee2f1e5e241aac65f5fe018 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 13:59:01 -0400 Subject: [PATCH 07/18] add example --- .../jspsych-extension-mouse-tracking.html | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 examples/jspsych-extension-mouse-tracking.html diff --git a/examples/jspsych-extension-mouse-tracking.html b/examples/jspsych-extension-mouse-tracking.html new file mode 100644 index 00000000..5a0c7244 --- /dev/null +++ b/examples/jspsych-extension-mouse-tracking.html @@ -0,0 +1,98 @@ + + + + + + + + + + + + + From 3e2e3ac86782c8c551b92cc087221994197adfe4 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 14:03:36 -0400 Subject: [PATCH 08/18] add changeset for extension --- .changeset/ten-owls-talk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-owls-talk.md diff --git a/.changeset/ten-owls-talk.md b/.changeset/ten-owls-talk.md new file mode 100644 index 00000000..17aafab6 --- /dev/null +++ b/.changeset/ten-owls-talk.md @@ -0,0 +1,5 @@ +--- +"@jspsych/extension-mouse-tracking": major +--- + +Created an extension that enables mouse tracking. The extension records the coordinates and time of mouse movements, as well as optionally recording the coordinates of objects on the screen to enable mapping of mouse movements onto screen objects. From dac7be71f7e7fe63d69bc771659d06e078ea5dfa Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 15:10:41 -0400 Subject: [PATCH 09/18] add mouseDown and mouseUp events to test utils --- .changeset/quick-mangos-vanish.md | 2 +- packages/test-utils/src/index.ts | 36 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/.changeset/quick-mangos-vanish.md b/.changeset/quick-mangos-vanish.md index d3f40d75..69186904 100644 --- a/.changeset/quick-mangos-vanish.md +++ b/.changeset/quick-mangos-vanish.md @@ -2,4 +2,4 @@ "@jspsych/test-utils": minor --- -Added mouseMove utility function to dispatch "mousemove" events with location relative to a target element. +Added `mouseMove()`, `mouseDown()`, and `mouseUp()` utility functions to dispatch mouse events with location relative to a target element. diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 7f5779a8..12ea1282 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -46,6 +46,42 @@ export function mouseMove(x: number, y: number, container: Element) { container.dispatchEvent(new MouseEvent("mousemove", eventInit)); } +/** + * Dispatch a `mouseup` event, with x and y defined relative to the container element. + * @param x The x location of the event, relative to the x location of `container`. + * @param y The y location of the event, relative to the y location of `container`. + * @param container The DOM element for relative location of the event. + */ +export function mouseUp(x: number, y: number, container: Element) { + const containerRect = container.getBoundingClientRect(); + + const eventInit = { + clientX: containerRect.x + x, + clientY: containerRect.y + y, + bubbles: true, + }; + + container.dispatchEvent(new MouseEvent("mouseup", eventInit)); +} + +/** + * Dispatch a `mousemove` event, with x and y defined relative to the container element. + * @param x The x location of the event, relative to the x location of `container`. + * @param y The y location of the event, relative to the y location of `container`. + * @param container The DOM element for relative location of the event. + */ +export function mouseDown(x: number, y: number, container: Element) { + const containerRect = container.getBoundingClientRect(); + + const eventInit = { + clientX: containerRect.x + x, + clientY: containerRect.y + y, + bubbles: true, + }; + + container.dispatchEvent(new MouseEvent("mousedown", eventInit)); +} + /** * https://github.com/facebook/jest/issues/2157#issuecomment-279171856 */ From 0e0d884a1effe0ac69549dc3462a5645738e14ad Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 15:25:48 -0400 Subject: [PATCH 10/18] allow recording of mouseup and mousedown --- .changeset/ten-owls-talk.md | 2 +- docs/extensions/mouse-tracking.md | 7 +- .../src/index.spec.ts | 147 +++++++++++++++++- .../extension-mouse-tracking/src/index.ts | 57 ++++++- 4 files changed, 202 insertions(+), 11 deletions(-) diff --git a/.changeset/ten-owls-talk.md b/.changeset/ten-owls-talk.md index 17aafab6..4011dd40 100644 --- a/.changeset/ten-owls-talk.md +++ b/.changeset/ten-owls-talk.md @@ -2,4 +2,4 @@ "@jspsych/extension-mouse-tracking": major --- -Created an extension that enables mouse tracking. The extension records the coordinates and time of mouse movements, as well as optionally recording the coordinates of objects on the screen to enable mapping of mouse movements onto screen objects. +Created an extension that enables mouse tracking. The extension records the coordinates and time of mousemove, mousedown, and mouseup events, as well as optionally recording the coordinates of objects on the screen to enable mapping of mouse events onto screen objects. diff --git a/docs/extensions/mouse-tracking.md b/docs/extensions/mouse-tracking.md index 4566374d..59e574fc 100644 --- a/docs/extensions/mouse-tracking.md +++ b/docs/extensions/mouse-tracking.md @@ -1,7 +1,7 @@ # mouse-tracking This extension supports mouse tracking. -Specifically, it can record the `x` `y` coordinates and time of [mousemove events](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event). +Specifically, it can record the `x` `y` coordinates and time of [mousemove events](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event), [mousedown events](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousedown_event), and [mouseup events](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseup_event). It also allows recording of the [bounding rectangle](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) of elements on the screen to support the calculation of mouse events relative to different elements. ## Parameters @@ -20,7 +20,7 @@ initJsPsych({ Parameter | Type | Default Value | Description ----------|------|---------------|------------ -minimum_sample_time | number | 0 | The minimum time between mouse samples. If mouse events occur more rapidly than this limit, they will not be recorded. Use this if you want to keep the data files smaller and don't need high resolution tracking data. The default value of 0 means that all events will be recorded. +minimum_sample_time | number | 0 | The minimum time between samples for `mousemove` events. If `mousemove` events occur more rapidly than this limit, they will not be recorded. Use this if you want to keep the data files smaller and don't need high resolution tracking data. The default value of 0 means that all events will be recorded. ### Trial Parameters @@ -38,12 +38,13 @@ var trial = { Parameter | Type | Default Value | Description ----------|------|---------------|------------ targets | array | [] | A list of elements on the page that you would like to record the coordinates of for comparison with the WebGazer data. Each entry in the array should be a valid [CSS selector string](https://www.w3schools.com/cssref/css_selectors.asp) that identifies the element. The selector string should be valid for exactly one element on the page. If the selector is valid for more than one element then only the first matching element will be recorded. +events | array | ['mousemove'] | A list of events to track. Can include 'mousemove', 'mousedown', and 'mouseup'. ## Data Generated Name | Type | Value -----|------|------ -mouse_tracking_data | array | An array of objects containing mouse movement data for the trial. Each object has an `x`, a `y`, and a `t` 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. +mouse_tracking_data | array | 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_targets | object | 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. ## Examples diff --git a/packages/extension-mouse-tracking/src/index.spec.ts b/packages/extension-mouse-tracking/src/index.spec.ts index b054b33e..03ff7c27 100644 --- a/packages/extension-mouse-tracking/src/index.spec.ts +++ b/packages/extension-mouse-tracking/src/index.spec.ts @@ -1,5 +1,5 @@ import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response"; -import { clickTarget, mouseMove, pressKey, startTimeline } from "@jspsych/test-utils"; +import { mouseDown, mouseMove, mouseUp, pressKey, startTimeline } from "@jspsych/test-utils"; import { initJsPsych } from "jspsych"; import MouseTrackingExtension from "."; @@ -38,14 +38,159 @@ describe("Mouse Tracking Extension", () => { expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ x: targetRect.x + 50, y: targetRect.y + 50, + event: "mousemove", }); expect(getData().values()[0].mouse_tracking_data[1]).toMatchObject({ x: targetRect.x + 55, y: targetRect.y + 50, + event: "mousemove", }); expect(getData().values()[0].mouse_tracking_data[2]).toMatchObject({ x: targetRect.x + 60, y: targetRect.y + 50, + event: "mousemove", + }); + }); + + test("adds mouse down data to trial", async () => { + const jsPsych = initJsPsych({ + extensions: [{ type: MouseTrackingExtension }], + }); + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "
", + extensions: [ + { + type: MouseTrackingExtension, + params: { events: ["mousedown"] }, + }, + ], + }, + ]; + + const { displayElement, getHTML, 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 expectFinished(); + + expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ + x: targetRect.x + 50, + y: targetRect.y + 50, + event: "mousedown", + }); + expect(getData().values()[0].mouse_tracking_data[1]).toMatchObject({ + x: targetRect.x + 55, + y: targetRect.y + 50, + event: "mousedown", + }); + expect(getData().values()[0].mouse_tracking_data[2]).toMatchObject({ + x: targetRect.x + 60, + y: targetRect.y + 50, + event: "mousedown", + }); + }); + + test("adds mouse up data to trial", async () => { + const jsPsych = initJsPsych({ + extensions: [{ type: MouseTrackingExtension }], + }); + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "
", + extensions: [ + { + type: MouseTrackingExtension, + params: { events: ["mouseup"] }, + }, + ], + }, + ]; + + const { displayElement, getHTML, 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 expectFinished(); + + expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ + x: targetRect.x + 50, + y: targetRect.y + 50, + event: "mouseup", + }); + expect(getData().values()[0].mouse_tracking_data[1]).toMatchObject({ + x: targetRect.x + 55, + y: targetRect.y + 50, + event: "mouseup", + }); + expect(getData().values()[0].mouse_tracking_data[2]).toMatchObject({ + x: targetRect.x + 60, + y: targetRect.y + 50, + event: "mouseup", + }); + }); + + test("ignores mousemove when not in events", async () => { + const jsPsych = initJsPsych({ + extensions: [{ type: MouseTrackingExtension }], + }); + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "
", + extensions: [ + { + type: MouseTrackingExtension, + params: { events: ["mousedown"] }, + }, + ], + }, + ]; + + const { displayElement, getHTML, 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 expectFinished(); + + expect(getData().values()[0].mouse_tracking_data.length).toBe(1); + + expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ + x: targetRect.x + 60, + y: targetRect.y + 50, + event: "mousedown", }); }); diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts index 550cdc6e..8f7cff63 100644 --- a/packages/extension-mouse-tracking/src/index.ts +++ b/packages/extension-mouse-tracking/src/index.ts @@ -15,7 +15,12 @@ interface OnStartParameters { * An array of string selectors. The selectors should identify one unique element on the page. * The DOMRect of the element will be stored in the data. */ - targets: Array; + targets?: Array; + /** + * An array of mouse events to track. Can include `"mousemove"`, `"mousedown"`, and `"mouseup"`. + * @default ['mousemove'] + */ + events?: Array; } class MouseTrackingExtension implements JsPsychExtension { @@ -32,6 +37,7 @@ class MouseTrackingExtension implements JsPsychExtension { private currentTrialStartTime: number; private minimumSampleTime: number; private lastSampleTime: number; + private eventsToTrack: Array; initialize = ({ minimum_sample_time = 0 }: InitializeParameters): Promise => { this.domObserver = new MutationObserver(this.mutationObserverCallback); @@ -43,10 +49,13 @@ class MouseTrackingExtension implements JsPsychExtension { }; on_start = (params: OnStartParameters): void => { + params = params || {}; + this.currentTrialData = []; this.currentTrialTargets = new Map(); - this.currentTrialSelectors = typeof params !== "undefined" ? params.targets : []; + this.currentTrialSelectors = params.targets || []; this.lastSampleTime = null; + this.eventsToTrack = params.events || ["mousemove"]; this.domObserver.observe(this.jsPsych.getDisplayElement(), { childList: true }); }; @@ -56,13 +65,29 @@ class MouseTrackingExtension implements JsPsychExtension { this.currentTrialStartTime = performance.now(); // start data collection - window.addEventListener("mousemove", this.mouseEventHandler); + if (this.eventsToTrack.includes("mousemove")) { + window.addEventListener("mousemove", this.mouseMoveEventHandler); + } + if (this.eventsToTrack.includes("mousedown")) { + window.addEventListener("mousedown", this.mouseDownEventHandler); + } + if (this.eventsToTrack.includes("mouseup")) { + window.addEventListener("mouseup", this.mouseUpEventHandler); + } }; on_finish = () => { this.domObserver.disconnect(); - window.removeEventListener("mousemove", this.mouseEventHandler); + if (this.eventsToTrack.includes("mousemove")) { + window.removeEventListener("mousemove", this.mouseMoveEventHandler); + } + if (this.eventsToTrack.includes("mousedown")) { + window.removeEventListener("mousedown", this.mouseDownEventHandler); + } + if (this.eventsToTrack.includes("mouseup")) { + window.removeEventListener("mouseup", this.mouseUpEventHandler); + } return { mouse_tracking_data: this.currentTrialData, @@ -70,7 +95,7 @@ class MouseTrackingExtension implements JsPsychExtension { }; }; - private mouseEventHandler = (e) => { + private mouseMoveEventHandler = (e) => { const x = e.clientX; const y = e.clientY; @@ -82,10 +107,30 @@ class MouseTrackingExtension implements JsPsychExtension { event_time - this.lastSampleTime >= this.minimumSampleTime ) { this.lastSampleTime = event_time; - this.currentTrialData.push({ x, y, t }); + this.currentTrialData.push({ x, y, t, event: "mousemove" }); } }; + private mouseUpEventHandler = (e) => { + const x = e.clientX; + const y = e.clientY; + + const event_time = performance.now(); + const t = Math.round(event_time - this.currentTrialStartTime); + + this.currentTrialData.push({ x, y, t, event: "mouseup" }); + }; + + private mouseDownEventHandler = (e) => { + const x = e.clientX; + const y = e.clientY; + + const event_time = performance.now(); + const t = Math.round(event_time - this.currentTrialStartTime); + + this.currentTrialData.push({ x, y, t, event: "mousedown" }); + }; + private mutationObserverCallback = (mutationsList, observer) => { for (const selector of this.currentTrialSelectors) { if (!this.currentTrialTargets[selector]) { From 4954deb2f7e34b049dc11613e8d1bdaadbd2e3f3 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 15:26:27 -0400 Subject: [PATCH 11/18] update description in list of extensions --- docs/extensions/list-of-extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensions/list-of-extensions.md b/docs/extensions/list-of-extensions.md index 3ad9b412..d45a779b 100644 --- a/docs/extensions/list-of-extensions.md +++ b/docs/extensions/list-of-extensions.md @@ -9,5 +9,5 @@ For an overview of what extensions are and how they work, see our [extensions ov Extension | Description ------ | ----------- -[mouse‑tracking](../extensions/mouse-tracking.md) | Enables tracking of mouse movements and recording location of objects on screen. +[mouse‑tracking](../extensions/mouse-tracking.md) | Enables tracking of mouse events and recording location of objects on screen. [webgazer](../extensions/webgazer.md) | Enables eye tracking using the [WebGazer](https://webgazer.cs.brown.edu/) library. \ No newline at end of file From 432314dc68b92de62e2310ea47656770d95a2254 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 16:27:10 -0400 Subject: [PATCH 12/18] Apply suggestions from code review Co-authored-by: bjoluc --- packages/extension-mouse-tracking/package.json | 2 +- packages/extension-mouse-tracking/src/index.ts | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/extension-mouse-tracking/package.json b/packages/extension-mouse-tracking/package.json index ef1dd678..4c9beb08 100644 --- a/packages/extension-mouse-tracking/package.json +++ b/packages/extension-mouse-tracking/package.json @@ -25,7 +25,7 @@ "repository": { "type": "git", "url": "git+https://github.com/jspsych/jsPsych.git", - "directory": "packages/extension-webgazer" + "directory": "packages/extension-mouse-tracking" }, "author": "Josh de Leeuw", "license": "MIT", diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts index 8f7cff63..b3d02645 100644 --- a/packages/extension-mouse-tracking/src/index.ts +++ b/packages/extension-mouse-tracking/src/index.ts @@ -39,13 +39,9 @@ class MouseTrackingExtension implements JsPsychExtension { private lastSampleTime: number; private eventsToTrack: Array; - initialize = ({ minimum_sample_time = 0 }: InitializeParameters): Promise => { + initialize = async ({ minimum_sample_time = 0 }: InitializeParameters) => { this.domObserver = new MutationObserver(this.mutationObserverCallback); this.minimumSampleTime = minimum_sample_time; - - return new Promise((resolve, reject) => { - resolve(); - }); }; on_start = (params: OnStartParameters): void => { @@ -134,12 +130,10 @@ class MouseTrackingExtension implements JsPsychExtension { private mutationObserverCallback = (mutationsList, observer) => { for (const selector of this.currentTrialSelectors) { if (!this.currentTrialTargets[selector]) { - if (this.jsPsych.getDisplayElement().querySelector(selector)) { - var coords = this.jsPsych - .getDisplayElement() - .querySelector(selector) - .getBoundingClientRect(); - this.currentTrialTargets[selector] = coords; + const target = this.jsPsych.getDisplayElement().querySelector(selector); + if (target) { + this.currentTrialTargets.set(selector, target.getBoundingClientRect()); + } } } } From f2306b57649a3243522d826eb8e124ce051970e4 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 16:27:31 -0400 Subject: [PATCH 13/18] add ms as units --- docs/extensions/mouse-tracking.md | 2 +- packages/extension-mouse-tracking/src/index.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/extensions/mouse-tracking.md b/docs/extensions/mouse-tracking.md index 59e574fc..196ba3bc 100644 --- a/docs/extensions/mouse-tracking.md +++ b/docs/extensions/mouse-tracking.md @@ -20,7 +20,7 @@ initJsPsych({ Parameter | Type | Default Value | Description ----------|------|---------------|------------ -minimum_sample_time | number | 0 | The minimum time between samples for `mousemove` events. If `mousemove` events occur more rapidly than this limit, they will not be recorded. Use this if you want to keep the data files smaller and don't need high resolution tracking data. The default value of 0 means that all events will be recorded. +minimum_sample_time | number | 0 | The minimum time between samples for `mousemove` events in milliseconds. If `mousemove` events occur more rapidly than this limit, they will not be recorded. Use this if you want to keep the data files smaller and don't need high resolution tracking data. The default value of 0 means that all events will be recorded. ### Trial Parameters diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts index 8f7cff63..f33d61aa 100644 --- a/packages/extension-mouse-tracking/src/index.ts +++ b/packages/extension-mouse-tracking/src/index.ts @@ -2,8 +2,9 @@ import { JsPsych, JsPsychExtension, JsPsychExtensionInfo } from "jspsych"; interface InitializeParameters { /** - * The minimum time between mouse samples. If mouse events occur more rapidly than this limit, they will - * not be recorded. Use this if you want to keep the data files smaller and don't need high resolution + * The minimum time between samples for `mousemove` events in milliseconds. + * If `mousemove` events occur more rapidly than this limit, they will not be recorded. + * Use this if you want to keep the data files smaller and don't need high resolution * tracking data. The default value of 0 means that all events will be recorded. * @default 0 */ From 6a607c6f1b446f328bf4bb2b56219d464ded1105 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 16:30:50 -0400 Subject: [PATCH 14/18] more suggestions from code review --- packages/extension-mouse-tracking/src/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts index 13c5fe89..ea3bd185 100644 --- a/packages/extension-mouse-tracking/src/index.ts +++ b/packages/extension-mouse-tracking/src/index.ts @@ -88,7 +88,7 @@ class MouseTrackingExtension implements JsPsychExtension { return { mouse_tracking_data: this.currentTrialData, - mouse_tracking_targets: this.currentTrialTargets, + mouse_tracking_targets: Object.fromEntries(this.currentTrialTargets.entries()), }; }; @@ -130,12 +130,11 @@ class MouseTrackingExtension implements JsPsychExtension { private mutationObserverCallback = (mutationsList, observer) => { for (const selector of this.currentTrialSelectors) { - if (!this.currentTrialTargets[selector]) { + if (!this.currentTrialTargets.has(selector)) { const target = this.jsPsych.getDisplayElement().querySelector(selector); if (target) { this.currentTrialTargets.set(selector, target.getBoundingClientRect()); } - } } } }; From 15d790e3ad1d662c49c7c7c6662dca8ebaedb398 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 16:47:35 -0400 Subject: [PATCH 15/18] update package-lock.json --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index 40981803..e2b8f52e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14545,6 +14545,7 @@ } }, "packages/extension-mouse-tracking": { + "name": "@jspsych/extension-mouse-tracking", "version": "0.1.0", "license": "MIT", "devDependencies": { From 69abb7e851f29a6bbf7f1859b8615f60531c88df Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 16:47:45 -0400 Subject: [PATCH 16/18] add test case for minimum_sample_time --- .../src/index.spec.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/extension-mouse-tracking/src/index.spec.ts b/packages/extension-mouse-tracking/src/index.spec.ts index 03ff7c27..0afbfd1e 100644 --- a/packages/extension-mouse-tracking/src/index.spec.ts +++ b/packages/extension-mouse-tracking/src/index.spec.ts @@ -227,4 +227,50 @@ describe("Mouse Tracking Extension", () => { expect(getData().values()[0].mouse_tracking_targets["#target"]).toEqual(targetRect); expect(getData().values()[0].mouse_tracking_targets["#target2"]).toEqual(target2Rect); }); + + test("ignores mousemove events that are faster than minimum_sample_time", async () => { + const jsPsych = initJsPsych({ + extensions: [{ type: MouseTrackingExtension, params: { minimum_sample_time: 100 } }], + }); + + const timeline = [ + { + type: htmlKeyboardResponse, + stimulus: "
", + extensions: [{ type: MouseTrackingExtension }], + }, + ]; + + const { displayElement, getHTML, getData, expectFinished } = await startTimeline( + timeline, + jsPsych + ); + + const targetRect = displayElement.querySelector("#target").getBoundingClientRect(); + + mouseMove(50, 50, displayElement.querySelector("#target")); + jest.advanceTimersByTime(50); + + // this one should be ignored + mouseMove(55, 50, displayElement.querySelector("#target")); + jest.advanceTimersByTime(50); + + // this one should register + mouseMove(60, 50, displayElement.querySelector("#target")); + + pressKey("a"); + + await expectFinished(); + + expect(getData().values()[0].mouse_tracking_data[0]).toMatchObject({ + x: targetRect.x + 50, + y: targetRect.y + 50, + event: "mousemove", + }); + expect(getData().values()[0].mouse_tracking_data[1]).toMatchObject({ + x: targetRect.x + 60, + y: targetRect.y + 50, + event: "mousemove", + }); + }); }); From 2932c3d0642152f1eda896c56111cea997ac56a2 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 17:06:10 -0400 Subject: [PATCH 17/18] better typing on clientX and clientY --- packages/extension-mouse-tracking/src/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts index ea3bd185..02ff963e 100644 --- a/packages/extension-mouse-tracking/src/index.ts +++ b/packages/extension-mouse-tracking/src/index.ts @@ -92,9 +92,9 @@ class MouseTrackingExtension implements JsPsychExtension { }; }; - private mouseMoveEventHandler = (e) => { - const x = e.clientX; - const y = e.clientY; + private mouseMoveEventHandler = ({ clientX, clientY }: MouseEvent) => { + const x = clientX; + const y = clientY; const event_time = performance.now(); const t = Math.round(event_time - this.currentTrialStartTime); @@ -108,9 +108,9 @@ class MouseTrackingExtension implements JsPsychExtension { } }; - private mouseUpEventHandler = (e) => { - const x = e.clientX; - const y = e.clientY; + private mouseUpEventHandler = ({ clientX, clientY }: MouseEvent) => { + const x = clientX; + const y = clientY; const event_time = performance.now(); const t = Math.round(event_time - this.currentTrialStartTime); @@ -118,9 +118,9 @@ class MouseTrackingExtension implements JsPsychExtension { this.currentTrialData.push({ x, y, t, event: "mouseup" }); }; - private mouseDownEventHandler = (e) => { - const x = e.clientX; - const y = e.clientY; + private mouseDownEventHandler = ({ clientX, clientY }: MouseEvent) => { + const x = clientX; + const y = clientY; const event_time = performance.now(); const t = Math.round(event_time - this.currentTrialStartTime); From ffe17b9302a1d342d8a0c8d33a970d849450abff Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 14 Oct 2021 17:21:34 -0400 Subject: [PATCH 18/18] use fancy renaming during destructuring --- packages/extension-mouse-tracking/src/index.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/extension-mouse-tracking/src/index.ts b/packages/extension-mouse-tracking/src/index.ts index 02ff963e..e251b5bc 100644 --- a/packages/extension-mouse-tracking/src/index.ts +++ b/packages/extension-mouse-tracking/src/index.ts @@ -92,10 +92,7 @@ class MouseTrackingExtension implements JsPsychExtension { }; }; - private mouseMoveEventHandler = ({ clientX, clientY }: MouseEvent) => { - const x = clientX; - const y = clientY; - + private mouseMoveEventHandler = ({ clientX: x, clientY: y }: MouseEvent) => { const event_time = performance.now(); const t = Math.round(event_time - this.currentTrialStartTime); @@ -108,20 +105,14 @@ class MouseTrackingExtension implements JsPsychExtension { } }; - private mouseUpEventHandler = ({ clientX, clientY }: MouseEvent) => { - const x = clientX; - const y = clientY; - + private mouseUpEventHandler = ({ clientX: x, clientY: y }: MouseEvent) => { const event_time = performance.now(); const t = Math.round(event_time - this.currentTrialStartTime); this.currentTrialData.push({ x, y, t, event: "mouseup" }); }; - private mouseDownEventHandler = ({ clientX, clientY }: MouseEvent) => { - const x = clientX; - const y = clientY; - + private mouseDownEventHandler = ({ clientX: x, clientY: y }: MouseEvent) => { const event_time = performance.now(); const t = Math.round(event_time - this.currentTrialStartTime);