Skip to content

Commit

Permalink
Update intro exit timings (#899)
Browse files Browse the repository at this point in the history
* update timings and templates

* fix tests

* add finalPayoffs to postflight report

* update tests

* test start and end timers

* fix data export when there is no game

Addresses #898

* fix data export when there is no game

Addresses #898

* skip qualtrics tests because of bot detection
  • Loading branch information
JamesPHoughton authored Sep 26, 2024
1 parent d4126fe commit a2a6835
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 69 deletions.
48 changes: 47 additions & 1 deletion client/src/components/ConditionalRender.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
useStageTimer,
usePlayer,
Expand Down Expand Up @@ -37,6 +37,52 @@ export function TimeConditionalRender({ displayTime, hideTime, children }) {
const timer = useStageTimer();
const player = usePlayer();

const [trigger, setTrigger] = useState(false); // State to trigger re-render in intro/exit

useEffect(() => {
// this is to trigger a re-render during intro/exit steps at hideTime and displayTime
console.log(
"timeconditionalRenderTick",
"displayTime",
displayTime,
"hideTime",
hideTime
);
if (!displayTime && !hideTime) return () => null; // No time conditions
if (timer) return () => null; // Game is running, don't need triggers to rerender

const elapsed = (Date.now() - player.get("localStageStartTime")) / 1000;
console.log(
"timeconditionalRenderTick, elapsed",
elapsed,
"displayTime",
displayTime,
"hideTime",
hideTime
);

let displayTimeoutId = null;
if (displayTime && elapsed < displayTime) {
console.log("setting display trigger for ", displayTime - elapsed);
displayTimeoutId = setTimeout(() => {
setTrigger((prev) => !prev); // Toggle the trigger state
}, (displayTime - elapsed) * 1000);
}

let hideTimeoutId = null;
if (hideTime && elapsed < hideTime) {
console.log("setting hide trigger for ", hideTime - elapsed);
hideTimeoutId = setTimeout(() => {
setTrigger((prev) => !prev); // Toggle the trigger state
}, (hideTime - elapsed) * 1000);
}

return () => {
clearTimeout(displayTimeoutId);
clearTimeout(hideTimeoutId);
};
}, [trigger, timer, player, displayTime, hideTime]); // Dependency array includes trigger to re-run the effect

const msElapsed = timer
? timer.elapsed // game
: Date.now() - player.get("localStageStartTime"); // intro/exit
Expand Down
29 changes: 23 additions & 6 deletions client/src/elements/KitchenTimer.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import React from "react";
import { useStageTimer } from "@empirica/core/player/classic/react";

import React, { useState, useEffect } from "react";
import { useStageTimer, usePlayer } from "@empirica/core/player/classic/react";
import { Progress } from "react-sweet-progress";
import "react-sweet-progress/lib/style.css";

export function KitchenTimer({ startTime, endTime, warnTimeRemaining = 10 }) {
const stageTimer = useStageTimer();
if (!stageTimer) return null;
const stageElapsed = (stageTimer?.elapsed || 0) / 1000;
const player = usePlayer();
const [trigger, setTrigger] = useState(false); // State to trigger re-render every second in intro/exit

useEffect(() => {
// during intro/exit steps, need to manually re-render to display the timer ticking
if (stageTimer) return () => null; // Game is running, don't need triggers to rerender
const timeoutId = setTimeout(() => {
setTrigger((prev) => !prev); // Toggle the trigger state
}, 1000);
return () => clearTimeout(timeoutId);
}, [trigger, stageTimer]);

const stageElapsedMs = stageTimer
? stageTimer.elapsed || 0
: Date.now() - player.get("localStageStartTime");
const stageElapsed = stageElapsedMs / 1000;

const timerDuration = endTime - startTime;

// not started
Expand Down Expand Up @@ -38,7 +52,10 @@ export function KitchenTimer({ startTime, endTime, warnTimeRemaining = 10 }) {
};

return (
<div className="m-1.5rem max-w-xl">
<div
className="m-1.5rem max-w-xl"
data-test={`timer_start_${startTime}_end_${endTime}`}
>
<Progress percent={percent} theme={theme} status="default" />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion client/src/intro-exit/Consent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function Consent({ next }) {
connectionInfo?.isKnownVpn ||
connectionInfo?.timezone !== browserInfo?.timezone;
connectionInfo.effectiveType = navigator?.connection?.effectiveType;
connectionInfo.saveData = navigator?.connection?.saveData;
connectionInfo.saveData = navigator?.connection?.saveData; // The saveData read-only property of the NetworkInformation interface returns true if the user has set a reduced data usage option on the user agent.
connectionInfo.downlink = navigator?.connection?.downlink;
connectionInfo.rtt = navigator?.connection?.rtt;

Expand Down
23 changes: 15 additions & 8 deletions client/src/intro-exit/GenericIntroExitStep.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Element } from "../elements/Element";
import {
ConditionsConditionalRender,
PositionConditionalRender,
TimeConditionalRender,
} from "../components/ConditionalRender";

export function GenericIntroExitStep({ name, elements, index, next, phase }) {
Expand All @@ -32,15 +33,21 @@ export function GenericIntroExitStep({ name, elements, index, next, phase }) {
};

const renderElement = (element, i) => (
<PositionConditionalRender
key={`element_${i}`}
showToPositions={element.showToPositions}
hideFromPositions={element.hideFromPositions}
<TimeConditionalRender
displayTime={element.displayTime}
hideTime={element.hideTime}
key={`time_condition_element_${index}`}
>
<ConditionsConditionalRender conditions={element.conditions}>
<Element element={element} onSubmit={onSubmit} />
</ConditionsConditionalRender>
</PositionConditionalRender>
<PositionConditionalRender
key={`position_conditionelement_${i}`}
showToPositions={element.showToPositions}
hideFromPositions={element.hideFromPositions}
>
<ConditionsConditionalRender conditions={element.conditions}>
<Element element={element} onSubmit={onSubmit} />
</ConditionsConditionalRender>
</PositionConditionalRender>
</TimeConditionalRender>
);

return (
Expand Down
23 changes: 14 additions & 9 deletions client/src/intro-exit/NoGames.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,22 @@ We release studies on a regular basis, and we hope that you will have the opport
if (player) {
console.log("NoGames: error");
const exitCodes = player.get("exitCodes");
const failureMessage = `
## 😬 Server error
We are sorry, your study has unexpectedly stopped.
// const failureMessage = `
// ## 😬 Server error
// We are sorry, your study has unexpectedly stopped.

${
exitCodes !== "none"
? `Please enter code **${exitCodes.error}** to be compensated for your time.`
: ""
}
// ${
// exitCodes !== "none"
// ? `Please enter code **${exitCodes.error}** to be compensated for your time.`
// : ""
// }

// We hope you can join us in a future study.
// `;

We hope you can join us in a future study.
const failureMessage = `
## 🥱 The experiment is now closed.
We release studies on a regular basis, and we hope that you will have the opportunity to participate soon.
`;
return <Markdown text={failureMessage} />;
}
Expand Down
32 changes: 29 additions & 3 deletions cypress/e2e/01_Normal_Paths_Omnibus.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ describe(
// Political affiliation survey
cy.stepSurveyPoliticalPartyUS(playerKeys[0]);
cy.stepSurveyPoliticalPartyUS(playerKeys[1]);
cy.stepSurveyPoliticalPartyUS(playerKeys[2]);

// Todo: Check that we get a warning if we try to leave the page
// cy.on("window:confirm", (text) => {
Expand Down Expand Up @@ -182,6 +183,8 @@ describe(
// Test Prompts in Intro
cy.playerCanNotSee(playerKeys[0], "TestDisplay00");
cy.playerCanNotSee(playerKeys[1], "TestDisplay00");
cy.playerCanNotSee(playerKeys[0], "TestDisplay01"); // timed display after 4 seconds
cy.playerCanSee(playerKeys[0], "TestDisplay02"); // hidden after 4 seconds

cy.get(
`[test-player-id="${playerKeys[0]}"] [data-test="projects/example/multipleChoice.md"] input[value="Markdown"]`
Expand All @@ -207,8 +210,23 @@ describe(
`[test-player-id="${playerKeys[1]}"] textarea[data-test="projects/example/openResponse.md"]`
).type(`Intro Open Response for ${playerKeys[1]}`, { force: true });

cy.playerCanSee(playerKeys[0], "TestDisplay00");
cy.get(
`[test-player-id="${playerKeys[2]}"] textarea[data-test="projects/example/openResponse.md"]`
).type(`Intro Open Response for ${playerKeys[2]}`, { force: true });

cy.get(
`[test-player-id="${playerKeys[2]}"] [data-test="projects/example/multipleChoiceWizards.md"] input[value="Merlin"]`
).click();

cy.get(
`[test-player-id="${playerKeys[1]}"] [data-test="timer_start_0_end_10"]`
);

cy.wait(6000); // for testing timed render
cy.playerCanSee(playerKeys[0], "TestDisplay00"); // conditional on multipleChoice equalling Markdown
cy.playerCanNotSee(playerKeys[1], "TestDisplay00");
cy.playerCanSee(playerKeys[0], "TestDisplay01"); // timed display after 4 seconds
cy.playerCanNotSee(playerKeys[0], "TestDisplay02"); // hidden after 4 seconds

cy.submitPlayers(playerKeys.slice(0, 2)); // submit both completing players

Expand Down Expand Up @@ -718,6 +736,7 @@ describe(
"prompt_listSorterPrompt",
"prompt_individualOpenResponse",
"prompt_introOpenResponse",
"prompt_sharedMultipleChoiceWizards",
]);

// check that prompt correctly saves open response data
Expand Down Expand Up @@ -778,6 +797,14 @@ describe(

// check that the screen resolution and user agent are saved
expect(objs[1].browserInfo.width).to.be.greaterThan(0);

// check that we have data from the intro steps for all players that complete it
expect(objs[0]).to.have.property("surveys");
expect(objs[1]).to.have.property("surveys");
expect(objs[2]).to.have.property("surveys");
expect(
objs[2].surveys.survey_politicalPartyUS.responses.party
).to.equal("Republican");
});

// check for server-side errors
Expand All @@ -792,7 +819,7 @@ describe(
expect(errorLines[0]).to.include("Error test message from batch");
});

// check participant data saved
// load participant data
cy.readFile(
`../data/participantData/noWorkerIdGiven_${playerKeys[0]}.jsonl`
)
Expand All @@ -805,7 +832,6 @@ describe(
.as("participantObjects");

cy.get("@participantObjects").then((objs) => {
// check that prompt data is included for both individual and group prompts
expect(objs.filter((obj) => obj.key === "platformId")[0]?.val).to.equal(
`noWorkerIdGiven_${playerKeys[0]}`
);
Expand Down
30 changes: 18 additions & 12 deletions cypress/e2e/02_Batch_Canceled.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,15 @@ describe("Batch canceled", { retries: { runMode: 2, openMode: 0 } }, () => {
cy.contains("About this study").should("not.exist");

// Should boot to server error message
cy.get(`[test-player-id="${playerKeys[0]}"]`).contains("Server error", {
timeout: 10000,
});
cy.get(`[test-player-id="${playerKeys[0]}"]`).contains("cypressError", {
timeout: 10000,
});
cy.get(`[test-player-id="${playerKeys[0]}"]`).contains(
"experiment is now closed",
{
timeout: 10000,
}
);
// cy.get(`[test-player-id="${playerKeys[0]}"]`).contains("cypressError", {
// timeout: 10000,
// });
});

it("from game", () => {
Expand Down Expand Up @@ -150,12 +153,15 @@ describe("Batch canceled", { retries: { runMode: 2, openMode: 0 } }, () => {

// Should boot to server error message
cy.visit(`/?playerKey=${playerKeys[0]}`);
cy.get(`[test-player-id="${playerKeys[0]}"]`).contains("Server error", {
timeout: 10000,
});
cy.get(`[test-player-id="${playerKeys[0]}"]`).contains("cypressError", {
timeout: 10000,
});
cy.get(`[test-player-id="${playerKeys[0]}"]`).contains(
"experiment is now closed",
{
timeout: 10000,
}
);
// cy.get(`[test-player-id="${playerKeys[0]}"]`).contains("cypressError", {
// timeout: 10000,
// });

cy.wait(3000); // wait for batch close callbacks to complete
// load the data
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/10_Etherpad_Qualtrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe(
cy.empiricaStartBatch(1);
});

it("walks properly", () => {
it.skip("walks properly", () => {
const playerKeys = [`testplayer_${Math.floor(Math.random() * 1e13)}`];

cy.empiricaSetupWindow({ playerKeys });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ introSequences:
- reference: prompt.multipleChoiceIntroExample
comparator: equals
value: Markdown
- type: prompt
file: projects/example/testDisplay01.md
displayTime: 6
- type: prompt
file: projects/example/testDisplay02.md
hideTime: 6
- type: timer
endTime: 10
warnTimeRemaining: 4
- type: submitButton
name: introSubmitButton
buttonText: Continue
Expand Down
Loading

0 comments on commit a2a6835

Please sign in to comment.