Skip to content

Commit a652410

Browse files
committed
Integrate uniform timeline with replay controls
1 parent ec7181e commit a652410

3 files changed

Lines changed: 54 additions & 10 deletions

File tree

src/analysis/individualStudy/replay/SingleTask.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,24 @@ const ICON_GAP = 2;
2020

2121
function taskFill({
2222
incomplete,
23-
hasCorrect,
24-
isCorrect,
23+
answerStatus,
2524
}: {
2625
incomplete: boolean,
27-
hasCorrect: boolean,
28-
isCorrect: boolean,
26+
answerStatus: ComponentAnswerStatus | null,
2927
}) {
3028
if (incomplete) {
3129
return '#e9ecef';
3230
}
3331

34-
if (!hasCorrect) {
35-
return 'lightgray';
32+
if (answerStatus === 'correct') {
33+
return '#bfe8c6';
34+
}
35+
36+
if (answerStatus === 'incorrect') {
37+
return '#f3c1c1';
3638
}
3739

38-
return isCorrect ? '#bfe8c6' : '#f3c1c1';
40+
return 'lightgray';
3941
}
4042

4143
export function SingleTask({
@@ -114,7 +116,7 @@ export function SingleTask({
114116
>
115117
<rect
116118
opacity={isDimmed ? 0.45 : 1}
117-
fill={taskFill({ incomplete, hasCorrect, isCorrect })}
119+
fill={taskFill({ incomplete, answerStatus })}
118120
stroke={isHovered ? 'cornflowerblue' : undefined}
119121
strokeWidth={isHovered ? 3 : 0}
120122
x={xScale(scaleStart) + TASK_GAP}

src/analysis/individualStudy/replay/tests/AllTasksTimeline.spec.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { SingleTaskLabelLines } from '../SingleTaskLabelLines';
1616
// ── mocks ────────────────────────────────────────────────────────────────────
1717

1818
vi.mock('@mantine/core', () => ({
19+
Box: ({ children }: { children: ReactNode }) => <div>{children}</div>,
1920
Center: ({ children }: { children: ReactNode }) => <div>{children}</div>,
2021
Stack: ({ children }: { children: ReactNode }) => <div>{children}</div>,
2122
Tooltip: ({ children, label }: { children: ReactNode; label?: ReactNode }) => (
@@ -137,13 +138,15 @@ describe('SingleTask', () => {
137138
<svg><SingleTask {...baseProps} answerStatus="correct" /></svg>,
138139
);
139140
expect(html).toContain('icon-check');
141+
expect(html).toContain('fill="#bfe8c6"');
140142
});
141143

142144
test('shows x icon for an incorrect response', () => {
143145
const html = renderToStaticMarkup(
144146
<svg><SingleTask {...baseProps} answerStatus="incorrect" /></svg>,
145147
);
146148
expect(html).toContain('icon-x');
149+
expect(html).toContain('fill="#f3c1c1"');
147150
});
148151

149152
test('shows an accessible grey checkmark for an unknown response', () => {
@@ -153,6 +156,7 @@ describe('SingleTask', () => {
153156
expect(html).toContain('icon-check');
154157
expect(html).toContain('var(--mantine-color-gray-6)');
155158
expect(html).toContain('Response recorded; correctness not configured.');
159+
expect(html).toContain('fill="lightgray"');
156160
});
157161

158162
test('shows microphone icon when hasAudio', () => {
@@ -277,6 +281,39 @@ describe('AllTasksTimeline', () => {
277281
expect(html).toContain('<rect');
278282
});
279283

284+
test('uses equal-width task slots and omits browsed-away intervals in uniform mode', () => {
285+
const participant = makeParticipant({
286+
answers: {
287+
trial1_0: makeAnswer({
288+
windowEvents: [
289+
[t0 + 1_000, 'visibility', 'hidden'],
290+
[t0 + 3_000, 'visibility', 'visible'],
291+
],
292+
}),
293+
trial2_1: makeAnswer({
294+
componentName: 'trial2',
295+
trialOrder: '1_0',
296+
startTime: t0 + 20_000,
297+
endTime: t0 + 40_000,
298+
}),
299+
},
300+
});
301+
302+
const html = renderToStaticMarkup(
303+
<AllTasksTimeline
304+
participantData={participant}
305+
width={600}
306+
studyId="test-study"
307+
studyConfig={emptyConfig}
308+
maxLength={undefined}
309+
timelineMode="uniform"
310+
/>,
311+
);
312+
313+
expect(html).toContain('width:136px');
314+
expect(html).not.toContain('Browsed away');
315+
});
316+
280317
test('handles all tracked window event types without error', () => {
281318
const participant = makeParticipant({
282319
answers: {

src/analysis/individualStudy/table/tests/TableView.spec.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ vi.mock('@tabler/icons-react', () => ({
5858
}));
5959

6060
vi.mock('../../replay/AllTasksTimeline', () => ({
61-
AllTasksTimeline: ({ taskOrder }: { taskOrder: string }) => <div data-task-order={taskOrder}>AllTasksTimeline</div>,
61+
AllTasksTimeline: ({ taskOrder, timelineMode }: { taskOrder: string; timelineMode: string }) => (
62+
<div data-task-order={taskOrder} data-timeline-mode={timelineMode}>AllTasksTimeline</div>
63+
),
6264
}));
6365

6466
vi.mock('../../ParticipantRejectModal', () => ({
@@ -142,16 +144,19 @@ describe('TableView', () => {
142144
expect(html).toContain('Order');
143145
expect(html).toContain('Sequence');
144146
expect(html).toContain('Answer time');
147+
expect(html).toContain('Time');
148+
expect(html).toContain('Uniform');
145149
});
146150

147-
test('uses sequence ordering by default for participant timelines', () => {
151+
test('uses sequence ordering and time sizing by default for participant timelines', () => {
148152
const participant = makeParticipant();
149153
renderToStaticMarkup(
150154
<TableView {...defaultProps} visibleParticipants={[participant]} />,
151155
);
152156

153157
const html = renderToStaticMarkup(capturedTableOptions!.renderDetailPanel({ row: { original: participant } }));
154158
expect(html).toContain('data-task-order="sequence"');
159+
expect(html).toContain('data-timeline-mode="time"');
155160
});
156161

157162
// ── Status column ──────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)