Skip to content

Commit f365a19

Browse files
feat: add conditions support to discussion block (#1222)
* feat: add conditions support to discussion block (#1221) - Add optional `conditions` field to discussion schema, reusing existing condition infrastructure (ConditionsConditionalRender) - Refactor Discussion component to accept config object instead of individual props, eliminating prop drilling from Stage.jsx - Add `fallback` prop to ConditionsConditionalRender so discussion conditions can fall back to single-column layout - Rename shouldShowDiscussion → positionAllowsDiscussion for clarity - Add Cypress e2e test covering: condition met (chat visible), condition not met (single-column fallback), and position-hidden cases - Add 5 preflight validation tests for discussion conditions schema Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: compartmentalize test 17 fixtures into dedicated directory Move discussion conditions test fixtures into test/discussionConditions/ with its own config, treatment YAML, and prompt files — matching the pattern used by test 16. Revert the treatment addition from the shared cypress.treatments.yaml. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: reuse shared conditionsSchema for discussion conditions Use z.lazy(() => conditionsSchema) instead of duplicating the array schema inline, ensuring discussion conditions get the same altTemplateContext support and error messages as element conditions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 451e212 commit f365a19

12 files changed

Lines changed: 355 additions & 36 deletions

File tree

client/src/Stage.jsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function Stage() {
2424
const elements = stage?.get("elements") || [];
2525
const playerPosition = player?.get("position");
2626

27-
const shouldShowDiscussion = useMemo(() => {
27+
const positionAllowsDiscussion = useMemo(() => {
2828
if (!discussion) return false;
2929

3030
const numericPosition = parseInt(playerPosition);
@@ -105,20 +105,7 @@ export function Stage() {
105105

106106
<div className="flex h-full w-full flex-col gap-4 pb-4 md:flex-row md:items-stretch md:px-6 md:min-h-[calc(100vh-4rem)]">
107107
<div className="w-full md:flex-1 md:min-w-[24rem]">
108-
<Discussion
109-
chatType={discussion.chatType}
110-
showNickname={discussion.showNickname ?? true}
111-
showTitle={discussion.showTitle}
112-
showSelfView={discussion.showSelfView ?? true}
113-
showReportMissing={discussion.showReportMissing ?? true}
114-
showAudioMute={discussion.showAudioMute ?? true}
115-
showVideoMute={discussion.showVideoMute ?? true}
116-
layout={discussion.layout}
117-
rooms={discussion.rooms}
118-
reactionEmojisAvailable={discussion.reactionEmojisAvailable || []}
119-
reactToSelf={discussion.reactToSelf ?? true}
120-
numReactionsPerMessage={discussion.numReactionsPerMessage ?? 1}
121-
/>
108+
<Discussion discussion={discussion} />
122109
</div>
123110

124111
<div
@@ -145,8 +132,15 @@ export function Stage() {
145132
return (
146133
<StageProgressLabelProvider>
147134
<SubmissionConditionalRender>
148-
{shouldShowDiscussion && renderDiscussionPage()}
149-
{!shouldShowDiscussion && renderNoDiscussionPage()}
135+
{positionAllowsDiscussion && (
136+
<ConditionsConditionalRender
137+
conditions={discussion?.conditions}
138+
fallback={renderNoDiscussionPage()}
139+
>
140+
{renderDiscussionPage()}
141+
</ConditionsConditionalRender>
142+
)}
143+
{!positionAllowsDiscussion && renderNoDiscussionPage()}
150144
</SubmissionConditionalRender>
151145
</StageProgressLabelProvider>
152146
);

client/src/components/ConditionalRender.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,16 @@ export function PositionConditionalRender({
8585
return children;
8686
}
8787

88-
export function ConditionsConditionalRender({ conditions, children }) {
88+
export function ConditionsConditionalRender({ conditions, children, fallback = null }) {
8989
if (!conditions || !conditions.length) return children;
9090
return (
91-
<RecursiveConditionalRender conditions={conditions}>
91+
<RecursiveConditionalRender conditions={conditions} fallback={fallback}>
9292
{children}
9393
</RecursiveConditionalRender>
9494
);
9595
}
9696

97-
function RecursiveConditionalRender({ conditions, children }) {
97+
function RecursiveConditionalRender({ conditions, children, fallback = null }) {
9898
// only do one condition at a time, nesting these components,
9999
// so that we only need to get one reference in each component,
100100
// and can obey the rules for hooks. (ie, can't short-circuit before getting the reference)
@@ -146,12 +146,12 @@ function RecursiveConditionalRender({ conditions, children }) {
146146
);
147147
}
148148

149-
if (!conditionMet) return null;
149+
if (!conditionMet) return fallback;
150150

151151
if (conditions.length === 1) return children; // this is the only condition, and it passed
152152

153153
return (
154-
<RecursiveConditionalRender conditions={conditions.slice(1)}>
154+
<RecursiveConditionalRender conditions={conditions.slice(1)} fallback={fallback}>
155155
{children}
156156
</RecursiveConditionalRender>
157157
);

client/src/elements/Discussion.jsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,22 @@ import { ReportMissingProvider } from "../call/ReportMissing";
66
import { useIdleContext } from "../components/IdleProvider";
77
import { VideoCall } from "../call/VideoCall";
88

9-
export function Discussion({
10-
chatType,
11-
showNickname,
12-
showTitle,
13-
showSelfView = true,
14-
showReportMissing = true,
15-
showAudioMute = true,
16-
showVideoMute = true,
17-
layout,
18-
rooms,
19-
reactionEmojisAvailable,
20-
reactToSelf,
21-
numReactionsPerMessage,
22-
}) {
9+
export function Discussion({ discussion }) {
10+
const {
11+
chatType,
12+
showNickname = true,
13+
showTitle,
14+
showSelfView = true,
15+
showReportMissing = true,
16+
showAudioMute = true,
17+
showVideoMute = true,
18+
layout,
19+
rooms,
20+
reactionEmojisAvailable = [],
21+
reactToSelf = true,
22+
numReactionsPerMessage = 1,
23+
} = discussion;
24+
2325
const stage = useStage();
2426
const { setAllowIdle } = useIdleContext();
2527

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const batchConfigFixture = require("../fixtures/mockCDN/test/discussionConditions/test.config.json");
2+
3+
describe(
4+
"Discussion Conditions",
5+
{ retries: { runMode: 2, openMode: 0 } },
6+
() => {
7+
beforeEach(() => {
8+
cy.empiricaClearBatches();
9+
10+
const config = {
11+
...batchConfigFixture,
12+
};
13+
14+
cy.empiricaCreateCustomBatch(JSON.stringify(config), {});
15+
cy.wait(3000);
16+
cy.empiricaStartBatch(1);
17+
});
18+
19+
it("shows or hides discussion based on conditions and position", () => {
20+
const playerKeys = [
21+
`discCond_A_${Math.floor(Math.random() * 1e13)}`,
22+
`discCond_B_${Math.floor(Math.random() * 1e13)}`,
23+
];
24+
const hitId = "discussionConditionsHIT";
25+
26+
cy.empiricaSetupWindow({ playerKeys, hitId });
27+
cy.interceptIpApis();
28+
29+
// Intro steps
30+
playerKeys.forEach((playerKey) => {
31+
cy.stepIntro(playerKey);
32+
});
33+
34+
playerKeys.forEach((playerKey) => {
35+
cy.stepConsent(playerKey);
36+
cy.stepAttentionCheck(playerKey);
37+
cy.stepVideoCheck(playerKey, {
38+
setupMicrophone: false,
39+
setupCamera: false,
40+
});
41+
cy.stepNickname(playerKey);
42+
});
43+
44+
// Lobby
45+
playerKeys.forEach((playerKey) => {
46+
cy.waitForGameLoad(playerKey);
47+
});
48+
49+
// ===== Stage 1: Setup Choice =====
50+
// Both players select "HTML" so the condition `equals HTML, position: all` will be true
51+
playerKeys.forEach((playerKey) => {
52+
cy.playerCanSee(playerKey, "Setup Choice");
53+
cy.get(
54+
`[data-player-id="${playerKey}"] [data-test="test/discussionConditions/setupChoice.md"] input[value="HTML"]`
55+
).click();
56+
});
57+
cy.submitPlayers(playerKeys);
58+
59+
// ===== Stage 2: Discussion Condition Met =====
60+
// Condition: prompt.setupChoice equals HTML (position: all) → TRUE
61+
// Discussion should be visible (two-column layout with text chat)
62+
playerKeys.forEach((playerKey) => {
63+
cy.playerCanSee(playerKey, "Discussion Condition Met");
64+
cy.get(`[data-player-id="${playerKey}"] [data-test="discussion"]`).should(
65+
"be.visible"
66+
);
67+
// The no-discussion single-column layout should NOT be present
68+
cy.get(
69+
`[data-player-id="${playerKey}"] [data-test="stageContent"]`
70+
).should("not.exist");
71+
});
72+
73+
// Verify text chat works
74+
cy.typeInChat(playerKeys[0], "Hello from player A");
75+
cy.get(`[data-player-id="${playerKeys[1]}"]`).contains(
76+
"Hello from player A"
77+
);
78+
79+
cy.submitPlayers(playerKeys);
80+
81+
// ===== Stage 3: Discussion Condition Not Met =====
82+
// Condition: prompt.setupChoice equals Markdown (position: all) → FALSE
83+
// Discussion should be hidden, single-column layout should render
84+
playerKeys.forEach((playerKey) => {
85+
cy.playerCanSee(playerKey, "Discussion Condition Not Met");
86+
// Discussion panel should NOT exist
87+
cy.get(
88+
`[data-player-id="${playerKey}"] [data-test="discussion"]`
89+
).should("not.exist");
90+
// Single-column layout should be present
91+
cy.get(
92+
`[data-player-id="${playerKey}"] [data-test="stageContent"]`
93+
).should("be.visible");
94+
});
95+
cy.submitPlayers(playerKeys);
96+
97+
// ===== Stage 4: Discussion Position Hidden =====
98+
// hideFromPositions: [0, 1] hides from both players → single-column layout
99+
playerKeys.forEach((playerKey) => {
100+
cy.playerCanSee(playerKey, "Discussion Position Hidden");
101+
cy.get(
102+
`[data-player-id="${playerKey}"] [data-test="discussion"]`
103+
).should("not.exist");
104+
cy.get(
105+
`[data-player-id="${playerKey}"] [data-test="stageContent"]`
106+
).should("be.visible");
107+
});
108+
cy.submitPlayers(playerKeys);
109+
110+
// Exit
111+
playerKeys.forEach((playerKey) => {
112+
cy.stepQCSurvey(playerKey);
113+
});
114+
});
115+
}
116+
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
name: test/discussionConditions/conditionMetMessage.md
3+
type: noResponse
4+
---
5+
6+
# Discussion Condition Met
7+
8+
The discussion panel should be visible alongside this content.
9+
10+
---
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
name: test/discussionConditions/conditionNotMetMessage.md
3+
type: noResponse
4+
---
5+
6+
# Discussion Condition Not Met
7+
8+
The discussion panel should be hidden and this should render in single-column layout.
9+
10+
---
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
name: test/discussionConditions/positionHiddenMessage.md
3+
type: noResponse
4+
---
5+
6+
# Discussion Position Hidden
7+
8+
The discussion panel should be hidden via hideFromPositions.
9+
10+
---
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
name: test/discussionConditions/setupChoice.md
3+
type: multipleChoice
4+
---
5+
6+
# Setup Choice
7+
8+
Pick a format to set up the condition for the next stages.
9+
10+
---
11+
12+
- Markdown
13+
- HTML
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"batchName": "cytest_discussion_conditions",
3+
"cdn": "test",
4+
"treatmentFile": "test/discussionConditions/testDiscussionConditions.treatments.yaml",
5+
"customIdInstructions": "none",
6+
"platformConsent": "US",
7+
"consentAddendum": "none",
8+
"debrief": "none",
9+
"checkAudio": false,
10+
"checkVideo": false,
11+
"introSequence": "none",
12+
"treatments": ["cypress_discussion_conditions"],
13+
"payoffs": "equal",
14+
"knockdowns": "none",
15+
"dispatchWait": 1,
16+
"launchDate": "immediate",
17+
"centralPrereg": false,
18+
"preregRepos": [],
19+
"dataRepos": [],
20+
"videoStorage": "none",
21+
"exitCodes": "none"
22+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
treatments:
2+
- name: cypress_discussion_conditions
3+
desc: Test discussion conditions and position filtering
4+
playerCount: 2
5+
gameStages:
6+
- name: Setup Choice
7+
duration: 3600
8+
elements:
9+
- type: prompt
10+
file: test/discussionConditions/setupChoice.md
11+
name: setupChoice
12+
- type: submitButton
13+
14+
- name: Discussion Condition Met
15+
duration: 3600
16+
discussion:
17+
chatType: text
18+
showNickname: true
19+
showTitle: true
20+
conditions:
21+
- reference: prompt.setupChoice
22+
comparator: equals
23+
position: all
24+
value: HTML
25+
elements:
26+
- type: prompt
27+
file: test/discussionConditions/conditionMetMessage.md
28+
- type: submitButton
29+
30+
- name: Discussion Condition Not Met
31+
duration: 3600
32+
discussion:
33+
chatType: text
34+
showNickname: true
35+
showTitle: true
36+
conditions:
37+
- reference: prompt.setupChoice
38+
comparator: equals
39+
position: all
40+
value: Markdown
41+
elements:
42+
- type: prompt
43+
file: test/discussionConditions/conditionNotMetMessage.md
44+
- type: submitButton
45+
46+
- name: Discussion Position Hidden
47+
duration: 3600
48+
discussion:
49+
chatType: text
50+
showNickname: true
51+
showTitle: true
52+
hideFromPositions: [0, 1]
53+
elements:
54+
- type: prompt
55+
file: test/discussionConditions/positionHiddenMessage.md
56+
- type: submitButton

0 commit comments

Comments
 (0)