From e2a9e6b33e26a1f351cc4ba4c0caadeafb06bd9b Mon Sep 17 00:00:00 2001 From: Bankminer78 Date: Mon, 15 Jul 2024 13:47:23 -0400 Subject: [PATCH] Changed AudioPlayer to implement interface; fixed build error; rewrote plugins using AudioPlayer and added tests; changed test-utils' clickTarget to respect disabled forms --- .../src/modules/plugin-api/AudioPlayer.ts | 10 +- .../src/index.spec.ts | 159 ++++++- .../plugin-audio-button-response/src/index.ts | 290 ++++++------- .../src/index.ts | 5 +- .../src/index.spec.ts | 132 +++++- .../plugin-audio-slider-response/src/index.ts | 401 +++++++++--------- packages/test-utils/src/index.ts | 7 + 7 files changed, 628 insertions(+), 376 deletions(-) diff --git a/packages/jspsych/src/modules/plugin-api/AudioPlayer.ts b/packages/jspsych/src/modules/plugin-api/AudioPlayer.ts index 9daef348ab..fe95449965 100644 --- a/packages/jspsych/src/modules/plugin-api/AudioPlayer.ts +++ b/packages/jspsych/src/modules/plugin-api/AudioPlayer.ts @@ -3,7 +3,15 @@ export interface AudioPlayerOptions { audioContext?: AudioContext; } -export class AudioPlayer { +export interface AudioPlayerInterface { + load(): Promise; + play(): void; + stop(): void; + addEventListener(eventName: string, callback: EventListenerOrEventListenerObject): void; + removeEventListener(eventName: string, callback: EventListenerOrEventListenerObject): void; +} + +export class AudioPlayer implements AudioPlayerInterface { private audio: HTMLAudioElement | AudioBufferSourceNode; private audioContext: AudioContext | null; private useWebAudio: boolean; diff --git a/packages/plugin-audio-button-response/src/index.spec.ts b/packages/plugin-audio-button-response/src/index.spec.ts index 4a252f827d..be15c64169 100644 --- a/packages/plugin-audio-button-response/src/index.spec.ts +++ b/packages/plugin-audio-button-response/src/index.spec.ts @@ -1,13 +1,62 @@ -import { clickTarget, simulateTimeline, startTimeline } from "@jspsych/test-utils"; +jest.mock("../../jspsych/src/modules/plugin-api/AudioPlayer"); + +import { clickTarget, flushPromises, simulateTimeline, startTimeline } from "@jspsych/test-utils"; import { initJsPsych } from "jspsych"; +//@ts-expect-error mock +import { mockStop } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; import audioButtonResponse from "."; jest.useFakeTimers(); +beforeEach(() => { + jest.clearAllMocks(); +}); + // skip this until we figure out how to mock the audio loading -describe.skip("audio-button-response", () => { +describe("audio-button-response", () => { + it.only("works with all defaults", async () => { + const { expectFinished, expectRunning, displayElement, getHTML } = await startTimeline([ + { + type: audioButtonResponse, + choices: ["choice1"], + stimulus: "foo.mp3", + }, + ]); + + expectRunning(); + + console.log(getHTML()); + + clickTarget(displayElement.querySelector("button")); + + expectFinished(); + + await flushPromises(); + }); + it("works with use_webaudio:false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioButtonResponse, + choices: ["choice1"], + stimulus: "foo.mp3", + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); test("on_load event triggered after page setup complete", async () => { + const onLoadCallback = jest.fn(); + const timeline = [ { type: audioButtonResponse, @@ -15,9 +64,7 @@ describe.skip("audio-button-response", () => { prompt: "foo", choices: ["choice1"], on_load: () => { - expect(getHTML()).toContain("foo"); - - clickTarget(displayElement.querySelector("button")); + onLoadCallback(); }, }, ]; @@ -26,11 +73,107 @@ describe.skip("audio-button-response", () => { use_webaudio: false, }); - const { getHTML, finished, displayElement } = await startTimeline(timeline, jsPsych); + await startTimeline(timeline, jsPsych); + + expect(onLoadCallback).toHaveBeenCalled(); + }); + it("trial ends when button is clicked", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + prompt: "foo", + choices: ["choice1"], + }, + ], + jsPsych + ); + + await expectRunning(); - expect(getHTML()).not.toContain("foo"); + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + + it("ends when trial_ends_after_audio is true and audio finishes", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); - await finished; + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + trial_duration: 30000, + trial_ends_after_audio: true, + }, + ], + jsPsych + ); + + await expectRunning(); + + jest.runAllTimers(); + + await expectFinished(); + }); + it("ends when trial_duration is shorter than the audio duration, stopping the audio", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + trial_duration: 500, + }, + ], + jsPsych + ); + + await expectRunning(); + + expect(mockStop).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(500); + + expect(mockStop).toHaveBeenCalled(); + + await expectFinished(); + }); + it("prevents responses when response_allowed_while_playing is false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement, getHTML } = await startTimeline( + [ + { + type: audioButtonResponse, + stimulus: "foo.mp3", + choices: ["choice1"], + response_allowed_while_playing: false, + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectRunning(); + + jest.runAllTimers(); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); }); }); diff --git a/packages/plugin-audio-button-response/src/index.ts b/packages/plugin-audio-button-response/src/index.ts index 60c7d8126f..29257f3d4c 100644 --- a/packages/plugin-audio-button-response/src/index.ts +++ b/packages/plugin-audio-button-response/src/index.ts @@ -1,5 +1,8 @@ +import autoBind from "auto-bind"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { AudioPlayerInterface } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; + const info = { name: "audio-button-response", parameters: { @@ -98,196 +101,163 @@ type Info = typeof info; */ class AudioButtonResponsePlugin implements JsPsychPlugin { static info = info; - private audio; - + private audio: AudioPlayerInterface; + private params: TrialType; private buttonElements: HTMLElement[] = []; + private display: HTMLElement; + private response: { rt: number; button: number } = { rt: null, button: null }; + private context: AudioContext; + private startTime: number; + private trial_complete: (trial_data: { rt: number; stimulus: string; response: number }) => void; + + constructor(private jsPsych: JsPsych) { + autoBind(this); + } - constructor(private jsPsych: JsPsych) {} - - trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { + async trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { // hold the .resolve() function from the Promise that ends the trial - let trial_complete; - + this.trial_complete; + this.params = trial; + this.display = display_element; // setup stimulus - var context = this.jsPsych.pluginAPI.audioContext(); - - // store response - var response = { - rt: null, - button: null, - }; - - // record webaudio context start time - var startTime; + this.context = this.jsPsych.pluginAPI.audioContext(); // load audio file - this.jsPsych.pluginAPI - .getAudioBuffer(trial.stimulus) - .then((buffer) => { - if (context !== null) { - this.audio = context.createBufferSource(); - this.audio.buffer = buffer; - this.audio.connect(context.destination); - } else { - this.audio = buffer; - this.audio.currentTime = 0; - } - setupTrial(); - }) - .catch((err) => { - console.error( - `Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.` - ); - console.error(err); - }); - - const setupTrial = () => { - // set up end event if trial needs it - if (trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", end_trial); - } - - // enable buttons after audio ends if necessary - if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", enable_buttons); - } - - // Display buttons - const buttonGroupElement = document.createElement("div"); - buttonGroupElement.id = "jspsych-audio-button-response-btngroup"; - if (trial.button_layout === "grid") { - buttonGroupElement.classList.add("jspsych-btn-group-grid"); - if (trial.grid_rows === null && trial.grid_columns === null) { - throw new Error( - "You cannot set `grid_rows` to `null` without providing a value for `grid_columns`." - ); - } - const n_cols = - trial.grid_columns === null - ? Math.ceil(trial.choices.length / trial.grid_rows) - : trial.grid_columns; - const n_rows = - trial.grid_rows === null - ? Math.ceil(trial.choices.length / trial.grid_columns) - : trial.grid_rows; - buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`; - buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`; - } else if (trial.button_layout === "flex") { - buttonGroupElement.classList.add("jspsych-btn-group-flex"); - } + this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus); - for (const [choiceIndex, choice] of trial.choices.entries()) { - buttonGroupElement.insertAdjacentHTML("beforeend", trial.button_html(choice, choiceIndex)); - const buttonElement = buttonGroupElement.lastChild as HTMLElement; - buttonElement.dataset.choice = choiceIndex.toString(); - buttonElement.addEventListener("click", () => { - after_response(choiceIndex); - }); - this.buttonElements.push(buttonElement); - } + // set up end event if trial needs it + if (trial.trial_ends_after_audio) { + this.audio.addEventListener("ended", this.end_trial); + } - display_element.appendChild(buttonGroupElement); + // enable buttons after audio ends if necessary + if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) { + this.audio.addEventListener("ended", this.enable_buttons); + } - // Show prompt if there is one - if (trial.prompt !== null) { - display_element.insertAdjacentHTML("beforeend", trial.prompt); + // record start time + this.startTime = performance.now(); + + // Display buttons + const buttonGroupElement = document.createElement("div"); + buttonGroupElement.id = "jspsych-audio-button-response-btngroup"; + if (trial.button_layout === "grid") { + buttonGroupElement.classList.add("jspsych-btn-group-grid"); + if (trial.grid_rows === null && trial.grid_columns === null) { + throw new Error( + "You cannot set `grid_rows` to `null` without providing a value for `grid_columns`." + ); } + const n_cols = + trial.grid_columns === null + ? Math.ceil(trial.choices.length / trial.grid_rows) + : trial.grid_columns; + const n_rows = + trial.grid_rows === null + ? Math.ceil(trial.choices.length / trial.grid_columns) + : trial.grid_rows; + buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`; + buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`; + } else if (trial.button_layout === "flex") { + buttonGroupElement.classList.add("jspsych-btn-group-flex"); + } - if (!trial.response_allowed_while_playing) { - disable_buttons(); - } + for (const [choiceIndex, choice] of trial.choices.entries()) { + buttonGroupElement.insertAdjacentHTML("beforeend", trial.button_html(choice, choiceIndex)); + const buttonElement = buttonGroupElement.lastChild as HTMLElement; + buttonElement.dataset.choice = choiceIndex.toString(); + buttonElement.addEventListener("click", () => { + this.after_response(choiceIndex); + }); + this.buttonElements.push(buttonElement); + } - // start time - startTime = performance.now(); + display_element.appendChild(buttonGroupElement); - // start audio - if (context !== null) { - startTime = context.currentTime; - this.audio.start(startTime); - } else { - this.audio.play(); - } + // Show prompt if there is one + if (trial.prompt !== null) { + display_element.insertAdjacentHTML("beforeend", trial.prompt); + } - // end trial if time limit is set - if (trial.trial_duration !== null) { - this.jsPsych.pluginAPI.setTimeout(() => { - end_trial(); - }, trial.trial_duration); - } + if (!trial.response_allowed_while_playing) { + this.disable_buttons(); + } - on_load(); - }; + // end trial if time limit is set + if (trial.trial_duration !== null) { + this.jsPsych.pluginAPI.setTimeout(() => { + this.end_trial(); + }, trial.trial_duration); + } - // function to handle responses by the subject - const after_response = (choice) => { - // measure rt - var endTime = performance.now(); - var rt = Math.round(endTime - startTime); - if (context !== null) { - endTime = context.currentTime; - rt = Math.round((endTime - startTime) * 1000); - } - response.button = parseInt(choice); - response.rt = rt; + on_load(); - // disable all the buttons after a response - disable_buttons(); + this.audio.play(); - if (trial.response_ends_trial) { - end_trial(); - } - }; + return new Promise((resolve) => { + this.trial_complete = resolve; + }); + } - // function to end trial when it is time - const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); + private disable_buttons = () => { + for (const button of this.buttonElements) { + button.setAttribute("disabled", "disabled"); + } + }; - // stop the audio file if it is playing - // remove end event listeners if they exist - if (context !== null) { - this.audio.stop(); - } else { - this.audio.pause(); - } + private enable_buttons = () => { + for (const button of this.buttonElements) { + button.removeAttribute("disabled"); + } + }; + + // function to handle responses by the subject + private after_response = (choice) => { + // measure rt + var endTime = performance.now(); + var rt = Math.round(endTime - this.startTime); + if (this.context !== null) { + endTime = this.context.currentTime; + rt = Math.round((endTime - this.startTime) * 1000); + } + this.response.button = parseInt(choice); + this.response.rt = rt; - this.audio.removeEventListener("ended", end_trial); - this.audio.removeEventListener("ended", enable_buttons); + // disable all the buttons after a response + this.disable_buttons(); - // gather the data to store for the trial - var trial_data = { - rt: response.rt, - stimulus: trial.stimulus, - response: response.button, - }; + if (this.params.response_ends_trial) { + this.end_trial(); + } + }; - // clear the display - display_element.innerHTML = ""; + // method to end trial when it is time + private end_trial = () => { + // kill any remaining setTimeout handlers + this.jsPsych.pluginAPI.clearAllTimeouts(); - // move on to the next trial - this.jsPsych.finishTrial(trial_data); + // stop the audio file if it is playing + this.audio.stop(); - trial_complete(); - }; + // remove end event listeners if they exist + this.audio.removeEventListener("ended", this.end_trial); + this.audio.removeEventListener("ended", this.enable_buttons); - const disable_buttons = () => { - for (const button of this.buttonElements) { - button.setAttribute("disabled", "disabled"); - } + // gather the data to store for the trial + var trial_data = { + rt: this.response.rt, + stimulus: this.params.stimulus, + response: this.response.button, }; - const enable_buttons = () => { - for (const button of this.buttonElements) { - button.removeAttribute("disabled"); - } - }; + // clear the display + this.display.innerHTML = ""; - return new Promise((resolve) => { - trial_complete = resolve; - }); - } + // move on to the next trial + this.trial_complete(trial_data); + }; - simulate( + async simulate( trial: TrialType, simulation_mode, simulation_options: any, diff --git a/packages/plugin-audio-keyboard-response/src/index.ts b/packages/plugin-audio-keyboard-response/src/index.ts index 9315280c24..2727be5350 100644 --- a/packages/plugin-audio-keyboard-response/src/index.ts +++ b/packages/plugin-audio-keyboard-response/src/index.ts @@ -1,7 +1,7 @@ import autoBind from "auto-bind"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; -import { AudioPlayer } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; +import { AudioPlayerInterface } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; const info = { name: "audio-keyboard-response", @@ -63,7 +63,7 @@ type Info = typeof info; */ class AudioKeyboardResponsePlugin implements JsPsychPlugin { static info = info; - private audio: AudioPlayer; + private audio: AudioPlayerInterface; private params: TrialType; private display: HTMLElement; private response: { rt: number; key: string } = { rt: null, key: null }; @@ -79,7 +79,6 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin { this.finish = resolve; this.params = trial; this.display = display_element; - // load audio file this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus); diff --git a/packages/plugin-audio-slider-response/src/index.spec.ts b/packages/plugin-audio-slider-response/src/index.spec.ts index 180214d9cb..c9d8b75827 100644 --- a/packages/plugin-audio-slider-response/src/index.spec.ts +++ b/packages/plugin-audio-slider-response/src/index.spec.ts @@ -1,10 +1,140 @@ -import { pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils"; +jest.mock("../../jspsych/src/modules/plugin-api/AudioPlayer"); + +import { + clickTarget, + flushPromises, + pressKey, + simulateTimeline, + startTimeline, +} from "@jspsych/test-utils"; import { initJsPsych } from "jspsych"; +//@ts-expect-error mock +import { mockStop } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; import audioSliderResponse from "."; jest.useFakeTimers(); +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe("audio-slider-response", () => { + // this relies on AudioContext, which we haven't mocked yet + it.skip("works with all defaults", async () => { + const { expectFinished, expectRunning } = await startTimeline([ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + }, + ]); + + expectRunning(); + + pressKey("a"); + + expectFinished(); + + await flushPromises(); + }); + + it("works with use_webaudio:false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement, getHTML } = await startTimeline( + [ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + }, + ], + jsPsych + ); + + await expectRunning(); + + //jest.runAllTimers(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + + it("ends when trial_ends_after_audio is true and audio finishes", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + trial_ends_after_audio: true, + }, + ], + jsPsych + ); + + await expectRunning(); + + jest.runAllTimers(); + + await expectFinished(); + }); + + it("prevents responses when response_allowed_while_playing is false", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning, displayElement } = await startTimeline( + [ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + response_allowed_while_playing: false, + }, + ], + jsPsych + ); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectRunning(); + + jest.runAllTimers(); + + await expectRunning(); + + clickTarget(displayElement.querySelector("button")); + + await expectFinished(); + }); + + it("ends when trial_duration is shorter than the audio duration, stopping the audio", async () => { + const jsPsych = initJsPsych({ use_webaudio: false }); + + const { expectFinished, expectRunning } = await startTimeline( + [ + { + type: audioSliderResponse, + stimulus: "foo.mp3", + trial_duration: 500, + }, + ], + jsPsych + ); + + await expectRunning(); + + expect(mockStop).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(500); + + expect(mockStop).toHaveBeenCalled(); + + await expectFinished(); + }); +}); describe("audio-slider-response simulation", () => { test("data mode works", async () => { const timeline = [ diff --git a/packages/plugin-audio-slider-response/src/index.ts b/packages/plugin-audio-slider-response/src/index.ts index 08e400897b..e51dfa73be 100644 --- a/packages/plugin-audio-slider-response/src/index.ts +++ b/packages/plugin-audio-slider-response/src/index.ts @@ -1,5 +1,8 @@ +import autoBind from "auto-bind"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { AudioPlayerInterface } from "../../jspsych/src/modules/plugin-api/AudioPlayer"; + const info = { name: "audio-slider-response", parameters: { @@ -104,244 +107,236 @@ type Info = typeof info; */ class AudioSliderResponsePlugin implements JsPsychPlugin { static info = info; - private audio; - - constructor(private jsPsych: JsPsych) {} - - trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { - // hold the .resolve() function from the Promise that ends the trial - let trial_complete; + private audio: AudioPlayerInterface; + private context: AudioContext; + private params: TrialType; + private display: HTMLElement; + private response: { rt: number; response: number } = { rt: null, response: null }; + private startTime: number; + private half_thumb_width: number; + private trial_complete: (trial_data: { + rt: number; + slider_start: number; + response: number; + }) => void; + + constructor(private jsPsych: JsPsych) { + autoBind(this); + } + async trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { + // record webaudio context start time + this.startTime; + this.params = trial; + this.display = display_element; + // for storing data related to response + this.response; // half of the thumb width value from jspsych.css, used to adjust the label positions - var half_thumb_width = 7.5; + this.half_thumb_width = 7.5; + // hold the .resolve() function from the Promise that ends the trial + this.trial_complete; // setup stimulus - var context = this.jsPsych.pluginAPI.audioContext(); + this.context = this.jsPsych.pluginAPI.audioContext(); - // record webaudio context start time - var startTime; + // load audio file + this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus); - // for storing data related to response - var response; + this.setupTrial(); - // load audio file - this.jsPsych.pluginAPI - .getAudioBuffer(trial.stimulus) - .then((buffer) => { - if (context !== null) { - this.audio = context.createBufferSource(); - this.audio.buffer = buffer; - this.audio.connect(context.destination); - } else { - this.audio = buffer; - this.audio.currentTime = 0; - } - setupTrial(); - }) - .catch((err) => { - console.error( - `Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.` - ); - console.error(err); - }); + on_load(); - const setupTrial = () => { - // set up end event if trial needs it - if (trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", end_trial); - } + return new Promise((resolve) => { + this.trial_complete = resolve; + console.log("PROMISE"); + }); + } - // enable slider after audio ends if necessary - if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", enable_slider); - } + // to enable slider after audio ends + private enable_slider() { + document.querySelector("#jspsych-audio-slider-response-response").disabled = + false; + if (!this.params.require_movement) { + document.querySelector("#jspsych-audio-slider-response-next").disabled = + false; + } + } + + private setupTrial = () => { + console.log("SETUP TRIAL"); + // set up end event if trial needs it + if (this.params.trial_ends_after_audio) { + this.audio.addEventListener("ended", this.end_trial); + } + + // enable slider after audio ends if necessary + if (!this.params.response_allowed_while_playing && !this.params.trial_ends_after_audio) { + this.audio.addEventListener("ended", this.enable_slider); + } - var html = '
'; + var html = '
'; + html += + '
'; html += - ''; - html += '' + trial.labels[j] + ""; - html += "
"; - } - html += "
"; - html += "
"; + '' + this.params.labels[j] + ""; html += ""; + } + html += ""; + html += ""; + html += ""; - if (trial.prompt !== null) { - html += trial.prompt; - } + if (this.params.prompt !== null) { + html += this.params.prompt; + } - // add submit button - var next_disabled_attribute = ""; - if (trial.require_movement || !trial.response_allowed_while_playing) { - next_disabled_attribute = "disabled"; - } - html += - '"; + // add submit button + var next_disabled_attribute = ""; + if (this.params.require_movement || !this.params.response_allowed_while_playing) { + next_disabled_attribute = "disabled"; + } + html += + '"; - display_element.innerHTML = html; + this.display.innerHTML = html; - response = { - rt: null, - response: null, - }; + console.log("iinner", this.display.innerHTML); - if (!trial.response_allowed_while_playing) { - display_element.querySelector( - "#jspsych-audio-slider-response-response" - ).disabled = true; - display_element.querySelector( - "#jspsych-audio-slider-response-next" - ).disabled = true; - } + this.response = { + rt: null, + response: null, + }; - if (trial.require_movement) { - const enable_button = () => { - display_element.querySelector( - "#jspsych-audio-slider-response-next" - ).disabled = false; - }; + if (!this.params.response_allowed_while_playing) { + this.display.querySelector( + "#jspsych-audio-slider-response-response" + ).disabled = true; + this.display.querySelector("#jspsych-audio-slider-response-next").disabled = + true; + } - display_element - .querySelector("#jspsych-audio-slider-response-response") - .addEventListener("mousedown", enable_button); + if (this.params.require_movement) { + const enable_button = () => { + this.display.querySelector( + "#jspsych-audio-slider-response-next" + ).disabled = false; + }; - display_element - .querySelector("#jspsych-audio-slider-response-response") - .addEventListener("touchstart", enable_button); + this.display + .querySelector("#jspsych-audio-slider-response-response") + .addEventListener("mousedown", enable_button); - display_element - .querySelector("#jspsych-audio-slider-response-response") - .addEventListener("change", enable_button); - } + this.display + .querySelector("#jspsych-audio-slider-response-response") + .addEventListener("touchstart", enable_button); - display_element - .querySelector("#jspsych-audio-slider-response-next") - .addEventListener("click", () => { - // measure response time - var endTime = performance.now(); - var rt = Math.round(endTime - startTime); - if (context !== null) { - endTime = context.currentTime; - rt = Math.round((endTime - startTime) * 1000); - } - response.rt = rt; - response.response = display_element.querySelector( - "#jspsych-audio-slider-response-response" - ).valueAsNumber; - - if (trial.response_ends_trial) { - end_trial(); - } else { - display_element.querySelector( - "#jspsych-audio-slider-response-next" - ).disabled = true; - } - }); - - startTime = performance.now(); - // start audio - if (context !== null) { - startTime = context.currentTime; - this.audio.start(startTime); - } else { - this.audio.play(); - } + this.display + .querySelector("#jspsych-audio-slider-response-response") + .addEventListener("change", enable_button); + } - // end trial if trial_duration is set - if (trial.trial_duration !== null) { - this.jsPsych.pluginAPI.setTimeout(() => { - end_trial(); - }, trial.trial_duration); - } + this.display + .querySelector("#jspsych-audio-slider-response-next") + .addEventListener("click", () => { + // measure response time + var endTime = performance.now(); + var rt = Math.round(endTime - this.startTime); + if (this.context !== null) { + endTime = this.context.currentTime; + rt = Math.round((endTime - this.startTime) * 1000); + } + this.response.rt = rt; + this.response.response = this.display.querySelector( + "#jspsych-audio-slider-response-response" + ).valueAsNumber; - on_load(); - }; + if (this.params.response_ends_trial) { + this.end_trial(); + } else { + this.display.querySelector( + "#jspsych-audio-slider-response-next" + ).disabled = true; + } + }); - // function to enable slider after audio ends - function enable_slider() { - document.querySelector("#jspsych-audio-slider-response-response").disabled = - false; - if (!trial.require_movement) { - document.querySelector("#jspsych-audio-slider-response-next").disabled = - false; - } - } + this.startTime = performance.now(); - const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); + // start audio + this.audio.play(); - // stop the audio file if it is playing - // remove end event listeners if they exist - if (context !== null) { - this.audio.stop(); - } else { - this.audio.pause(); - } + // end trial if trial_duration is set + if (this.params.trial_duration !== null) { + this.jsPsych.pluginAPI.setTimeout(() => { + this.end_trial(); + }, this.params.trial_duration); + } - this.audio.removeEventListener("ended", end_trial); - this.audio.removeEventListener("ended", enable_slider); + console.log("END SETUP TRIAL"); + }; - // save data - var trialdata = { - rt: response.rt, - stimulus: trial.stimulus, - slider_start: trial.slider_start, - response: response.response, - }; + private end_trial = () => { + // kill any remaining setTimeout handlers + this.jsPsych.pluginAPI.clearAllTimeouts(); - display_element.innerHTML = ""; + // stop the audio file if it is playing + this.audio.stop(); - // next trial - this.jsPsych.finishTrial(trialdata); + // remove end event listeners if they exist + this.audio.removeEventListener("ended", this.end_trial); + this.audio.removeEventListener("ended", this.enable_slider); - trial_complete(); + // save data + var trialdata = { + rt: this.response.rt, + stimulus: this.params.stimulus, + slider_start: this.params.slider_start, + response: this.response.response, }; - return new Promise((resolve) => { - trial_complete = resolve; - }); - } + this.display.innerHTML = ""; + + // next trial + this.trial_complete(trialdata); + }; simulate( trial: TrialType, diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 614976bad6..08d4d8c930 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -33,6 +33,13 @@ export async function mouseDownMouseUpTarget(target: Element) { } export async function clickTarget(target: Element) { + // Check if the target is a form element and if it's disabled + if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement) { + if (target.disabled) { + console.log("Target is disabled, not dispatching click event."); + return; // Exit the function if the target is disabled + } + } await dispatchEvent(new MouseEvent("click", { bubbles: true }), target); }