diff --git a/dev-environment/src/components/DevApp.tsx b/dev-environment/src/components/DevApp.tsx index f109001..3eb9fd7 100644 --- a/dev-environment/src/components/DevApp.tsx +++ b/dev-environment/src/components/DevApp.tsx @@ -43,7 +43,7 @@ export const DevApp = () => { )} /> - + diff --git a/jest.config.js b/jest.config.js index 44b3352..20e523e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -66,9 +66,7 @@ module.exports = { // maxWorkers: "50%", // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + moduleDirectories: ['node_modules', 'src'], // An array of file extensions your modules use // moduleFileExtensions: [ diff --git a/package-lock.json b/package-lock.json index d5c5d7e..91a015d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "react-dom": "^17.0.2", "react-query": "^3.39.1", "react-resize-detector": "7.0.0", - "react-router-dom": "^5.3.0", + "react-router-dom": "^5.3.0 || ^6.0.0", "viem": "^1.6.4", "wagmi": "^1.3.10", "yup": "^0.32.11" @@ -58,6 +58,7 @@ "babel-plugin-transform-rename-import": "^2.3.0", "concurrently": "^7.4.0", "copyfiles": "^2.4.1", + "dotenv": "^16.3.1", "enzyme": "^3.11.0", "eslint": "^8.45.0", "eslint-config-prettier": "^8.8.0", @@ -7351,18 +7352,6 @@ "node": ">=16" } }, - "node_modules/@synthetixio/synpress/node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, "node_modules/@synthetixio/synpress/node_modules/ethers": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", @@ -10161,6 +10150,14 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/@zero-tech/zdao-sdk/node_modules/dotenv": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", + "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/@zero-tech/zdao-sdk/node_modules/graphql": { "version": "16.3.0", "license": "MIT", @@ -14309,10 +14306,15 @@ } }, "node_modules/dotenv": { - "version": "16.0.0", - "license": "BSD-2-Clause", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/dotenv-parse-variables": { @@ -34270,12 +34272,6 @@ "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", "dev": true }, - "dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", - "dev": true - }, "ethers": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", @@ -36418,6 +36414,11 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" }, + "dotenv": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", + "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==" + }, "graphql": { "version": "16.3.0" }, @@ -39388,7 +39389,10 @@ } }, "dotenv": { - "version": "16.0.0" + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true }, "dotenv-parse-variables": { "version": "2.0.0", diff --git a/package.json b/package.json index c65f9ef..72b7d22 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "babel-plugin-transform-rename-import": "^2.3.0", "concurrently": "^7.4.0", "copyfiles": "^2.4.1", + "dotenv": "^16.3.1", "enzyme": "^3.11.0", "eslint": "^8.45.0", "eslint-config-prettier": "^8.8.0", @@ -103,9 +104,9 @@ "moment": "^2.29.1", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-router-dom": "^5.3.0", "react-query": "^3.39.1", "react-resize-detector": "7.0.0", + "react-router-dom": "^5.3.0 || ^6.0.0", "viem": "^1.6.4", "wagmi": "^1.3.10", "yup": "^0.32.11" diff --git a/playwright.config.ts b/playwright.config.ts index 01ee648..e30584c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,6 +9,7 @@ config(); * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ + testDir: 'tests/e2e', timeout: 30 * 1000, expect: { timeout: 5000, diff --git a/src/features/view-dao-proposals/lib/helpers.test.ts b/src/features/view-dao-proposals/lib/helpers.test.ts new file mode 100644 index 0000000..afa3a95 --- /dev/null +++ b/src/features/view-dao-proposals/lib/helpers.test.ts @@ -0,0 +1,118 @@ +import { getProposalStatus } from './helpers'; +import { ProposalState } from '@zero-tech/zdao-sdk'; + +describe('getProposalStatus', () => { + describe('when proposal is not compatible', () => { + it('returns "-"', () => { + const result = getProposalStatus( + true, + true, + false, + [1, 2], + ProposalState.ACTIVE, + ); + expect(result).toEqual('-'); + }); + }); + + describe('when proposal is compatible', () => { + it('returns "No Votes" when proposal is closed and has no votes', () => { + const result = getProposalStatus( + true, + false, + true, + [1, 2], + ProposalState.CLOSED, + ); + expect(result).toEqual('No Votes'); + }); + + it('returns "No Votes Yet" when proposal is active and has no votes', () => { + const result = getProposalStatus( + true, + false, + true, + [1, 2], + ProposalState.ACTIVE, + ); + expect(result).toEqual('No Votes Yet'); + }); + + it('returns "Expired" when proposal is closed and has no scores', () => { + const result = getProposalStatus( + true, + true, + true, + [], + ProposalState.CLOSED, + ); + expect(result).toEqual('Expired'); + }); + + it('returns "More Votes Needed" when proposal is active and has no scores', () => { + const result = getProposalStatus( + true, + true, + true, + [], + ProposalState.ACTIVE, + ); + expect(result).toEqual('More Votes Needed'); + }); + + it('returns "Approved" when proposal is closed, can be executed and has more votes in favor', () => { + const result = getProposalStatus( + true, + true, + true, + [2, 1], + ProposalState.CLOSED, + ); + expect(result).toEqual('Approved'); + }); + + it('returns "Denied" when proposal is closed, cannot be executed and has more votes against', () => { + const result = getProposalStatus( + false, + true, + true, + [1, 2], + ProposalState.CLOSED, + ); + expect(result).toEqual('Denied'); + }); + + it('returns "Approval Favoured" when proposal is active and has more votes in favor', () => { + const result = getProposalStatus( + true, + true, + true, + [2, 1], + ProposalState.ACTIVE, + ); + expect(result).toEqual('Approval Favoured'); + }); + + it('returns "Denial Favoured" when proposal is active and has more votes against', () => { + const result = getProposalStatus( + true, + true, + true, + [1, 2], + ProposalState.ACTIVE, + ); + expect(result).toEqual('Denial Favoured'); + }); + + it('returns "More Votes Needed" when proposal has equal votes for and against', () => { + const result = getProposalStatus( + true, + true, + true, + [1, 1], + ProposalState.ACTIVE, + ); + expect(result).toEqual('More Votes Needed'); + }); + }); +}); diff --git a/src/features/view-dao-proposals/lib/helpers.ts b/src/features/view-dao-proposals/lib/helpers.ts index ac39347..da41070 100644 --- a/src/features/view-dao-proposals/lib/helpers.ts +++ b/src/features/view-dao-proposals/lib/helpers.ts @@ -108,60 +108,72 @@ export const getSnapshotProposalLink = ( return `https://snapshot.org/#/${dao.ens}/proposal/${proposal.id}`; }; -/** - * Format proposal status - * @param proposal to format - * @returns formatted proposal status string - */ -export const formatProposalStatus = (proposal?: Proposal): string => { - if (proposal) { - if (isFromSnapshotWithMultipleChoices(proposal)) { - return '-'; - } +export const getProposalStatus = ( + canExecute: boolean, + hasVotes: boolean, + isCompatible: boolean, + scores: number[], + state: ProposalState, +): string => { + if (!isCompatible) { + return '-'; + } - const isClosed = proposal.state === ProposalState.CLOSED; + const isClosed = state === ProposalState.CLOSED; - if (!proposal.votes) { - if (isClosed) { - return 'No Votes'; - } else { - return 'No Votes Yet'; - } + if (!hasVotes) { + if (isClosed) { + return 'No Votes'; + } else { + return 'No Votes Yet'; } + } - if (isEmpty(proposal.scores)) { - if (isClosed) { - return 'Expired'; - } else { - return 'More Votes Needed'; - } + if (isEmpty(scores)) { + if (isClosed) { + return 'Expired'; + } else { + return 'More Votes Needed'; } + } + + if (isClosed) { + if (canExecute) { + return 'Approved'; + } else { + return 'Denied'; + } + } + if (scores[0] > scores[1]) { if (isClosed) { - if (proposal.canExecute()) { + if (canExecute) { return 'Approved'; } else { return 'Denied'; } } - - if (proposal.scores[0] > proposal.scores[1]) { - if (isClosed) { - if (proposal.canExecute()) { - return 'Approved'; - } else { - return 'Denied'; - } - } - return isClosed ? 'Approved' : 'Approval Favoured'; - } else if (proposal.scores[0] < proposal.scores[1]) { - return isClosed ? 'Denied' : 'Denial Favoured'; - } else { - return 'More Votes Needed'; - } + return isClosed ? 'Approved' : 'Approval Favoured'; + } else if (scores[0] < scores[1]) { + return isClosed ? 'Denied' : 'Denial Favoured'; + } else { + return 'More Votes Needed'; } +}; - return ''; +/** + * Format proposal status + * @param proposal to format + * @returns formatted proposal status string + */ +export const formatProposalStatus = (proposal: Proposal): string => { + return getProposalStatus( + proposal.canExecute(), + Boolean(proposal.votes), + Boolean(proposal.metadata), + proposal.scores, + proposal.state, + ); }; /** diff --git a/src/features/view-proposal-attributes/components/ProposalAttributes.tsx b/src/features/view-proposal-attributes/components/ProposalAttributes.tsx new file mode 100644 index 0000000..ed58bc7 --- /dev/null +++ b/src/features/view-proposal-attributes/components/ProposalAttributes.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Proposal, ProposalState } from '@zero-tech/zdao-sdk'; + +import { formatDistance } from 'date-fns/fp'; +import { formatDateTime } from 'lib/util/datetime'; +import { useWeb3 } from 'lib/hooks'; +import { getEtherscanUri } from 'lib/util/network'; +import { formatProposalStatus } from 'features/view-dao-proposals/lib'; +import { kebabCaseToTitleCase } from '../lib/format'; + +import { Attribute, Attributes } from 'features/ui/Attributes/Attributes'; +import { EtherscanLink } from 'features/ui'; + +export interface ProposalAttributesProps { + proposal?: Proposal; + isLoading?: boolean; + votingTokenTicker?: string; +} + +export const ProposalAttributes = ({ + proposal, + isLoading, +}: ProposalAttributesProps) => { + const { chainId } = useWeb3(); + + const etherscanUri = getEtherscanUri(chainId ?? 1); + const isClosed = proposal?.state === ProposalState.CLOSED; + + return ( + + + + + + + + + + ) + } + /> + + ); +}; diff --git a/src/features/view-proposal-attributes/index.ts b/src/features/view-proposal-attributes/index.ts new file mode 100644 index 0000000..20502e2 --- /dev/null +++ b/src/features/view-proposal-attributes/index.ts @@ -0,0 +1 @@ +export * from './components/ProposalAttributes'; diff --git a/src/features/view-proposal-attributes/lib/format.ts b/src/features/view-proposal-attributes/lib/format.ts new file mode 100644 index 0000000..912742c --- /dev/null +++ b/src/features/view-proposal-attributes/lib/format.ts @@ -0,0 +1,3 @@ +export const kebabCaseToTitleCase = (str: string) => { + return str.replace(/-/, ' ').replace(/(^\w|\s\w)/g, (m) => m.toUpperCase()); +}; diff --git a/src/lib/hooks/queries/useProposalVotes.ts b/src/lib/hooks/queries/useProposalVotes.ts index 40a1841..23328be 100644 --- a/src/lib/hooks/queries/useProposalVotes.ts +++ b/src/lib/hooks/queries/useProposalVotes.ts @@ -15,7 +15,7 @@ export const useProposalVotes = ({ zna, proposalId }: UseDaoProposalParams) => { const query = useQuery( ['dao', 'proposal', 'votes', { zna, proposalId }], async () => { - return await proposal.listVotes(); + return await proposal.listVotes({ from: 0, count: 2 }); }, { enabled: Boolean(proposal), diff --git a/src/lib/util/datetime.ts b/src/lib/util/datetime.ts index 9eda16a..b50be20 100644 --- a/src/lib/util/datetime.ts +++ b/src/lib/util/datetime.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import { format } from 'date-fns'; /** * Format seconds as humanized string @@ -26,8 +26,6 @@ export const secondsToDhms = (seconds: number, showSeconds = false): string => { * @param formatter formatter string * @returns formatted humanized string */ -export const formatDateTime = (date: Date, formatter?: string): string => { - if (!moment(date).isValid()) return ''; - - return moment(date).format(formatter); +export const formatDateTime = (date: Date): string => { + return format(date, 'do MMM yy (h:mm a)'); }; diff --git a/src/pages/Proposal/Proposal.module.scss b/src/pages/Proposal-old/Proposal.module.scss similarity index 100% rename from src/pages/Proposal/Proposal.module.scss rename to src/pages/Proposal-old/Proposal.module.scss diff --git a/src/pages/Proposal/Proposal.tsx b/src/pages/Proposal-old/Proposal.tsx similarity index 100% rename from src/pages/Proposal/Proposal.tsx rename to src/pages/Proposal-old/Proposal.tsx diff --git a/src/pages/Proposal/components/Attributes/Attributes.tsx b/src/pages/Proposal-old/components/Attributes/Attributes.tsx similarity index 91% rename from src/pages/Proposal/components/Attributes/Attributes.tsx rename to src/pages/Proposal-old/components/Attributes/Attributes.tsx index 4ae5606..469f6e6 100644 --- a/src/pages/Proposal/components/Attributes/Attributes.tsx +++ b/src/pages/Proposal-old/components/Attributes/Attributes.tsx @@ -47,17 +47,12 @@ export const Attributes = ({ proposalId, zna }: AttributesProps) => { { + const { data: proposal, isLoading: isLoadingProposal } = useCurrentProposal(); + + return ( +
+ + + {proposal?.choices.length === 2 && ( + + )} + + {proposal && ( + + )} + {proposal && ( + <> +
+ + + )} +
+ ); +}; diff --git a/src/pages/proposal/components/AllProposalsLink.tsx b/src/pages/proposal/components/AllProposalsLink.tsx new file mode 100644 index 0000000..fe1ee1e --- /dev/null +++ b/src/pages/proposal/components/AllProposalsLink.tsx @@ -0,0 +1,24 @@ +import React, { useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { BackLinkButton } from '../../../features/ui'; +import { cloneDeep } from 'lodash'; + +export const AllProposalsLink = () => { + const history = useHistory(); + + const toAllProposals = useMemo(() => { + const pathname = history.location.pathname.replace( + /\/proposals\/.*/, + '/proposals/', + ); + const state = cloneDeep(history.location.state); + + return { + pathname, + state, + }; + }, [history]); + + return ; +}; diff --git a/src/pages/proposal/components/ProposalVoteList/ProposalVoteList.module.scss b/src/pages/proposal/components/ProposalVoteList/ProposalVoteList.module.scss new file mode 100644 index 0000000..7e23825 --- /dev/null +++ b/src/pages/proposal/components/ProposalVoteList/ProposalVoteList.module.scss @@ -0,0 +1,27 @@ +.Votes { + h2 { + font-size: 1.125rem; + margin: 0; + } + + thead { + position: sticky; + top: 0; + background-color: var(--color-primary-1); + } + + th, + td { + &:first-child { + padding-left: 0; + padding-right: 0; + } + } + + .LoadMoreButton { + width: 100%; + display: flex; + justify-content: center; + margin-top: 1.5rem; + } +} diff --git a/src/pages/proposal/components/ProposalVoteList/ProposalVoteList.tsx b/src/pages/proposal/components/ProposalVoteList/ProposalVoteList.tsx new file mode 100644 index 0000000..cb56f1d --- /dev/null +++ b/src/pages/proposal/components/ProposalVoteList/ProposalVoteList.tsx @@ -0,0 +1,111 @@ +import React from 'react'; + +import { useInfiniteQuery } from 'react-query'; +import ProposalClient from '@zero-tech/zdao-sdk/lib/client/ProposalClient'; +import { truncateAddress } from '@zero-tech/zui/utils'; +import { useCurrentProposal } from '../../lib/useCurrentProposal'; + +import { + Table, + Body, + Header, + HeaderGroup, +} from '@zero-tech/zui/components/Table'; +import { TableData } from '@zero-tech/zui/components/AsyncTable/Column'; +import { Button } from '@zero-tech/zui/components'; + +import styles from './ProposalVoteList.module.scss'; + +const PAGE_SIZE = 10; + +export const ProposalVoteList = () => { + const { zna, proposalId, data: proposal } = useCurrentProposal(); + + const { + data: voteHistory, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + } = useInfiniteQuery( + ['dao', 'proposal', 'votes', { zna, proposalId }], + async ({ pageParam = 0 }) => { + return proposal.listVotes({ + from: pageParam * PAGE_SIZE, + count: PAGE_SIZE, + }); + }, + { + getNextPageParam: (lastPage, pages) => { + if (lastPage.length === PAGE_SIZE) { + return pages.length; + } + }, + enabled: Boolean(proposal), + }, + ); + + const tokenSymbol = (proposal as ProposalClient)?.['options']?.strategies?.[0] + ?.params?.symbol; + + const handleOnClickLoadMore = () => { + if (!isFetchingNextPage) { + fetchNextPage(); + } + }; + + const hasNoVotes = + voteHistory?.pages.length && voteHistory?.pages[0].length === 0; + + const sumOfScores = proposal?.scores.reduce((a, b) => a + b, 0); + + return ( +
+

Vote History

+ + +
Address
+
Vote Direction
+
+ Amount{tokenSymbol ? ` (${tokenSymbol})` : ''} +
+
Voting Power
+
+ + {!hasNoVotes && + voteHistory?.pages.map((page) => { + return ( + + {page?.map((vote) => ( + + + {truncateAddress(vote.voter)} + + + {proposal?.choices[vote.choice - 1]} + + {vote.power} + + {Math.round((vote.power / sumOfScores) * 100) + '%'} + + + ))} + + ); + })} + +
+
+ {hasNoVotes && No votes yet} + {Boolean(voteHistory?.pages.length) && hasNextPage && ( + + )} +
+
+ ); +}; diff --git a/src/pages/proposal/components/ProposalVoteList/index.ts b/src/pages/proposal/components/ProposalVoteList/index.ts new file mode 100644 index 0000000..6bc4141 --- /dev/null +++ b/src/pages/proposal/components/ProposalVoteList/index.ts @@ -0,0 +1 @@ +export * from './ProposalVoteList'; diff --git a/src/pages/proposal/components/VoteBar/VoteBar.module.scss b/src/pages/proposal/components/VoteBar/VoteBar.module.scss new file mode 100644 index 0000000..f8be220 --- /dev/null +++ b/src/pages/proposal/components/VoteBar/VoteBar.module.scss @@ -0,0 +1,48 @@ +.Container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + gap: 0.5rem; + + > span { + display: flex; + font-size: 0.75rem; + font-weight: bold; + + &:first-of-type { + color: var(--color-secondary-11); + } + + &:last-of-type { + color: var(--color-failure-11); + } + } + + .Options { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + + .Option { + height: 0.5rem; + + &:first-child { + border-radius: 0.5rem 0 0 0.5rem; + } + + &:last-child { + border-radius: 0 0.5rem 0.5rem 0; + } + + &:nth-child(1n) { + background: linear-gradient(90deg, #52ffea 0%, #52cbff 100%); + } + + &:nth-child(2n) { + background: linear-gradient(90deg, #f763b0 0%, #e73a14 100%); + } + } + } +} diff --git a/src/pages/proposal/components/VoteBar/VoteBar.tsx b/src/pages/proposal/components/VoteBar/VoteBar.tsx new file mode 100644 index 0000000..75d70e2 --- /dev/null +++ b/src/pages/proposal/components/VoteBar/VoteBar.tsx @@ -0,0 +1,37 @@ +import React, { useMemo } from 'react'; + +import styles from './VoteBar.module.scss'; + +type VoteBarProps = { + options: string[]; + scores: number[]; +}; + +export const VoteBar = ({ scores, options }: VoteBarProps) => { + const barData = useMemo(() => { + const totalVotes = scores.reduce((acc, score) => acc + score, 0); + return scores.map((score, index) => ({ + percentage: (score / totalVotes) * 100, + option: options[index], + score, + })); + }, [options, scores]); + + const shouldShowLabels = (barData.length = 2); + + return ( +
+ {shouldShowLabels && {Math.round(barData[0].percentage)}%} +
+ {barData.map((bar) => ( +
+ ))} +
+ {shouldShowLabels && {Math.round(barData[1].percentage)}%} +
+ ); +}; diff --git a/src/pages/proposal/components/VoteBar/index.ts b/src/pages/proposal/components/VoteBar/index.ts new file mode 100644 index 0000000..1f5f51f --- /dev/null +++ b/src/pages/proposal/components/VoteBar/index.ts @@ -0,0 +1 @@ +export { VoteBar } from './VoteBar'; diff --git a/src/pages/proposal/index.ts b/src/pages/proposal/index.ts new file mode 100644 index 0000000..4f3e34d --- /dev/null +++ b/src/pages/proposal/index.ts @@ -0,0 +1 @@ +export * from './Proposal'; diff --git a/src/pages/proposal/lib/useCurrentProposal.ts b/src/pages/proposal/lib/useCurrentProposal.ts new file mode 100644 index 0000000..d01137b --- /dev/null +++ b/src/pages/proposal/lib/useCurrentProposal.ts @@ -0,0 +1,19 @@ +import { useParams } from 'react-router-dom'; +import { ProposalId } from '@zero-tech/zdao-sdk'; +import { useCurrentDao, useDaoProposal } from 'lib/hooks'; + +export const useCurrentProposal = () => { + const { proposalId } = useParams<{ proposalId: ProposalId }>(); + const { zna } = useCurrentDao(); + + const query = useDaoProposal({ + proposalId, + zna, + }); + + return { + ...query, + zna, + proposalId, + }; +}; diff --git a/tests/e2e/pages/proposal.ts b/tests/e2e/pages/proposal.ts new file mode 100644 index 0000000..a7d3b15 --- /dev/null +++ b/tests/e2e/pages/proposal.ts @@ -0,0 +1,17 @@ +import { type Page } from '@playwright/test'; + +export class ProposalPage { + readonly page: Page; + daoZna: string; + proposalId: string; + + constructor(page: Page, daoZna: string, proposalId: string) { + this.page = page; + this.daoZna = daoZna; + this.proposalId = proposalId; + } + + async goto() { + await this.page.goto(`/${this.daoZna}/daos/proposals/${this.proposalId}`); + } +} diff --git a/tests/e2e/specs/view-proposal-info.spec.ts b/tests/e2e/specs/view-proposal-info.spec.ts new file mode 100644 index 0000000..4f5c032 --- /dev/null +++ b/tests/e2e/specs/view-proposal-info.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '../../../fixtures'; +import { ProposalPage } from '../pages/proposal'; + +test.beforeEach(async ({ page }) => { + const proposals = new ProposalPage( + page, + '0.wilder.beasts', + '0x6ea2cf6b25fde4e64a001ba7536926ab2e7de514e07652707009474af1154d25', + ); + await proposals.goto(); +}); + +// @note: this is a fragile test, but it will do for now +test('can view all proposal info on proposal page', async ({ page }) => { + await expect(page.getByText("Let's Buy a GEN!")).toBeVisible({ + timeout: 120000, + }); + + // Check proposal info cards + await expect(page.getByText('Votes Submitted12')).toBeVisible(); // Votes submitted + await expect(page.getByText('StatusApproved')).toBeVisible(); // Vote status + await expect(page.getByText('Voting SystemSingle Choice')).toBeVisible(); // Vote mechanism + await expect( + page.getByText('Execution CriteriaAbsolute Majority'), + ).toBeVisible(); // Execution criteria + await expect(page.getByText('Creator0x78...91bF')).toBeVisible(); // Creator + await expect(page.getByText('Time Remaining-')).toBeVisible(); // Time remaining + + // Check proposal body + await expect( + page.getByText( + 'This proposal is to gain funding for purchasing a Moto and Whip', + ), + ).toBeVisible(); // description +});