fix: voting during waiting for node vote (#1748)
* fix: voting during waiting for node vote allow voting during waiting for node vote, bug fix for vote cancelled, show correct status * fix: adjust vote information to be more correct * test: begin adding tests for current proposal status * chore: clean up render logic, add more tests for current proposal status * chore: add tests for failed proposals * test: add final tests for vote information * test: fix rebase issues
This commit is contained in:
parent
5e75e0ee21
commit
1bc41c8a9a
@ -505,7 +505,7 @@
|
||||
"voteFailedReason": "Vote closed. Failed due to: ",
|
||||
"Passed": "Passed",
|
||||
"votePassed": "Vote passed.",
|
||||
"subjectToFurtherActions": "Vote passed {{daysAgo}} subject to further actions.",
|
||||
"WaitingForNodeVote": "Waiting for nodes to validate asset. ",
|
||||
"transactionHashPrompt": "Transaction hash will appear here once the transaction is approved in your Ethereum wallet",
|
||||
"newWalletVersionAvailable": "A new Vega wallet is available 🎉. ",
|
||||
"downloadNewWallet": "Download {{newVersionAvailable}}",
|
||||
@ -564,7 +564,7 @@
|
||||
"yesPercentage": "Yes percentage",
|
||||
"noPercentage": "No percentage",
|
||||
"proposalTerms": "Proposal terms",
|
||||
"currentlySetTo": "Currently set to ",
|
||||
"currentlySetTo": "Vote currently set to ",
|
||||
"rankingScore": "Ranking score",
|
||||
"stakeScore": "Stake score",
|
||||
"performanceScore": "Performance",
|
||||
@ -691,5 +691,7 @@
|
||||
"MoreAssetsInfo": "To see Explorer data on existing assets visit",
|
||||
"ProposalNotFound": "Proposal not found",
|
||||
"ProposalNotFoundDetails": "The proposal you are looking for is not here, it may have been enacted before the last chain restore. You could check the Vega forums/discord instead for information about it.",
|
||||
"FreeformProposal": "Freeform proposal"
|
||||
"FreeformProposal": "Freeform proposal",
|
||||
"unknownReason": "unknown reason",
|
||||
"votingEnded": "Voting has ended."
|
||||
}
|
||||
|
@ -0,0 +1,298 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { NETWORK_PARAMETERS_QUERY } from '@vegaprotocol/react-helpers';
|
||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import type { ProposalFields } from '../../__generated__/ProposalFields';
|
||||
import { CurrentProposalStatus } from './current-proposal-status';
|
||||
|
||||
const networkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
request: {
|
||||
query: NETWORK_PARAMETERS_QUERY,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateNetParam.requiredMajority',
|
||||
value: '0.00000001',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateNetParam.requiredParticipation',
|
||||
value: '0.000000001',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = ({ proposal }: { proposal: ProposalFields }) => {
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<CurrentProposalStatus proposal={proposal} />
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(60 * 60 * 1000);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('Proposal open - renders will fail state if the proposal will fail', async () => {
|
||||
const proposal = generateProposal();
|
||||
const failedProposal: ProposalFields = {
|
||||
...proposal,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: '0',
|
||||
totalTokens: '0',
|
||||
votes: null,
|
||||
},
|
||||
no: {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: '0',
|
||||
totalTokens: '0',
|
||||
votes: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
renderComponent({ proposal: failedProposal });
|
||||
expect(await screen.findByText('Vote currently set to')).toBeInTheDocument();
|
||||
expect(await screen.findByText('fail')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal open - renders will pass state if the proposal will pass', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({ proposal });
|
||||
expect(await screen.findByText('Vote currently set to')).toBeInTheDocument();
|
||||
expect(await screen.findByText('pass')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal enacted - renders vote passed and time since enactment', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({
|
||||
proposal: {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
enactmentDatetime: new Date(0).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(await screen.findByText('Vote passed.')).toBeInTheDocument();
|
||||
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal passed - renders vote passed and time since vote closed', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({
|
||||
proposal: {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
closingDatetime: new Date(0).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(await screen.findByText('Vote passed.')).toBeInTheDocument();
|
||||
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal waiting for node vote - will pass - renders if the vote will pass and status', async () => {
|
||||
const proposal = generateProposal();
|
||||
const failedProposal: ProposalFields = {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: '0',
|
||||
totalTokens: '0',
|
||||
votes: null,
|
||||
},
|
||||
no: {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: '0',
|
||||
totalTokens: '0',
|
||||
votes: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
renderComponent({ proposal: failedProposal });
|
||||
expect(
|
||||
await screen.findByText('Waiting for nodes to validate asset.')
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('Vote currently set to')).toBeInTheDocument();
|
||||
expect(await screen.findByText('fail')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal waiting for node vote - will fail - renders if the vote will pass and status', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({
|
||||
proposal: {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Waiting for nodes to validate asset.')
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('Vote currently set to')).toBeInTheDocument();
|
||||
expect(await screen.findByText('pass')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal failed - renders vote failed reason and vote closed ago', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({
|
||||
proposal: {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_FAILED,
|
||||
errorDetails: 'foo',
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
closingDatetime: new Date(0).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Vote closed. Failed due to:')
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('foo')).toBeInTheDocument();
|
||||
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal failed - renders rejection reason there are no error details', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({
|
||||
proposal: {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_FAILED,
|
||||
rejectionReason:
|
||||
ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
closingDatetime: new Date(0).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Vote closed. Failed due to:')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE')
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal failed - renders unknown reason if there are no error details or rejection reason', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({
|
||||
proposal: {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_FAILED,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
closingDatetime: new Date(0).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Vote closed. Failed due to:')
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('unknown reason')).toBeInTheDocument();
|
||||
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal failed - renders participation not met if participation is not met', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({
|
||||
proposal: {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_FAILED,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
closingDatetime: new Date(0).toISOString(),
|
||||
},
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: '0',
|
||||
totalTokens: '0',
|
||||
votes: null,
|
||||
},
|
||||
no: {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: '0',
|
||||
totalTokens: '0',
|
||||
votes: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Vote closed. Failed due to:')
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('Participation not met')).toBeInTheDocument();
|
||||
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Proposal failed - renders majority not met if majority is not met', async () => {
|
||||
const proposal = generateProposal();
|
||||
|
||||
renderComponent({
|
||||
proposal: {
|
||||
...proposal,
|
||||
state: ProposalState.STATE_FAILED,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
closingDatetime: new Date(0).toISOString(),
|
||||
},
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: '0',
|
||||
totalTokens: '0',
|
||||
votes: null,
|
||||
},
|
||||
no: {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: '1',
|
||||
totalTokens: '25242474195500835440000',
|
||||
votes: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Vote closed. Failed due to:')
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('Majority not met')).toBeInTheDocument();
|
||||
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -6,14 +6,39 @@ import { ProposalState } from '@vegaprotocol/types';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import type { ProposalFields } from '../../__generated__/ProposalFields';
|
||||
|
||||
export const StatusPass = ({ children }: { children: React.ReactNode }) => (
|
||||
export const StatusPass = ({ children }: { children: ReactNode }) => (
|
||||
<span className="text-vega-green">{children}</span>
|
||||
);
|
||||
|
||||
export const StatusFail = ({ children }: { children: React.ReactNode }) => (
|
||||
export const StatusFail = ({ children }: { children: ReactNode }) => (
|
||||
<span className="text-danger">{children}</span>
|
||||
);
|
||||
|
||||
const WillPass = ({
|
||||
willPass,
|
||||
children,
|
||||
}: {
|
||||
willPass: boolean;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
if (willPass) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<StatusPass>{t('pass')}</StatusPass>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<StatusFail>{t('fail')}</StatusFail>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const CurrentProposalStatus = ({
|
||||
proposal,
|
||||
}: {
|
||||
@ -36,21 +61,7 @@ export const CurrentProposalStatus = ({
|
||||
});
|
||||
|
||||
if (proposal.state === ProposalState.STATE_OPEN) {
|
||||
if (willPass) {
|
||||
return (
|
||||
<>
|
||||
{t('currentlySetTo')}
|
||||
<StatusPass>{t('pass')}</StatusPass>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{t('currentlySetTo')}
|
||||
<StatusFail>{t('fail')}</StatusFail>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <WillPass willPass={willPass}>{t('currentlySetTo')}</WillPass>;
|
||||
}
|
||||
|
||||
if (
|
||||
@ -81,7 +92,11 @@ export const CurrentProposalStatus = ({
|
||||
return (
|
||||
<>
|
||||
<span>{t('voteFailedReason')}</span>
|
||||
<StatusFail>{proposal.state}</StatusFail>
|
||||
<StatusFail>
|
||||
{proposal.errorDetails ||
|
||||
proposal.rejectionReason ||
|
||||
t('unknownReason')}
|
||||
</StatusFail>
|
||||
<span> {daysClosedAgo}</span>
|
||||
</>
|
||||
);
|
||||
@ -106,7 +121,10 @@ export const CurrentProposalStatus = ({
|
||||
|
||||
if (proposal.state === ProposalState.STATE_WAITING_FOR_NODE_VOTE) {
|
||||
return (
|
||||
<span>{t('subjectToFurtherActions', { daysAgo: daysClosedAgo })}</span>
|
||||
<WillPass willPass={willPass}>
|
||||
<span>{t('WaitingForNodeVote')}</span>{' '}
|
||||
<span>{t('currentlySetTo')}</span>
|
||||
</WillPass>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -31,17 +31,18 @@ import type { Proposals_proposalsConnection_edges_node as ProposalNode } from '.
|
||||
const renderComponent = (
|
||||
proposal: ProposalNode,
|
||||
mock = networkParamsQueryMock
|
||||
) => (
|
||||
<Router>
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
) =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
@ -53,15 +54,13 @@ afterAll(() => {
|
||||
|
||||
describe('Proposals list item details', () => {
|
||||
it('Renders proposal state: Enacted', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Enacted');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
@ -70,16 +69,14 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Passed', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Passed');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
@ -88,15 +85,13 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Waiting for node vote', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent(
|
||||
'Waiting for node vote'
|
||||
@ -107,15 +102,13 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 minutes left to vote', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveMinutes.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveMinutes.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
@ -124,15 +117,13 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 hours left to vote', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveHours.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveHours.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
@ -141,15 +132,13 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 days left to vote', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveDays.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveDays.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
@ -158,36 +147,34 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted for', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: {
|
||||
votes: [
|
||||
{
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.VALUE_YES,
|
||||
datetime: lastWeek.toString(),
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: mockPubkey.publicKey,
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: '1000',
|
||||
},
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: {
|
||||
votes: [
|
||||
{
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.VALUE_YES,
|
||||
datetime: lastWeek.toString(),
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: mockPubkey.publicKey,
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: '1000',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
no: generateNoVotes(0),
|
||||
},
|
||||
],
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
no: generateNoVotes(0),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
@ -196,36 +183,34 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted against', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
no: {
|
||||
votes: [
|
||||
{
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.VALUE_NO,
|
||||
datetime: lastWeek.toString(),
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: mockPubkey.publicKey,
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: '1000',
|
||||
},
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
no: {
|
||||
votes: [
|
||||
{
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.VALUE_NO,
|
||||
datetime: lastWeek.toString(),
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: mockPubkey.publicKey,
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: '1000',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
],
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
@ -234,19 +219,17 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - participation not reached', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(0),
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(0),
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
@ -255,19 +238,17 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - majority not reached', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(1, 1000000000000000000),
|
||||
yes: generateYesVotes(1, 1000000000000000000),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(1, 1000000000000000000),
|
||||
yes: generateYesVotes(1, 1000000000000000000),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
@ -276,59 +257,35 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - will pass', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(3000, 1000000000000000000),
|
||||
no: generateNoVotes(0),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(3000, 1000000000000000000),
|
||||
no: generateNoVotes(0),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass');
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - will fail', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(0),
|
||||
no: generateNoVotes(3000, 1000000000000000000),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to fail');
|
||||
});
|
||||
|
||||
it('Renders proposal state: Declined - participation not reached', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_DECLINED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(0),
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_DECLINED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(0),
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
@ -337,19 +294,17 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Declined - majority not reached', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_DECLINED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(1, 1000000000000000000),
|
||||
yes: generateYesVotes(1, 1000000000000000000),
|
||||
},
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_DECLINED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(1, 1000000000000000000),
|
||||
yes: generateYesVotes(1, 1000000000000000000),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
@ -358,17 +313,15 @@ describe('Proposals list item details', () => {
|
||||
});
|
||||
|
||||
it('Renders proposal state: Rejected', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_REJECTED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
rejectionReason:
|
||||
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
|
||||
})
|
||||
)
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_REJECTED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
rejectionReason:
|
||||
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
|
@ -133,12 +133,11 @@ export const ProposalsListItemDetails = ({
|
||||
voteStatus =
|
||||
(!participationMet && <ParticipationNotReached />) ||
|
||||
(!majorityMet && <MajorityNotReached />) ||
|
||||
(willPass && (
|
||||
(willPass ? (
|
||||
<>
|
||||
{t('Set to')} <StatusPass>{t('pass')}</StatusPass>
|
||||
</>
|
||||
)) ||
|
||||
(!willPass && (
|
||||
) : (
|
||||
<>
|
||||
{t('Set to')} <StatusFail>{t('fail')}</StatusFail>
|
||||
</>
|
||||
|
@ -92,17 +92,21 @@ export function useUserVote(
|
||||
*/
|
||||
async function castVote(value: VoteValue) {
|
||||
if (!proposalId || !pubKey) return;
|
||||
|
||||
const previousVoteState = voteState;
|
||||
setVoteState(VoteState.Requested);
|
||||
|
||||
try {
|
||||
await sendTx(pubKey, {
|
||||
const res = await sendTx(pubKey, {
|
||||
voteSubmission: {
|
||||
value: value,
|
||||
proposalId,
|
||||
},
|
||||
});
|
||||
setVoteState(VoteState.Pending);
|
||||
if (res === null) {
|
||||
setVoteState(previousVoteState);
|
||||
} else {
|
||||
setVoteState(VoteState.Pending);
|
||||
}
|
||||
|
||||
// Now await vote via poll in parent component
|
||||
} catch (err) {
|
||||
|
@ -43,7 +43,7 @@ describe('Vote buttons', () => {
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(screen.getByText('Voting has ended. You did not vote')).toBeTruthy();
|
||||
expect(screen.getByText('Voting has ended.')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should provide a connect wallet prompt if no pubkey', () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { format } from 'date-fns';
|
||||
import * as React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
@ -88,10 +88,18 @@ export const VoteButtons = ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
const [changeVote, setChangeVote] = React.useState(false);
|
||||
const proposalVotable = useMemo(
|
||||
() =>
|
||||
[
|
||||
ProposalState.STATE_OPEN,
|
||||
ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
].includes(proposalState),
|
||||
[proposalState]
|
||||
);
|
||||
|
||||
const cantVoteUI = React.useMemo(() => {
|
||||
if (proposalState !== ProposalState.STATE_OPEN) {
|
||||
return t('youDidNotVote');
|
||||
if (!proposalVotable) {
|
||||
return t('votingEnded');
|
||||
}
|
||||
|
||||
if (!pubKey) {
|
||||
@ -141,13 +149,13 @@ export const VoteButtons = ({
|
||||
|
||||
return false;
|
||||
}, [
|
||||
t,
|
||||
proposalVotable,
|
||||
pubKey,
|
||||
currentStakeAvailable,
|
||||
proposalState,
|
||||
appDispatch,
|
||||
minVoterBalance,
|
||||
spamProtectionMinTokens,
|
||||
t,
|
||||
appDispatch,
|
||||
openVegaWalletDialog,
|
||||
]);
|
||||
|
||||
@ -188,7 +196,7 @@ export const VoteButtons = ({
|
||||
{voteDatetime ? (
|
||||
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
|
||||
) : null}
|
||||
{proposalState === ProposalState.STATE_OPEN ? (
|
||||
{proposalVotable ? (
|
||||
<ButtonLink
|
||||
data-testid="change-vote-button"
|
||||
onClick={() => {
|
||||
|
@ -3,12 +3,7 @@ import React from 'react';
|
||||
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { addDecimal } from '../../../lib/decimals';
|
||||
import type {
|
||||
ProposalFields,
|
||||
ProposalFields_votes_no_votes,
|
||||
ProposalFields_votes_yes_votes,
|
||||
} from '../__generated__/ProposalFields';
|
||||
import type { ProposalFields } from '../__generated__/ProposalFields';
|
||||
|
||||
const useProposalNetworkParams = ({
|
||||
proposal,
|
||||
@ -100,34 +95,12 @@ export const useVoteInformation = ({
|
||||
);
|
||||
|
||||
const noTokens = React.useMemo(() => {
|
||||
if (!proposal.votes.no.votes) {
|
||||
return new BigNumber(0);
|
||||
}
|
||||
const totalNoVotes = proposal.votes.no.votes.reduce(
|
||||
(prevValue: BigNumber, newValue: ProposalFields_votes_no_votes) => {
|
||||
return new BigNumber(
|
||||
newValue.party.stakingSummary.currentStakeAvailable
|
||||
).plus(prevValue);
|
||||
},
|
||||
new BigNumber(0)
|
||||
);
|
||||
return new BigNumber(addDecimal(totalNoVotes, 18));
|
||||
}, [proposal.votes.no.votes]);
|
||||
return new BigNumber(proposal.votes.no.totalTokens);
|
||||
}, [proposal.votes.no.totalTokens]);
|
||||
|
||||
const yesTokens = React.useMemo(() => {
|
||||
if (!proposal.votes.yes.votes) {
|
||||
return new BigNumber(0);
|
||||
}
|
||||
const totalYesVotes = proposal.votes.yes.votes.reduce(
|
||||
(prevValue: BigNumber, newValue: ProposalFields_votes_yes_votes) => {
|
||||
return new BigNumber(
|
||||
newValue.party.stakingSummary.currentStakeAvailable
|
||||
).plus(prevValue);
|
||||
},
|
||||
new BigNumber(0)
|
||||
);
|
||||
return new BigNumber(addDecimal(totalYesVotes, 18));
|
||||
}, [proposal.votes.yes.votes]);
|
||||
return new BigNumber(proposal.votes.yes.totalTokens);
|
||||
}, [proposal.votes.yes.totalTokens]);
|
||||
|
||||
const totalTokensVoted = React.useMemo(
|
||||
() => yesTokens.plus(noTokens),
|
||||
@ -153,11 +126,8 @@ export const useVoteInformation = ({
|
||||
}, [requiredParticipation, totalTokensVoted, totalSupply]);
|
||||
|
||||
const majorityMet = React.useMemo(() => {
|
||||
return (
|
||||
yesPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage) ||
|
||||
noPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage)
|
||||
);
|
||||
}, [yesPercentage, noPercentage, requiredMajorityPercentage]);
|
||||
return yesPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage);
|
||||
}, [yesPercentage, requiredMajorityPercentage]);
|
||||
|
||||
const totalTokensPercentage = React.useMemo(() => {
|
||||
return totalTokensVoted.multipliedBy(100).dividedBy(totalSupply);
|
||||
@ -171,7 +141,6 @@ export const useVoteInformation = ({
|
||||
),
|
||||
[participationMet, requiredMajorityPercentage, yesPercentage]
|
||||
);
|
||||
|
||||
return {
|
||||
willPass,
|
||||
totalTokensPercentage,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ProposalState, VoteValue } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as faker from 'faker';
|
||||
import isArray from 'lodash/isArray';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
@ -7,7 +8,9 @@ import type { DeepPartial } from '../../../lib/type-helpers';
|
||||
import type {
|
||||
ProposalFields,
|
||||
ProposalFields_votes_no,
|
||||
ProposalFields_votes_no_votes,
|
||||
ProposalFields_votes_yes,
|
||||
ProposalFields_votes_yes_votes,
|
||||
} from '../__generated__/ProposalFields';
|
||||
|
||||
export function generateProposal(
|
||||
@ -76,34 +79,39 @@ export const generateYesVotes = (
|
||||
numberOfVotes = 5,
|
||||
fixedTokenValue?: number
|
||||
): ProposalFields_votes_yes => {
|
||||
const votes = Array.from(Array(numberOfVotes)).map(() => {
|
||||
const vote: ProposalFields_votes_yes_votes = {
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.VALUE_YES,
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: faker.datatype.uuid(),
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: fixedTokenValue
|
||||
? fixedTokenValue.toString()
|
||||
: faker.datatype
|
||||
.number({
|
||||
min: 1000000000000000000,
|
||||
max: 10000000000000000000000,
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
|
||||
return vote;
|
||||
});
|
||||
return {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: faker.datatype.number({ min: 0, max: 100 }).toString(),
|
||||
totalTokens: faker.datatype
|
||||
.number({ min: 1, max: 10000000000000000000000 })
|
||||
totalNumber: votes.length.toString(),
|
||||
totalTokens: votes
|
||||
.reduce((acc, cur) => {
|
||||
return acc.plus(cur.party.stakingSummary.currentStakeAvailable);
|
||||
}, new BigNumber(0))
|
||||
.toString(),
|
||||
votes: Array.from(Array(numberOfVotes)).map(() => {
|
||||
return {
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.VALUE_YES,
|
||||
party: {
|
||||
id: faker.datatype.uuid(),
|
||||
__typename: 'Party',
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: fixedTokenValue
|
||||
? fixedTokenValue.toString()
|
||||
: faker.datatype
|
||||
.number({
|
||||
min: 1000000000000000000,
|
||||
max: 10000000000000000000000,
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
}),
|
||||
votes,
|
||||
};
|
||||
};
|
||||
|
||||
@ -111,33 +119,37 @@ export const generateNoVotes = (
|
||||
numberOfVotes = 5,
|
||||
fixedTokenValue?: number
|
||||
): ProposalFields_votes_no => {
|
||||
const votes = Array.from(Array(numberOfVotes)).map(() => {
|
||||
const vote: ProposalFields_votes_no_votes = {
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.VALUE_NO,
|
||||
party: {
|
||||
id: faker.datatype.uuid(),
|
||||
__typename: 'Party',
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: fixedTokenValue
|
||||
? fixedTokenValue.toString()
|
||||
: faker.datatype
|
||||
.number({
|
||||
min: 1000000000000000000,
|
||||
max: 10000000000000000000000,
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
return vote;
|
||||
});
|
||||
return {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: faker.datatype.number({ min: 0, max: 100 }).toString(),
|
||||
totalTokens: faker.datatype
|
||||
.number({ min: 1000000000000000000, max: 10000000000000000000000 })
|
||||
totalNumber: votes.length.toString(),
|
||||
totalTokens: votes
|
||||
.reduce((acc, cur) => {
|
||||
return acc.plus(cur.party.stakingSummary.currentStakeAvailable);
|
||||
}, new BigNumber(0))
|
||||
.toString(),
|
||||
votes: Array.from(Array(numberOfVotes)).map(() => {
|
||||
return {
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.VALUE_NO,
|
||||
party: {
|
||||
id: faker.datatype.uuid(),
|
||||
__typename: 'Party',
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: fixedTokenValue
|
||||
? fixedTokenValue.toString()
|
||||
: faker.datatype
|
||||
.number({
|
||||
min: 1000000000000000000,
|
||||
max: 10000000000000000000000,
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
}),
|
||||
votes,
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user