From a1d40993896a029d4780422bce2bcff38ac9f5ee Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Fri, 7 Jun 2024 14:00:32 -0400 Subject: [PATCH] inital metadata module directory setup --- packages/metadata/README.md | 3 + packages/metadata/package.json | 40 +++++++ packages/metadata/rollup.config.mjs | 3 + packages/metadata/src/index.ts | 176 ++++++++++++++++++++++++++++ packages/metadata/tsconfig.json | 7 ++ 5 files changed, 229 insertions(+) create mode 100644 packages/metadata/README.md create mode 100644 packages/metadata/package.json create mode 100644 packages/metadata/rollup.config.mjs create mode 100644 packages/metadata/src/index.ts create mode 100644 packages/metadata/tsconfig.json diff --git a/packages/metadata/README.md b/packages/metadata/README.md new file mode 100644 index 0000000000..fadbce9711 --- /dev/null +++ b/packages/metadata/README.md @@ -0,0 +1,3 @@ +# Test utility functions for jsPsych-related test cases + +These functions are used to assist in testing jsPsych plugins. diff --git a/packages/metadata/package.json b/packages/metadata/package.json new file mode 100644 index 0000000000..b3ea6a13ad --- /dev/null +++ b/packages/metadata/package.json @@ -0,0 +1,40 @@ +{ + "name": "@jspsych/metadata", + "version": "0.0.1", + "description": "Functions for generating and creating metadata file from data to create a Psych-DS compliant dataset", + "type": "module", + "main": "dist/index.cjs", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "typings": "dist/index.d.ts", + "files": [ + "src", + "dist" + ], + "source": "src/index.ts", + "scripts": { + "tsc": "tsc", + "build": "rollup --config", + "build:watch": "npm run build -- --watch" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jspsych/jsPsych.git", + "directory": "packages/metadata" + }, + "author": "vzhang (vzhang@vassar.edu)", + "license": "Vassar", + "bugs": { + "url": "https://github.com/jspsych/jsPsych/issues" + }, + "peerDependencies": { + "jspsych": ">=7.0.0", + "@types/jest": "*" + }, + "devDependencies": { + "@jspsych/config": "^1.1.0", + "jspsych": "^7.0.0" + } +} diff --git a/packages/metadata/rollup.config.mjs b/packages/metadata/rollup.config.mjs new file mode 100644 index 0000000000..22296de328 --- /dev/null +++ b/packages/metadata/rollup.config.mjs @@ -0,0 +1,3 @@ +import { makeNodeRollupConfig } from "@jspsych/config/rollup"; + +export default makeNodeRollupConfig(); diff --git a/packages/metadata/src/index.ts b/packages/metadata/src/index.ts new file mode 100644 index 0000000000..614976bad6 --- /dev/null +++ b/packages/metadata/src/index.ts @@ -0,0 +1,176 @@ +import { setImmediate as flushMicroTasks } from "timers"; + +import { JsPsych } from "jspsych"; + +/** + * https://github.com/facebook/jest/issues/2157#issuecomment-279171856 + */ +export function flushPromises() { + return new Promise((resolve) => flushMicroTasks(resolve)); +} + +export function dispatchEvent(event: Event, target: Element = document.body) { + target.dispatchEvent(event); + return flushPromises(); +} + +export async function keyDown(key: string) { + await dispatchEvent(new KeyboardEvent("keydown", { key })); +} + +export async function keyUp(key: string) { + await dispatchEvent(new KeyboardEvent("keyup", { key })); +} + +export async function pressKey(key: string) { + await keyDown(key); + await keyUp(key); +} + +export async function mouseDownMouseUpTarget(target: Element) { + await dispatchEvent(new MouseEvent("mousedown", { bubbles: true }), target); + await dispatchEvent(new MouseEvent("mouseup", { bubbles: true }), target); +} + +export async function clickTarget(target: Element) { + await dispatchEvent(new MouseEvent("click", { bubbles: true }), target); +} + +/** + * Dispatch a `MouseEvent` of type `eventType`, 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. + */ +async function dispatchMouseEvent(eventType: string, x: number, y: number, container: Element) { + const containerRect = container.getBoundingClientRect(); + await dispatchEvent( + new MouseEvent(eventType, { + clientX: containerRect.x + x, + clientY: containerRect.y + y, + bubbles: true, + }), + container + ); +} + +/** + * 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 async function mouseMove(x: number, y: number, container: Element) { + await dispatchMouseEvent("mousemove", x, y, container); +} + +/** + * 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 async function mouseUp(x: number, y: number, container: Element) { + await dispatchMouseEvent("mouseup", x, y, container); +} + +/** + * 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 async function mouseDown(x: number, y: number, container: Element) { + await dispatchMouseEvent("mousedown", x, y, container); +} + +/** + * Runs the given timeline by calling `jsPsych.run()` on the provided JsPsych object. + * + * @param timeline The timeline that is passed to `jsPsych.run()` + * @param jsPsych The jsPsych instance to be used. If left empty, a new instance will be created. If + * a settings object is passed instead, the settings will be used to create the jsPsych instance. + * + * @returns An object containing test helper functions, the jsPsych instance, and the jsPsych + * display element + */ +export async function startTimeline(timeline: any[], jsPsych: JsPsych | any = {}) { + const jsPsychInstance = jsPsych instanceof JsPsych ? jsPsych : new JsPsych(jsPsych); + + let hasFinished = false; + const finished = jsPsychInstance.run(timeline).then(() => { + hasFinished = true; + }); + await flushPromises(); + + const displayElement = jsPsychInstance.getDisplayElement(); + + return { + jsPsych: jsPsychInstance, + displayElement, + /** Shorthand for `jsPsych.getDisplayElement().innerHTML` */ + getHTML: () => displayElement.innerHTML, + /** Shorthand for `jsPsych.data.get()` */ + getData: () => jsPsychInstance.data.get(), + expectFinished: async () => { + await flushPromises(); + expect(hasFinished).toBe(true); + }, + expectRunning: async () => { + await flushPromises(); + expect(hasFinished).toBe(false); + }, + /** A promise that is resolved when `jsPsych.run()` is done. */ + finished, + }; +} + +/** + * Runs the given timeline by calling `jsPsych.simulate()` on the provided JsPsych object. + * + * @param timeline The timeline that is passed to `jsPsych.run()` + * @param simulation_mode Either 'data-only' mode or 'visual' mode. + * @param simulation_options Options to pass to `jsPsych.simulate()` + * @param jsPsych The jsPsych instance to be used. If left empty, a new instance will be created. If + * a settings object is passed instead, the settings will be used to create the jsPsych instance. + * + * @returns An object containing test helper functions, the jsPsych instance, and the jsPsych + * display element + */ +export async function simulateTimeline( + timeline: any[], + simulation_mode?: "data-only" | "visual", + simulation_options: any = {}, + jsPsych: JsPsych | any = {} +) { + const jsPsychInstance = jsPsych instanceof JsPsych ? jsPsych : new JsPsych(jsPsych); + + let hasFinished = false; + const finished = jsPsychInstance + .simulate(timeline, simulation_mode, simulation_options) + .then(() => { + hasFinished = true; + }); + await flushPromises(); + + const displayElement = jsPsychInstance.getDisplayElement(); + + return { + jsPsych: jsPsychInstance, + displayElement, + /** Shorthand for `jsPsych.getDisplayElement().innerHTML` */ + getHTML: () => displayElement.innerHTML, + /** Shorthand for `jsPsych.data.get()` */ + getData: () => jsPsychInstance.data.get(), + expectFinished: async () => { + await flushPromises(); + expect(hasFinished).toBe(true); + }, + expectRunning: async () => { + await flushPromises(); + expect(hasFinished).toBe(false); + }, + /** A promise that is resolved when `jsPsych.simulate()` is done. */ + finished, + }; +} diff --git a/packages/metadata/tsconfig.json b/packages/metadata/tsconfig.json new file mode 100644 index 0000000000..588f044808 --- /dev/null +++ b/packages/metadata/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@jspsych/config/tsconfig.core.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src"] +}