Skip to content

Commit 6dd8595

Browse files
authored
Merge pull request #64 from coder13/participation-condition
Refine participation-condition advancement and linked round UI
2 parents a475d4c + cb09e3e commit 6dd8595

12 files changed

Lines changed: 1044 additions & 64 deletions

File tree

src/components/CutoffTimeLimitPanel/CutoffTimeLimitPanel.stories.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react';
22
import {
33
getStorybookRoundFixture,
44
makeStorybookCompetitionFixtureWithRound,
5+
storybookParticipationConditionLinkedRoundsFixture,
6+
storybookParticipationConditionPercentFixture,
57
} from '@/storybook/competitionFixtures';
68
import { makeCompetitionContainerDecorator } from '@/storybook/competitionStorybook';
79
import { CutoffTimeLimitPanel } from './CutoffTimeLimitPanel';
@@ -53,6 +55,33 @@ export const RankingAdvancement: Story = {
5355
},
5456
};
5557

58+
export const ParticipationConditionPercent: Story = {
59+
parameters: {
60+
competitionFixture: storybookParticipationConditionPercentFixture,
61+
},
62+
args: {
63+
round: storybookParticipationConditionPercentFixture.events[0].rounds[0],
64+
},
65+
};
66+
67+
export const ParticipationConditionLinkedRounds: Story = {
68+
parameters: {
69+
competitionFixture: storybookParticipationConditionLinkedRoundsFixture,
70+
},
71+
args: {
72+
round: storybookParticipationConditionLinkedRoundsFixture.events[0].rounds[1],
73+
},
74+
};
75+
76+
export const ParticipationConditionLinkedRoundsStart: Story = {
77+
parameters: {
78+
competitionFixture: storybookParticipationConditionLinkedRoundsFixture,
79+
},
80+
args: {
81+
round: storybookParticipationConditionLinkedRoundsFixture.events[0].rounds[0],
82+
},
83+
};
84+
5685
export const CutoffAndTimeLimit: Story = {
5786
args: {
5887
round: getStorybookRoundFixture('222-r1'),

src/components/CutoffTimeLimitPanel/CutoffTimeLimitPanel.test.tsx

Lines changed: 208 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,196 @@ jest.mock('react-tiny-popover', () => ({
2222
}));
2323

2424
jest.mock('react-i18next', () => ({
25-
Trans: ({ i18nKey }: { i18nKey: string }) => i18nKey,
25+
Trans: ({ i18nKey, values }: { i18nKey: string; values?: Record<string, unknown> }) => {
26+
if (i18nKey === 'common.wca.advancement.ranking') {
27+
return `Top ${values?.level} to next round`;
28+
}
29+
30+
if (i18nKey === 'common.wca.advancement.percent') {
31+
return `Top ${values?.level}% to next round`;
32+
}
33+
34+
if (i18nKey === 'common.wca.advancement.linkedRanking') {
35+
return `Top ${values?.level} combined across ${values?.rounds} advance to next round`;
36+
}
37+
38+
if (i18nKey === 'common.wca.advancement.linkedPercent') {
39+
return `Top ${values?.level}% combined across ${values?.rounds} advance to next round`;
40+
}
41+
42+
if (i18nKey === 'common.wca.cumulativeTimelimit') {
43+
return `Time Limit: ${values?.time} Cumulative`;
44+
}
45+
46+
if (i18nKey === 'common.wca.cumulativeTimelimitWithrounds') {
47+
return `Time Limit: ${values?.time} Total with:`;
48+
}
49+
50+
return i18nKey;
51+
},
2652
useTranslation: () => ({
27-
t: (key: string) => key,
53+
t: (key: string, options?: Record<string, unknown>) => {
54+
if (key === 'common.help') {
55+
return 'help';
56+
}
57+
58+
if (key === 'common.wca.cutoff') {
59+
return 'Cutoff';
60+
}
61+
62+
if (key === 'common.wca.timeLimit') {
63+
return 'Time Limit';
64+
}
65+
66+
if (key === 'common.activityCodeToName.round') {
67+
return `Round ${options?.roundNumber}`;
68+
}
69+
70+
if (key === 'common.wca.advancement.ranking') {
71+
return `Top ${options?.level} advance to ${options?.what}`;
72+
}
73+
74+
if (key === 'common.wca.advancement.percent') {
75+
return `Top ${options?.level}% advance to ${options?.what}`;
76+
}
77+
78+
if (key === 'common.wca.advancement.linkedRanking') {
79+
return `Top ${options?.level} in dual rounds ${options?.rounds} advance to ${options?.what}`;
80+
}
81+
82+
if (key === 'common.wca.advancement.linkedPercent') {
83+
return `Top ${options?.level}% in dual rounds ${options?.rounds} advance to ${options?.what}`;
84+
}
85+
86+
if (key === 'common.wca.advancement.nextRound') {
87+
return 'next round';
88+
}
89+
90+
if (key === 'common.wca.advancement.final') {
91+
return 'final';
92+
}
93+
94+
if (key === 'common.wca.advancement.resultThresholdUnknown') {
95+
return 'an unknown result';
96+
}
97+
98+
if (options?.defaultValue) {
99+
return String(options.defaultValue)
100+
.replace('{{level}}', String(options.level ?? ''))
101+
.replace('{{rounds}}', String(options.rounds ?? ''))
102+
.replace('{{round}}', String(options.round ?? ''))
103+
.replace('{{what}}', String(options.what ?? ''))
104+
.replace('{{scope}}', String(options.scope ?? ''))
105+
.replace('{{result}}', String(options.result ?? ''));
106+
}
107+
108+
return key;
109+
},
28110
}),
29111
}));
30112

113+
const wcifMock = {
114+
id: 'TestComp2026',
115+
schedule: { venues: [] },
116+
events: [
117+
{
118+
id: '333',
119+
rounds: [
120+
{
121+
id: '333-r1',
122+
format: 'a',
123+
cutoff: null,
124+
timeLimit: null,
125+
advancementCondition: {
126+
type: 'ranking',
127+
level: 16,
128+
},
129+
results: [],
130+
},
131+
{
132+
id: '333-r2',
133+
format: 'a',
134+
cutoff: null,
135+
timeLimit: null,
136+
participationRuleset: {
137+
participationSource: {
138+
type: 'round',
139+
roundId: '333-r1',
140+
resultCondition: {
141+
type: 'percent',
142+
value: 75,
143+
},
144+
},
145+
},
146+
results: [],
147+
},
148+
{
149+
id: '333-r3',
150+
format: 'a',
151+
cutoff: null,
152+
timeLimit: null,
153+
participationRuleset: {
154+
participationSource: {
155+
type: 'linkedRounds',
156+
roundIds: ['333-r1', '333-r2'],
157+
resultCondition: {
158+
type: 'ranking',
159+
value: 12,
160+
},
161+
},
162+
},
163+
results: [],
164+
},
165+
],
166+
},
167+
{
168+
id: '222',
169+
rounds: [
170+
{
171+
id: '222-r1',
172+
format: 'a',
173+
cutoff: null,
174+
timeLimit: null,
175+
results: [],
176+
},
177+
{
178+
id: '222-r2',
179+
format: 'a',
180+
cutoff: null,
181+
timeLimit: null,
182+
results: [],
183+
},
184+
{
185+
id: '222-r3',
186+
format: 'a',
187+
cutoff: null,
188+
timeLimit: null,
189+
participationRuleset: {
190+
participationSource: {
191+
type: 'linkedRounds',
192+
roundIds: ['222-r1', '222-r2'],
193+
resultCondition: {
194+
type: 'ranking',
195+
value: 8,
196+
},
197+
},
198+
},
199+
results: [],
200+
},
201+
],
202+
},
203+
],
204+
};
205+
31206
jest.mock('@/providers/WCIFProvider', () => ({
32207
useWCIF: () => ({
33208
competitionId: 'TestComp2026',
34-
wcif: {
35-
id: 'TestComp2026',
36-
schedule: { venues: [] },
37-
},
209+
wcif: wcifMock,
38210
setTitle: () => {},
39211
}),
40212
}));
41213

42-
const round = {
214+
const cutoffOnlyRound = {
43215
id: '333-r1',
44216
cutoff: {
45217
numberOfAttempts: 2,
@@ -49,7 +221,7 @@ const round = {
49221
advancementCondition: null,
50222
} as unknown as Round;
51223

52-
function renderPanel() {
224+
function renderPanel(round: Round) {
53225
return render(
54226
<MemoryRouter>
55227
<CutoffTimeLimitPanel round={round} />
@@ -59,7 +231,7 @@ function renderPanel() {
59231

60232
describe('CutoffTimeLimitPanel', () => {
61233
it('uses theme-aware popover classes for help content', () => {
62-
renderPanel();
234+
renderPanel(cutoffOnlyRound);
63235

64236
fireEvent.click(screen.getByRole('button', { name: /help/i }));
65237

@@ -71,4 +243,31 @@ describe('CutoffTimeLimitPanel', () => {
71243
expect(popoverContent).toHaveClass('text-default');
72244
expect(popoverContent).not.toHaveClass('bg-white');
73245
});
246+
247+
it('shows the legacy advancement text for stable wcif rounds', () => {
248+
renderPanel(wcifMock.events[0].rounds[0] as unknown as Round);
249+
250+
expect(screen.getByText('Top 16 advance to next round')).toBeInTheDocument();
251+
});
252+
253+
it('shows advancement text derived from the next round participation ruleset', () => {
254+
renderPanel({
255+
...(wcifMock.events[0].rounds[0] as object),
256+
advancementCondition: null,
257+
} as Round);
258+
259+
expect(screen.getByText('Top 75% advance to next round')).toBeInTheDocument();
260+
});
261+
262+
it('shows linked-round advancement text when a later round depends on combined results', () => {
263+
renderPanel(wcifMock.events[0].rounds[1] as unknown as Round);
264+
265+
expect(screen.getByText('Top 12 in dual rounds 1 & 2 advance to final')).toBeInTheDocument();
266+
});
267+
268+
it('shows the same dual-round advancement text for the first round in a linked-round set', () => {
269+
renderPanel(wcifMock.events[1].rounds[0] as unknown as Round);
270+
271+
expect(screen.getByText('Top 8 in dual rounds 1 & 2 advance to final')).toBeInTheDocument();
272+
});
74273
});

0 commit comments

Comments
 (0)