Skip to content

Commit 062aebb

Browse files
committed
implement proposals sorting [ci]
Signed-off-by: Paweł Perek <pawel.perek@digitalasset.com>
1 parent 6b82685 commit 062aebb

File tree

4 files changed

+420
-25
lines changed

4 files changed

+420
-25
lines changed
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { render, screen } from '@testing-library/react';
4+
import { describe, expect, test } from 'vitest';
5+
import { MemoryRouter } from 'react-router-dom';
6+
import { ContractId } from '@daml/types';
7+
import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules';
8+
import {
9+
ActionRequiredSection,
10+
ActionRequiredData,
11+
} from '../../components/governance/ActionRequiredSection';
12+
import { ProposalListingSection } from '../../components/governance/ProposalListingSection';
13+
import { ProposalListingData } from '../../utils/types';
14+
15+
describe('Governance Page Sorting', () => {
16+
describe('Action Required Section', () => {
17+
test('should render items sorted by voting closes date ascending (closest deadline first)', () => {
18+
// Pass data intentionally in unsorted order
19+
const unsortedRequests: ActionRequiredData[] = [
20+
{
21+
actionName: 'Action C - Latest deadline',
22+
contractId: 'c' as ContractId<VoteRequest>,
23+
votingCloses: '2025-01-25 12:00',
24+
createdAt: '2025-01-10 12:00',
25+
requester: 'sv1',
26+
},
27+
{
28+
actionName: 'Action A - Earliest deadline',
29+
contractId: 'a' as ContractId<VoteRequest>,
30+
votingCloses: '2025-01-15 12:00',
31+
createdAt: '2025-01-10 12:00',
32+
requester: 'sv1',
33+
},
34+
{
35+
actionName: 'Action B - Middle deadline',
36+
contractId: 'b' as ContractId<VoteRequest>,
37+
votingCloses: '2025-01-20 12:00',
38+
createdAt: '2025-01-10 12:00',
39+
requester: 'sv1',
40+
},
41+
];
42+
43+
render(
44+
<MemoryRouter>
45+
<ActionRequiredSection actionRequiredRequests={unsortedRequests} />
46+
</MemoryRouter>
47+
);
48+
49+
const cards = screen.getAllByTestId('action-required-card');
50+
expect(cards).toHaveLength(3);
51+
52+
// Verify order: earliest deadline first
53+
const actionNames = cards.map(
54+
card => card.querySelector('[data-testid="action-required-action-content"]')?.textContent
55+
);
56+
expect(actionNames[0]).toBe('Action A - Earliest deadline');
57+
expect(actionNames[1]).toBe('Action B - Middle deadline');
58+
expect(actionNames[2]).toBe('Action C - Latest deadline');
59+
});
60+
61+
test('should handle same-day deadlines with different times', () => {
62+
const unsortedRequests: ActionRequiredData[] = [
63+
{
64+
actionName: 'Action B - Later time',
65+
contractId: 'b' as ContractId<VoteRequest>,
66+
votingCloses: '2025-01-15 18:00',
67+
createdAt: '2025-01-10 12:00',
68+
requester: 'sv1',
69+
},
70+
{
71+
actionName: 'Action A - Earlier time',
72+
contractId: 'a' as ContractId<VoteRequest>,
73+
votingCloses: '2025-01-15 10:00',
74+
createdAt: '2025-01-10 12:00',
75+
requester: 'sv1',
76+
},
77+
];
78+
79+
render(
80+
<MemoryRouter>
81+
<ActionRequiredSection actionRequiredRequests={unsortedRequests} />
82+
</MemoryRouter>
83+
);
84+
85+
const cards = screen.getAllByTestId('action-required-card');
86+
const actionNames = cards.map(
87+
card => card.querySelector('[data-testid="action-required-action-content"]')?.textContent
88+
);
89+
expect(actionNames[0]).toBe('Action A - Earlier time');
90+
expect(actionNames[1]).toBe('Action B - Later time');
91+
});
92+
});
93+
94+
describe('Inflight Votes Section', () => {
95+
const baseData: Omit<
96+
ProposalListingData,
97+
'actionName' | 'contractId' | 'voteTakesEffect' | 'votingThresholdDeadline'
98+
> = {
99+
yourVote: 'accepted',
100+
status: 'In Progress',
101+
voteStats: { accepted: 5, rejected: 2, 'no-vote': 1 },
102+
acceptanceThreshold: BigInt(11),
103+
};
104+
105+
test('should render items sorted by effective date ascending (closest first)', () => {
106+
// Pass data intentionally in unsorted order
107+
const unsortedRequests: ProposalListingData[] = [
108+
{
109+
...baseData,
110+
actionName: 'Action C - Latest effective',
111+
contractId: 'c' as ContractId<VoteRequest>,
112+
voteTakesEffect: '2025-01-30 12:00',
113+
votingThresholdDeadline: '2025-01-25 12:00',
114+
},
115+
{
116+
...baseData,
117+
actionName: 'Action A - Earliest effective',
118+
contractId: 'a' as ContractId<VoteRequest>,
119+
voteTakesEffect: '2025-01-20 12:00',
120+
votingThresholdDeadline: '2025-01-15 12:00',
121+
},
122+
{
123+
...baseData,
124+
actionName: 'Action B - Middle effective',
125+
contractId: 'b' as ContractId<VoteRequest>,
126+
voteTakesEffect: '2025-01-25 12:00',
127+
votingThresholdDeadline: '2025-01-20 12:00',
128+
},
129+
];
130+
131+
render(
132+
<MemoryRouter>
133+
<ProposalListingSection
134+
sectionTitle="Inflight Votes"
135+
data={unsortedRequests}
136+
noDataMessage="No data"
137+
uniqueId="inflight-votes"
138+
showVoteStats
139+
showAcceptanceThreshold
140+
showThresholdDeadline
141+
sortOrder="effectiveAtAsc"
142+
/>
143+
</MemoryRouter>
144+
);
145+
146+
const rows = screen.getAllByTestId('inflight-votes-row');
147+
expect(rows).toHaveLength(3);
148+
149+
const actionNames = rows.map(
150+
row => row.querySelector('[data-testid="inflight-votes-row-action-name"]')?.textContent
151+
);
152+
expect(actionNames[0]).toBe('Action A - Earliest effective');
153+
expect(actionNames[1]).toBe('Action B - Middle effective');
154+
expect(actionNames[2]).toBe('Action C - Latest effective');
155+
});
156+
157+
test('should place Threshold items after items with specific effective dates', () => {
158+
const unsortedRequests: ProposalListingData[] = [
159+
{
160+
...baseData,
161+
actionName: 'Threshold Action',
162+
contractId: 't' as ContractId<VoteRequest>,
163+
voteTakesEffect: 'Threshold',
164+
votingThresholdDeadline: '2025-01-15 12:00',
165+
},
166+
{
167+
...baseData,
168+
actionName: 'Dated Action',
169+
contractId: 'd' as ContractId<VoteRequest>,
170+
voteTakesEffect: '2025-01-30 12:00',
171+
votingThresholdDeadline: '2025-01-25 12:00',
172+
},
173+
];
174+
175+
render(
176+
<MemoryRouter>
177+
<ProposalListingSection
178+
sectionTitle="Inflight Votes"
179+
data={unsortedRequests}
180+
noDataMessage="No data"
181+
uniqueId="inflight-votes"
182+
showVoteStats
183+
showAcceptanceThreshold
184+
showThresholdDeadline
185+
sortOrder="effectiveAtAsc"
186+
/>
187+
</MemoryRouter>
188+
);
189+
190+
const rows = screen.getAllByTestId('inflight-votes-row');
191+
const actionNames = rows.map(
192+
row => row.querySelector('[data-testid="inflight-votes-row-action-name"]')?.textContent
193+
);
194+
195+
// Dated items should come before Threshold items
196+
expect(actionNames[0]).toBe('Dated Action');
197+
expect(actionNames[1]).toBe('Threshold Action');
198+
});
199+
200+
test('should sort multiple Threshold items by voting deadline', () => {
201+
const unsortedRequests: ProposalListingData[] = [
202+
{
203+
...baseData,
204+
actionName: 'Threshold C - Latest deadline',
205+
contractId: 'c' as ContractId<VoteRequest>,
206+
voteTakesEffect: 'Threshold',
207+
votingThresholdDeadline: '2025-01-25 12:00',
208+
},
209+
{
210+
...baseData,
211+
actionName: 'Threshold A - Earliest deadline',
212+
contractId: 'a' as ContractId<VoteRequest>,
213+
voteTakesEffect: 'Threshold',
214+
votingThresholdDeadline: '2025-01-15 12:00',
215+
},
216+
{
217+
...baseData,
218+
actionName: 'Threshold B - Middle deadline',
219+
contractId: 'b' as ContractId<VoteRequest>,
220+
voteTakesEffect: 'Threshold',
221+
votingThresholdDeadline: '2025-01-20 12:00',
222+
},
223+
];
224+
225+
render(
226+
<MemoryRouter>
227+
<ProposalListingSection
228+
sectionTitle="Inflight Votes"
229+
data={unsortedRequests}
230+
noDataMessage="No data"
231+
uniqueId="inflight-votes"
232+
showVoteStats
233+
showAcceptanceThreshold
234+
showThresholdDeadline
235+
sortOrder="effectiveAtAsc"
236+
/>
237+
</MemoryRouter>
238+
);
239+
240+
const rows = screen.getAllByTestId('inflight-votes-row');
241+
const actionNames = rows.map(
242+
row => row.querySelector('[data-testid="inflight-votes-row-action-name"]')?.textContent
243+
);
244+
245+
expect(actionNames[0]).toBe('Threshold A - Earliest deadline');
246+
expect(actionNames[1]).toBe('Threshold B - Middle deadline');
247+
expect(actionNames[2]).toBe('Threshold C - Latest deadline');
248+
});
249+
});
250+
251+
describe('Vote History Section', () => {
252+
const baseData: Omit<
253+
ProposalListingData,
254+
'actionName' | 'contractId' | 'voteTakesEffect' | 'votingThresholdDeadline'
255+
> = {
256+
yourVote: 'accepted',
257+
status: 'Implemented',
258+
voteStats: { accepted: 8, rejected: 2, 'no-vote': 1 },
259+
acceptanceThreshold: BigInt(11),
260+
};
261+
262+
test('should render items sorted by effective date descending (most recent first)', () => {
263+
// Pass data intentionally in unsorted order
264+
const unsortedRequests: ProposalListingData[] = [
265+
{
266+
...baseData,
267+
actionName: 'Action A - Oldest',
268+
contractId: 'a' as ContractId<VoteRequest>,
269+
voteTakesEffect: '2025-01-10 12:00',
270+
votingThresholdDeadline: '2025-01-05 12:00',
271+
},
272+
{
273+
...baseData,
274+
actionName: 'Action C - Most recent',
275+
contractId: 'c' as ContractId<VoteRequest>,
276+
voteTakesEffect: '2025-01-20 12:00',
277+
votingThresholdDeadline: '2025-01-15 12:00',
278+
},
279+
{
280+
...baseData,
281+
actionName: 'Action B - Middle',
282+
contractId: 'b' as ContractId<VoteRequest>,
283+
voteTakesEffect: '2025-01-15 12:00',
284+
votingThresholdDeadline: '2025-01-10 12:00',
285+
},
286+
];
287+
288+
render(
289+
<MemoryRouter>
290+
<ProposalListingSection
291+
sectionTitle="Vote History"
292+
data={unsortedRequests}
293+
noDataMessage="No data"
294+
uniqueId="vote-history"
295+
showStatus
296+
showVoteStats
297+
showAcceptanceThreshold
298+
sortOrder="effectiveAtDesc"
299+
/>
300+
</MemoryRouter>
301+
);
302+
303+
const rows = screen.getAllByTestId('vote-history-row');
304+
expect(rows).toHaveLength(3);
305+
306+
const actionNames = rows.map(
307+
row => row.querySelector('[data-testid="vote-history-row-action-name"]')?.textContent
308+
);
309+
expect(actionNames[0]).toBe('Action C - Most recent');
310+
expect(actionNames[1]).toBe('Action B - Middle');
311+
expect(actionNames[2]).toBe('Action A - Oldest');
312+
});
313+
314+
test('should handle same-day effective dates with different times', () => {
315+
const unsortedRequests: ProposalListingData[] = [
316+
{
317+
...baseData,
318+
actionName: 'Action A - Earlier time',
319+
contractId: 'a' as ContractId<VoteRequest>,
320+
voteTakesEffect: '2025-01-15 10:00',
321+
votingThresholdDeadline: '2025-01-10 12:00',
322+
},
323+
{
324+
...baseData,
325+
actionName: 'Action B - Later time',
326+
contractId: 'b' as ContractId<VoteRequest>,
327+
voteTakesEffect: '2025-01-15 18:00',
328+
votingThresholdDeadline: '2025-01-10 12:00',
329+
},
330+
];
331+
332+
render(
333+
<MemoryRouter>
334+
<ProposalListingSection
335+
sectionTitle="Vote History"
336+
data={unsortedRequests}
337+
noDataMessage="No data"
338+
uniqueId="vote-history"
339+
showStatus
340+
showVoteStats
341+
showAcceptanceThreshold
342+
sortOrder="effectiveAtDesc"
343+
/>
344+
</MemoryRouter>
345+
);
346+
347+
const rows = screen.getAllByTestId('vote-history-row');
348+
const actionNames = rows.map(
349+
row => row.querySelector('[data-testid="vote-history-row-action-name"]')?.textContent
350+
);
351+
352+
// Later time should come first (most recent)
353+
expect(actionNames[0]).toBe('Action B - Later time');
354+
expect(actionNames[1]).toBe('Action A - Earlier time');
355+
});
356+
});
357+
});

apps/sv/frontend/src/components/governance/ActionRequiredSection.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,26 @@ export const ActionRequiredSection: React.FC<ActionRequiredProps> = (
3030
) => {
3131
const { actionRequiredRequests } = props;
3232

33+
// Sort by voting closes date ascending (closest deadline first)
34+
const sortedRequests = actionRequiredRequests.toSorted((a, b) =>
35+
dayjs(a.votingCloses).isBefore(dayjs(b.votingCloses)) ? -1 : 1
36+
);
37+
3338
return (
3439
<Box sx={{ mb: 4 }} data-testid="action-required-section">
3540
<PageSectionHeader
3641
title="Action Required"
37-
badgeCount={actionRequiredRequests.length}
42+
badgeCount={sortedRequests.length}
3843
data-testid="action-required"
3944
/>
4045

4146
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, mb: 3 }}>
42-
{actionRequiredRequests.length === 0 ? (
47+
{sortedRequests.length === 0 ? (
4348
<Alert severity="info" data-testid={'action-required-section-no-items'}>
4449
No Action Required items available
4550
</Alert>
4651
) : (
47-
actionRequiredRequests.map((ar, index) => (
52+
sortedRequests.map((ar, index) => (
4853
<ActionCard
4954
key={index}
5055
action={ar.actionName}

0 commit comments

Comments
 (0)