diff --git a/components/admins/components/__tests__/adminOptions.test.tsx b/components/admins/components/__tests__/adminOptions.test.tsx deleted file mode 100644 index a1b754a..0000000 --- a/components/admins/components/__tests__/adminOptions.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { afterEach, describe, expect, test } from 'bun:test'; -import React from 'react'; -import { screen, cleanup, within, fireEvent } from '@testing-library/react'; -import AdminOptions from '@/components/admins/components/adminOptions'; -import matchers from '@testing-library/jest-dom/matchers'; -import { renderWithChainProvider } from '@/tests/render'; -import { mockPoaParams, mockGroup } from '@/tests/mock'; - -expect.extend(matchers); - -const renderWithProps = (props = {}) => { - const defaultProps = { - poaParams: mockPoaParams, - group: mockGroup, - isLoading: false, - address: 'test_address', - admin: 'admin1', - }; - return renderWithChainProvider(); -}; - -describe('AdminOptions', () => { - afterEach(cleanup); - - test('renders loading state correctly', () => { - renderWithProps({ isLoading: true }); - expect(screen.getByText('Admin')).toBeInTheDocument(); - }); - - test('renders admin details correctly when not loading', () => { - renderWithProps(); - expect(screen.getByText('Admin')).toBeInTheDocument(); - expect(screen.getByAltText('Profile Avatar')).toBeInTheDocument(); - const titleContainer = screen.getByLabelText('title'); - expect(within(titleContainer).getByText('title1')).toBeInTheDocument(); - const detailsContainer = screen.getByLabelText('details'); - expect(within(detailsContainer).getByText('details1')).toBeInTheDocument(); - }); - - test('opens description modal on button click', () => { - renderWithProps(); - fireEvent.click(screen.getByLabelText('three-dots')); - const modal = document.getElementById('description-modal') as HTMLDialogElement; - expect(modal).toBeInTheDocument(); - expect(modal.open).toBe(true); - }); -}); diff --git a/components/admins/components/adminOptions.tsx b/components/admins/components/adminOptions.tsx deleted file mode 100644 index a8a5e46..0000000 --- a/components/admins/components/adminOptions.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { ExtendedGroupType } from '@/hooks'; - -import { BsThreeDots } from 'react-icons/bs'; -import { DescriptionModal } from '../modals/descriptionModal'; -import ProfileAvatar from '@/utils/identicon'; - -interface AdminOptionsProps { - group: ExtendedGroupType; - isLoading: boolean; - address: string; - admin: string; -} - -export default function AdminOptions({ group, isLoading }: Readonly) { - const handleOpen = () => { - const modal = document.getElementById(`update-admin-modal`) as HTMLDialogElement; - modal?.showModal(); - }; - - const handleDescription = () => { - const modal = document.getElementById(`description-modal`) as HTMLDialogElement; - modal?.showModal(); - }; - - return ( -
-
-

Admin

-
-
- {isLoading &&
} - {!isLoading && ( -
-
- -
- - {group?.ipfsMetadata?.title} - - - - {group?.ipfsMetadata?.details} - - - -
- -
-
- )} - - -
- ); -} diff --git a/components/admins/components/index.tsx b/components/admins/components/index.tsx index c20c7a2..b2a1351 100644 --- a/components/admins/components/index.tsx +++ b/components/admins/components/index.tsx @@ -1,3 +1,2 @@ -export * from './adminOptions'; export * from './stakingParams'; export * from './validatorList'; diff --git a/components/groups/components/CountdownTimer.tsx b/components/groups/components/CountdownTimer.tsx index d8d95ce..66c9b0f 100644 --- a/components/groups/components/CountdownTimer.tsx +++ b/components/groups/components/CountdownTimer.tsx @@ -26,9 +26,9 @@ export default function CountdownTimer({ endTime }: { endTime: Date }) { return (
-
+
- + { }), })); renderWithChainProvider(); - expect(screen.getByText('No proposals found')).toBeInTheDocument(); + expect(screen.getByText('No proposal was found.')).toBeInTheDocument(); }); test('renders proposals list correctly', () => { diff --git a/components/groups/components/groupProposals.tsx b/components/groups/components/groupProposals.tsx index e905b62..95c9455 100644 --- a/components/groups/components/groupProposals.tsx +++ b/components/groups/components/groupProposals.tsx @@ -241,15 +241,12 @@ export default function GroupProposals({ return (
{/* Header section */} -
-
- -

{groupName}

+

{groupName}

@@ -269,25 +266,25 @@ export default function GroupProposals({
{/* Search and New Proposal section */} -
-

Proposals

+
+

Proposals

setSearchTerm(e.target.value)} /> - +
- - -
+ + +
{/* Table section - will fill remaining space */} @@ -357,25 +354,25 @@ export default function GroupProposals({ handleRowClick(proposal)} - className="hover:bg-base-200 text-black dark:text-white rounded-lg cursor-pointer" + className="group text-black dark:text-white rounded-lg cursor-pointer" > - + {proposal.id.toString()} - + {proposal.title} - + {timeLeft} - + {proposal.messages.length > 0 ? proposal.messages.map((message, index) => (
{getHumanReadableType((message as any)['@type'])}
)) : 'No messages'} - + {isTalliesLoading ? ( ) : ( @@ -388,7 +385,7 @@ export default function GroupProposals({ ) : ( -
No proposals found
+
No proposal was found.
)}
diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 55e581e..c87bfaa 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -83,7 +83,7 @@ export function YourGroups({

My groups @@ -92,7 +92,7 @@ export function YourGroups({ setSearchTerm(e.target.value)} /> @@ -109,71 +109,69 @@ export function YourGroups({

-
- - - - - - - - - - - - - {isLoading - ? // Skeleton - Array(12) - .fill(0) - .map((_, index) => ( - - - - - - - - - )) - : // content - filteredGroups.map((group, index) => ( - 0 - ? proposals[group.policies[0].address] - : [] - } - onSelectGroup={(policyAddress, groupName) => - handleSelectGroup( - policyAddress, - groupName, - (group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType) - ?.threshold ?? '0' - ) - } - /> - ))} - -
Group NameActive proposalsAuthorsGroup BalanceQualified MajorityGroup address
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ + + + + + + + + + + + + {isLoading + ? // Skeleton + Array(12) + .fill(0) + .map((_, index) => ( + + + + + + + + + )) + : // content + filteredGroups.map((group, index) => ( + 0 + ? proposals[group.policies[0].address] + : [] + } + onSelectGroup={(policyAddress, groupName) => + handleSelectGroup( + policyAddress, + groupName, + (group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType) + ?.threshold ?? '0' + ) + } + /> + ))} + +
Group NameActive proposalsAuthorsGroup BalanceQualified MajorityGroup address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -226,7 +224,7 @@ function GroupRow({ return ( { e.stopPropagation(); onSelectGroup( @@ -238,33 +236,35 @@ function GroupRow({ ); }} > - +
{truncateString(groupName, 24)}
- + {activeProposals.length > 0 ? ( - {activeProposals.length} + + {activeProposals.length} + ) : ( '-' )} - + {truncateString( getAuthor(group.ipfsMetadata?.authors) || 'Unknown', getAuthor(group.ipfsMetadata?.authors || '').startsWith('manifest1') ? 6 : 24 )} - + {Number(shiftDigits(balance?.amount ?? '0', -6)).toLocaleString(undefined, { maximumFractionDigits: 6, })}{' '} MFX - {`${(group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType).threshold ?? '0'} / ${group.total_weight ?? '0'}`} - + {`${(group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType).threshold ?? '0'} / ${group.total_weight ?? '0'}`} + diff --git a/components/groups/modals/__tests__/voteDetailsModal.test.tsx b/components/groups/modals/__tests__/voteDetailsModal.test.tsx new file mode 100644 index 0000000..3035299 --- /dev/null +++ b/components/groups/modals/__tests__/voteDetailsModal.test.tsx @@ -0,0 +1,115 @@ +import { describe, test, expect, jest, mock, afterEach } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import VoteDetailsModal from '../voteDetailsModal'; +import { + ProposalSDKType, + MemberSDKType, + VoteSDKType, + ProposalStatus, + ProposalExecutorResult, +} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; +import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { + manifestAddr1, + mockGroupFormData, + mockMembers, + mockProposals, + mockTally, + mockVotes, +} from '@/tests/mock'; + +expect.extend(matchers); + +mock.module('react-apexcharts', () => ({ + default: jest.fn(), +})); + +mock.module('@cosmos-kit/react', () => ({ + useChain: jest.fn().mockReturnValue({ + address: mockProposals['test_policy_address'][0].proposers[0], + chain: { fees: null }, + }), +})); + +const mockProposal = mockProposals['test_policy_address'][0]; + +describe('VoteDetailsModal', () => { + const defaultProps = { + tallies: mockTally, + votes: mockVotes, + members: mockMembers, + proposal: mockProposal, + onClose: jest.fn(), + modalId: 'voteDetailsModal', + refetchVotes: jest.fn(), + refetchTally: jest.fn(), + refetchProposals: jest.fn(), + }; + + afterEach(() => { + mock.restore(); + cleanup(); + }); + + test('renders the component with provided props', () => { + renderWithChainProvider(); + expect(screen.getByText(`#${mockProposal.id.toString()}`)).toBeInTheDocument(); + expect(screen.getByText(mockProposal.title)).toBeInTheDocument(); + expect(screen.getByText('SUMMARY')).toBeInTheDocument(); + expect(screen.getByText(mockProposal.summary)).toBeInTheDocument(); + }); + + test('renders the tally chart', () => { + renderWithChainProvider(); + expect(screen.getByLabelText('chart-tally')).toBeInTheDocument(); + }); + + test('renders voting countdown timer', () => { + renderWithChainProvider(); + expect(screen.getByLabelText('voting-countdown-1')).toBeInTheDocument(); + expect(screen.getByLabelText('voting-countdown-2')).toBeInTheDocument(); + }); + + test('renders messages section with correct data', () => { + renderWithChainProvider(); + expect(screen.getByText('MESSAGES')).toBeInTheDocument(); + expect(screen.getByText('Send')).toBeInTheDocument(); + expect(screen.getByText('from_address:')).toBeInTheDocument(); + expect(screen.getByText('to_address:')).toBeInTheDocument(); + }); + + test('conditionally renders execute button when proposal is accepted', () => { + const props = { + ...defaultProps, + proposal: { + ...mockProposal, + status: 'PROPOSAL_STATUS_ACCEPTED', + executor_result: 'PROPOSAL_EXECUTOR_RESULT_NOT_RUN', + }, + }; + renderWithChainProvider(); + expect(screen.getByText('Execute')).toBeInTheDocument(); + }); + + test('conditionally renders vote button when proposal is open and user has not voted', () => { + renderWithChainProvider(); + expect(screen.getByText('Vote')).toBeInTheDocument(); + }); + + test('does not render vote button when user has already voted', () => { + const props = { ...defaultProps, votes: [{ ...mockVotes[0], voter: 'proposer1' }] }; + renderWithChainProvider(); + const btn = screen.getByLabelText('action-btn'); + expect(btn.textContent).not.toBe('Vote'); + }); + + test('handles vote button click and opens voting modal', async () => { + renderWithChainProvider(); + const voteButton = screen.getByText('Vote'); + fireEvent.click(voteButton); + await waitFor(() => expect(screen.getByLabelText('vote-modal')).toBeInTheDocument()); + }); +}); diff --git a/components/groups/modals/voteDetailsModal.tsx b/components/groups/modals/voteDetailsModal.tsx index 140f409..5c9f602 100644 --- a/components/groups/modals/voteDetailsModal.tsx +++ b/components/groups/modals/voteDetailsModal.tsx @@ -66,7 +66,7 @@ function VoteDetailsModal({ const { theme } = useTheme(); - const textColor = theme === 'dark' ? '#E0D1D4' : '#2e2e2e'; + const textColor = theme === 'dark' ? '#FFFFFF' : '#161616'; const normalizedMembers = useMemo( () => @@ -130,6 +130,9 @@ function VoteDetailsModal({ labels: { useSeriesColors: true, }, + markers: { + strokeWidth: 0, + }, }, states: { normal: { @@ -182,7 +185,7 @@ function VoteDetailsModal({ data: chartData, }, ], - colors: ['#00D515', '#F54562', '#FBBD23', '#3B82F6'], + colors: ['#4CAF50', '#E53935', '#FFB300', '#3F51B5'], tooltip: { enabled: false, }, @@ -349,9 +352,9 @@ function VoteDetailsModal({ if (Array.isArray(value)) { return (
-

{key}:

+

{key}:

{value.map((item, index) => ( -
+
{renderMessageField(`Item ${index + 1}`, item, depth + 1)}
))} @@ -360,7 +363,7 @@ function VoteDetailsModal({ } else { return (
-

{key}:

+

{key}:

{Object.entries(value).map(([subKey, subValue]) => renderMessageField(subKey, subValue, depth + 1) )} @@ -370,11 +373,13 @@ function VoteDetailsModal({ } else { return (
-

{key}:

+

{key}:

{typeof value === 'string' && value.match(/^[a-zA-Z0-9]{40,}$/) ? ( ) : ( -

{truncateText(String(value))}

+

+ {truncateText(String(value))} +

)}
); @@ -416,21 +421,25 @@ function VoteDetailsModal({ return ( -
+
- +
-

#{proposal?.id?.toString() ?? '0'}

- +

+ #{proposal?.id?.toString() ?? '0'} +

+ {getStatusLabel(proposal)}
{userHasVoted && (
- Your vote: + Your vote:

TITLE

-

{proposal?.title}

+

+ {proposal?.title} +

SUBMITTED - + {new Date(proposal?.submit_time).toDateString().toLocaleString()}

SUMMARY

-
-

{proposal?.summary}

+
+

{proposal?.summary}

-

MESSAGES

-
+

MESSAGES

+
{proposal?.messages?.map((message: any, index: number) => { const messageType = message['@type']; const fieldsToShow = importantFields[messageType] || defaultFields; return (
-

+

{messageType.split('.').pop().replace('Msg', '')}

@@ -486,7 +497,7 @@ function VoteDetailsModal({ })}
-
+

VOTING COUNTDOWN

@@ -497,24 +508,30 @@ function VoteDetailsModal({

TALLY

-
+

MEMBERS

-
-
+
+
- + - - - @@ -523,12 +540,12 @@ function VoteDetailsModal({ {normalizedMembers?.map((member, index) => { const memberVote = voteMap[member?.address]; return ( - + - - + @@ -539,7 +556,7 @@ function VoteDetailsModal({ -
+

VOTING COUNTDOWN

@@ -548,6 +565,7 @@ function VoteDetailsModal({
{getButtonState.action && (
+ Address + Weight + Vote
{member?.weight} + {member?.weight} {optionToVote(memberVote?.toString()) || 'N/A'}