diff --git a/govtool/backend/app/Main.hs b/govtool/backend/app/Main.hs index 4ba525ad0..522b40bb6 100644 --- a/govtool/backend/app/Main.hs +++ b/govtool/backend/app/Main.hs @@ -115,6 +115,7 @@ startApp vvaConfig sentryService = do $ setOnException (exceptionHandler vvaConfig sentryService) defaultSettings cacheEnv <- do let newCache = Cache.newCache (Just $ TimeSpec (fromIntegral (cacheDurationSeconds vvaConfig)) 0) + let newDRepListCache = Cache.newCache (Just $ TimeSpec (fromIntegral (dRepListCacheDurationSeconds vvaConfig)) 0) proposalListCache <- newCache getProposalCache <- newCache currentEpochCache <- newCache @@ -123,7 +124,7 @@ startApp vvaConfig sentryService = do dRepGetVotesCache <- newCache dRepInfoCache <- newCache dRepVotingPowerCache <- newCache - dRepListCache <- newCache + dRepListCache <- newDRepListCache networkMetricsCache <- newCache networkInfoCache <- newCache networkTotalStakeCache <- newCache diff --git a/govtool/backend/example-config.json b/govtool/backend/example-config.json index 800f7dde1..7cfb4d5f2 100644 --- a/govtool/backend/example-config.json +++ b/govtool/backend/example-config.json @@ -10,6 +10,7 @@ "port" : 9999, "host" : "localhost", "cachedurationseconds": 20, + "dreplistcachedurationseconds": 600, "sentrydsn": "https://username:password@senty.host/id", "sentryenv": "dev" } diff --git a/govtool/backend/sql/list-dreps.sql b/govtool/backend/sql/list-dreps.sql index 1ae00d6fa..5ba8e3250 100644 --- a/govtool/backend/sql/list-dreps.sql +++ b/govtool/backend/sql/list-dreps.sql @@ -32,6 +32,16 @@ LatestVoteEpoch AS ( JOIN tx ON tx.id = lvp.tx_id JOIN block ON block.id = tx.block_id ), +VotesLastYear AS ( + SELECT + vp.drep_voter AS drep_id, + COUNT(DISTINCT vp.gov_action_proposal_id) AS votes_last_year + FROM voting_procedure vp + JOIN tx ON tx.id = vp.tx_id + JOIN block ON block.id = tx.block_id + WHERE block.time >= now() - INTERVAL '1 year' + GROUP BY vp.drep_voter +), RankedDRepRegistration AS ( SELECT DISTINCT ON (dr.drep_hash_id) dr.id, @@ -127,6 +137,7 @@ DRepData AS ( off_chain_vote_drep_data.qualifications, off_chain_vote_drep_data.image_url, off_chain_vote_drep_data.image_hash, + COALESCE(vly.votes_last_year, 0) AS votes_last_year, COALESCE( ( SELECT jsonb_agg( @@ -239,6 +250,7 @@ DRepData AS ( LEFT JOIN tx AS tx_first_register ON tx_first_register.id = dr_first_register.tx_id LEFT JOIN block AS block_first_register ON block_first_register.id = tx_first_register.block_id LEFT JOIN LatestVoteEpoch lve ON lve.drep_id = dh.id + LEFT JOIN VotesLastYear vly ON vly.drep_id = dh.id CROSS JOIN DRepActivity GROUP BY dh.raw, @@ -265,6 +277,7 @@ DRepData AS ( off_chain_vote_drep_data.qualifications, off_chain_vote_drep_data.image_url, off_chain_vote_drep_data.image_hash, + vly.votes_last_year, ( SELECT jsonb_agg( jsonb_build_object( @@ -317,10 +330,6 @@ WHERE ( COALESCE(?, '') = '' OR (CASE WHEN LENGTH(?) % 2 = 0 AND ? ~ '^[0-9a-fA-F]+$' THEN drep_hash = ? ELSE false END) OR - view ILIKE ? OR - given_name ILIKE ? OR - payment_address ILIKE ? OR - objectives ILIKE ? OR - motivations ILIKE ? OR - qualifications ILIKE ? + (CASE WHEN lower(?) ~ '^drep1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]+$' THEN view = lower(?) ELSE FALSE END) OR + given_name ILIKE ? ) diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index 5185c3882..de95e6301 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -160,6 +160,7 @@ drepRegistrationToDrep Types.DRepRegistration {..} = dRepQualifications = dRepRegistrationQualifications, dRepImageUrl = dRepRegistrationImageUrl, dRepImageHash = HexText <$> dRepRegistrationImageHash, + dRepVotesLastYear = dRepRegistrationVotesLastYear, dRepIdentityReferences = DRepReferences <$> dRepRegistrationIdentityReferences, dRepLinkReferences = DRepReferences <$> dRepRegistrationLinkReferences } @@ -205,6 +206,8 @@ drepList mSearchQuery statuses mSortMode mPage mPageSize = do Just Random -> fmap snd . sortOn fst . Prelude.zip randomizedOrderList Just VotingPower -> sortOn $ \Types.DRepRegistration {..} -> Down dRepRegistrationVotingPower + Just Activity -> sortOn $ \Types.DRepRegistration {..} -> + Down dRepRegistrationVotesLastYear Just RegistrationDate -> sortOn $ \Types.DRepRegistration {..} -> Down dRepRegistrationLatestRegistrationDate Just Status -> sortOn $ \Types.DRepRegistration {..} -> diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index 2b1badf27..e0afad810 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -205,7 +205,7 @@ instance ToParamSchema GovernanceActionType where & enum_ ?~ map toJSON (enumFromTo minBound maxBound :: [GovernanceActionType]) -data DRepSortMode = Random | VotingPower | RegistrationDate | Status deriving +data DRepSortMode = Random | VotingPower | Activity | RegistrationDate | Status deriving ( Bounded , Enum , Eq @@ -917,6 +917,7 @@ data DRep , dRepQualifications :: Maybe Text , dRepImageUrl :: Maybe Text , dRepImageHash :: Maybe HexText + , dRepVotesLastYear :: Maybe Integer , dRepIdentityReferences :: Maybe DRepReferences , dRepLinkReferences :: Maybe DRepReferences } @@ -944,6 +945,7 @@ exampleDrep = <> "\"qualifications\": \"Some Qualifications\"," <> "\"qualifications\": \"Some Qualifications\"," <> "\"imageUrl\": \"https://image.url\"," + <> "\"votesLastYear\": 15," <> "\"imageHash\": \"9198b1b204273ba5c67a13310b5a806034160f6a063768297e161d9b759cad61\"}" -- ToSchema instance for DRep diff --git a/govtool/backend/src/VVA/Config.hs b/govtool/backend/src/VVA/Config.hs index 997a3c231..b11358f3d 100644 --- a/govtool/backend/src/VVA/Config.hs +++ b/govtool/backend/src/VVA/Config.hs @@ -68,19 +68,21 @@ instance DefaultConfig DBConfig where data VVAConfigInternal = VVAConfigInternal { -- | db-sync database access. - vVAConfigInternalDbsyncconfig :: DBConfig + vVAConfigInternalDbsyncconfig :: DBConfig -- | Server port. - , vVAConfigInternalPort :: Int + , vVAConfigInternalPort :: Int -- | Server host. - , vVAConfigInternalHost :: Text + , vVAConfigInternalHost :: Text -- | Request cache duration - , vVaConfigInternalCacheDurationSeconds :: Int + , vVaConfigInternalCacheDurationSeconds :: Int + -- | DRep List request cache duration + , vVaConfigInternalDRepListCacheDurationSeconds :: Int -- | Sentry DSN - , vVAConfigInternalSentrydsn :: String + , vVAConfigInternalSentrydsn :: String -- | Sentry environment - , vVAConfigInternalSentryEnv :: String + , vVAConfigInternalSentryEnv :: String -- | Pinata API JWT - , vVAConfigInternalPinataApiJwt :: Maybe Text + , vVAConfigInternalPinataApiJwt :: Maybe Text } deriving (FromConfig, Generic, Show) @@ -92,6 +94,7 @@ instance DefaultConfig VVAConfigInternal where vVAConfigInternalPort = 3000, vVAConfigInternalHost = "localhost", vVaConfigInternalCacheDurationSeconds = 20, + vVaConfigInternalDRepListCacheDurationSeconds = 600, vVAConfigInternalSentrydsn = "https://username:password@senty.host/id", vVAConfigInternalSentryEnv = "development", vVAConfigInternalPinataApiJwt = Nothing @@ -101,19 +104,21 @@ instance DefaultConfig VVAConfigInternal where data VVAConfig = VVAConfig { -- | db-sync database credentials. - dbSyncConnectionString :: Text + dbSyncConnectionString :: Text -- | Server port. - , serverPort :: Int + , serverPort :: Int -- | Server host. - , serverHost :: Text + , serverHost :: Text -- | Request cache duration - , cacheDurationSeconds :: Int + , cacheDurationSeconds :: Int + -- | DRep List request cache duration + , dRepListCacheDurationSeconds :: Int -- | Sentry DSN - , sentryDSN :: String + , sentryDSN :: String -- | Sentry environment - , sentryEnv :: String + , sentryEnv :: String -- | Pinata API JWT - , pinataApiJwt :: Maybe Text + , pinataApiJwt :: Maybe Text } deriving (Generic, Show, ToJSON) @@ -153,6 +158,7 @@ convertConfig VVAConfigInternal {..} = serverPort = vVAConfigInternalPort, serverHost = vVAConfigInternalHost, cacheDurationSeconds = vVaConfigInternalCacheDurationSeconds, + dRepListCacheDurationSeconds = vVaConfigInternalDRepListCacheDurationSeconds, sentryDSN = vVAConfigInternalSentrydsn, sentryEnv = vVAConfigInternalSentryEnv, pinataApiJwt = vVAConfigInternalPinataApiJwt diff --git a/govtool/backend/src/VVA/DRep.hs b/govtool/backend/src/VVA/DRep.hs index 39342a7eb..44b172f7f 100644 --- a/govtool/backend/src/VVA/DRep.hs +++ b/govtool/backend/src/VVA/DRep.hs @@ -59,6 +59,7 @@ data DRepQueryResult , queryQualifications :: Maybe Text , queryImageUrl :: Maybe Text , queryImageHash :: Maybe Text + , queryVotesLastYear :: Maybe Integer , queryIdentityReferences :: Maybe Value , queryLinkReferences :: Maybe Value } @@ -69,7 +70,7 @@ instance FromRow DRepQueryResult where <$> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field - <*> field <*> field <*> field <*> field + <*> field <*> field <*> field <*> field <*> field sqlFrom :: ByteString -> SQL.Query sqlFrom bs = fromString $ unpack $ Text.decodeUtf8 bs @@ -86,12 +87,9 @@ listDReps mSearchQuery = withPool $ \conn -> do , searchParam -- LENGTH(?) , searchParam -- AND ? , searchParam -- decode(?, 'hex') - , "%" <> searchParam <> "%" -- dh.view + , searchParam -- lower(?) + , searchParam -- lower(?) , "%" <> searchParam <> "%" -- given_name - , "%" <> searchParam <> "%" -- payment_address - , "%" <> searchParam <> "%" -- objectives - , "%" <> searchParam <> "%" -- motivations - , "%" <> searchParam <> "%" -- qualifications ) :: IO [DRepQueryResult]) timeZone <- liftIO getCurrentTimeZone @@ -116,6 +114,7 @@ listDReps mSearchQuery = withPool $ \conn -> do (queryQualifications result) (queryImageUrl result) (queryImageHash result) + (queryVotesLastYear result) (queryIdentityReferences result) (queryLinkReferences result) | result <- results diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index 98fb7b44e..9ace0e5bc 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -160,6 +160,7 @@ data DRepRegistration , dRepRegistrationQualifications :: Maybe Text , dRepRegistrationImageUrl :: Maybe Text , dRepRegistrationImageHash :: Maybe Text + , dRepRegistrationVotesLastYear :: Maybe Integer , dRepRegistrationIdentityReferences :: Maybe Value , dRepRegistrationLinkReferences :: Maybe Value } @@ -187,6 +188,7 @@ instance FromRow DRepRegistration where <*> field -- dRepRegistrationQualifications <*> field -- dRepRegistrationImageUrl <*> field -- dRepRegistrationImageHash + <*> field -- dRepRegistrationVotesLastYear <*> field -- dRepRegistrationIdentityReferences <*> field -- dRepRegistrationLinkReferences diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index 3d9f85152..0d75d1dca 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -13,7 +13,7 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.6", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.7", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "1.0.13-beta", "@mui/icons-material": "^5.14.3", @@ -3392,9 +3392,9 @@ } }, "node_modules/@intersect.mbo/govtool-outcomes-pillar-ui": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.6.tgz", - "integrity": "sha512-bRo58lf/amigBS1Jp3xhAs1IH8HsSRFCGBahBKl1avon+OIowKn9LgStqoQemfdFDq6HSb9oWJPd5p21SjxLew==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.7.tgz", + "integrity": "sha512-GGJCDTkwOb4T1DHZ0lQ2RzNL/BoY3y/+wkrpLIoJLtMaoVsa5i41JSx250By8gvVfmmpaLcnwhShQNkJsPRG1w==", "license": "ISC", "dependencies": { "@fontsource/poppins": "^5.0.14", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index dcc5d13b8..ad031c36c 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -27,7 +27,7 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.6", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.7", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "1.0.13-beta", "@mui/icons-material": "^5.14.3", diff --git a/govtool/frontend/src/components/molecules/DRepDataForm.tsx b/govtool/frontend/src/components/molecules/DRepDataForm.tsx index 6b2a65506..e5b077b49 100644 --- a/govtool/frontend/src/components/molecules/DRepDataForm.tsx +++ b/govtool/frontend/src/components/molecules/DRepDataForm.tsx @@ -107,7 +107,7 @@ export const DRepDataForm = ({ control, errors, register, watch }: Props) => { subtitle={t("forms.dRepData.imageHelpfulText")} /> >; setChosenSorting: Dispatch>; setFiltersOpen?: Dispatch>; @@ -51,6 +53,7 @@ export const DataActionsBar: FC = ({ ...props }) => { setSortOpen, sortOpen, sortOptions = [], + placeholder = "Search...", } = props; const { palette: { boxShadow2 }, @@ -61,7 +64,7 @@ export const DataActionsBar: FC = ({ ...props }) => { setSearchText(e.target.value)} - placeholder="Search..." + placeholder={placeholder} value={searchText} startAdornment={ = ({ ...props }) => { }} /> } + endAdornment={ + searchText && ( + setSearchText("")} + sx={{ ml: 1 }} + > + + + ) + } sx={{ bgcolor: "white", border: 1, diff --git a/govtool/frontend/src/components/molecules/PaginationFooter.tsx b/govtool/frontend/src/components/molecules/PaginationFooter.tsx new file mode 100644 index 000000000..55f66cabf --- /dev/null +++ b/govtool/frontend/src/components/molecules/PaginationFooter.tsx @@ -0,0 +1,158 @@ +import { + Box, + Typography, + IconButton, + Select, + MenuItem, + SelectChangeEvent, +} from "@mui/material"; +import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import { FC, useState } from "react"; + +type Props = { + page: number; + total: number; + pageSize: number; + onPageChange: (nextPage: number) => void; + onPageSizeChange: (nextRpp: number) => void; + pageSizeOptions?: number[]; +}; + +export const PaginationFooter: FC = ({ + page, + total, + pageSize, + onPageChange, + onPageSizeChange, + pageSizeOptions = [5, 10, 25, 50], +}) => { + const pageCount = Math.max(1, Math.ceil((total || 0) / (pageSize || 1))); + const clampedPage = Math.min(Math.max(page, 1), pageCount); + + const start = total === 0 ? 0 : (clampedPage - 1) * pageSize + 1; + const end = total === 0 ? 0 : Math.min(clampedPage * pageSize, total); + + const handlePrev = () => onPageChange(Math.max(clampedPage - 1, 1)); + const handleNext = () => onPageChange(Math.min(clampedPage + 1, pageCount)); + + const handlePageSizeChange = (e: SelectChangeEvent) => { + const next = Number(e.target.value); + onPageSizeChange(next); + + const nextPageCount = Math.max(1, Math.ceil((total || 0) / next)); + if (clampedPage > nextPageCount) { + onPageChange(nextPageCount); + } + }; + + const [open, setOpen] = useState(false); + + const ArrowIcon: React.FC> = (props,) => ( + ) => { + e.preventDefault(); + e.stopPropagation(); + setOpen(true); + }} + /> + ); + + return ( + + + + Rows per page: + + + + + + + {start}-{end} of {total} + + + + + + + + + {clampedPage} + + + = pageCount || total === 0} + aria-label="Next page" + > + + + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index 25c1807d2..6a49ac90b 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -275,7 +275,7 @@ export const VoteActionForm = ({ border: !showWholeVoteContext ? "1px solid #E1E1E1" : "none", borderRadius: "4px", backgroundColor: !showWholeVoteContext ? fadedPurple.c50 : "transparent", - padding: 2, + padding: 2 }} > {finalVoteContextText} @@ -316,7 +317,7 @@ export const VoteActionForm = ({ }} disableRipple variant="text" - data-testid="external-modal-button" + data-testid="show-more-button" > ({ /> {fieldState.error && ( )} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx index a108b3261..86e128c86 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -70,6 +70,7 @@ export const VoteContextChoice = ({ variant="outlined" onClick={handleLetGovToolStore} sx={{ width: isMobile ? "100%" : "259px", whiteSpace: "nowrap", height: "48px", fontWeight: "500" }} + data-testid="govtool-pins-data-to-ipfs-option-button" > {t("createGovernanceAction.govToolPinsDataToIPFS")} @@ -77,6 +78,7 @@ export const VoteContextChoice = ({ variant="outlined" onClick={handleStoreItMyself} sx={{ width: isMobile ? "100%" : "287px", whiteSpace: "nowrap", height: "48px", fontWeight: "500" }} + data-testid="download-and-store-yourself-option-button" > {t("createGovernanceAction.downloadAndStoreYourself")} diff --git a/govtool/frontend/src/consts/dRepDirectory/sorting.ts b/govtool/frontend/src/consts/dRepDirectory/sorting.ts index 0e6c4d016..2ae238b15 100644 --- a/govtool/frontend/src/consts/dRepDirectory/sorting.ts +++ b/govtool/frontend/src/consts/dRepDirectory/sorting.ts @@ -1,7 +1,7 @@ export const DREP_DIRECTORY_SORTING = [ { - key: "Random", - label: "Random", + key: "Activity", + label: "Activity", }, { key: "RegistrationDate", diff --git a/govtool/frontend/src/context/contextProviders.tsx b/govtool/frontend/src/context/contextProviders.tsx index f456018bb..7337418b1 100644 --- a/govtool/frontend/src/context/contextProviders.tsx +++ b/govtool/frontend/src/context/contextProviders.tsx @@ -3,6 +3,7 @@ import { CardanoProvider, useCardano } from "./wallet"; import { ModalProvider, useModal } from "./modal"; import { SnackbarProvider, useSnackbar } from "./snackbar"; import { DataActionsBarProvider } from "./dataActionsBar"; +import { PaginationProvider } from "./pagination"; import { FeatureFlagProvider } from "./featureFlag"; import { GovernanceActionProvider } from "./governanceAction"; import { AdaHandleProvider } from "./adaHandle"; @@ -23,11 +24,13 @@ const ContextProviders = ({ children }: Props) => ( - - - {children} - - + + + + {children} + + + diff --git a/govtool/frontend/src/context/dataActionsBar.tsx b/govtool/frontend/src/context/dataActionsBar.tsx index e2776346c..2bf291edd 100644 --- a/govtool/frontend/src/context/dataActionsBar.tsx +++ b/govtool/frontend/src/context/dataActionsBar.tsx @@ -24,6 +24,7 @@ interface DataActionsBarContextType { debouncedSearchText: string; filtersOpen: boolean; searchText: string; + lastPath: string; setChosenFilters: Dispatch>; setChosenSorting: Dispatch>; setFiltersOpen: Dispatch>; @@ -120,6 +121,7 @@ const DataActionsBarProvider: FC = ({ children }) => { debouncedSearchText, filtersOpen, searchText, + lastPath, setChosenFilters, setChosenSorting, setFiltersOpen, diff --git a/govtool/frontend/src/context/index.ts b/govtool/frontend/src/context/index.ts index 0ece5a1b5..6914a1289 100644 --- a/govtool/frontend/src/context/index.ts +++ b/govtool/frontend/src/context/index.ts @@ -1,6 +1,7 @@ export * from "./appContext"; export * from "./contextProviders"; export * from "./dataActionsBar"; +export * from "./pagination"; export * from "./modal"; export * from "./pendingTransaction"; export * from "./snackbar"; diff --git a/govtool/frontend/src/context/pagination.tsx b/govtool/frontend/src/context/pagination.tsx new file mode 100644 index 000000000..f979ec00a --- /dev/null +++ b/govtool/frontend/src/context/pagination.tsx @@ -0,0 +1,56 @@ +import React, { + createContext, + useContext, + Dispatch, + SetStateAction, + FC, + useState, + useMemo, +} from "react"; + +type PaginationContextType = { + page: number; + pageSize: number; + setPage: Dispatch>; + setPageSize: Dispatch>; +}; + +const PaginationContext = createContext< + PaginationContextType | undefined>( + undefined); +PaginationContext.displayName = "PaginationContext"; + +type PaginationProviderProps = { + children: React.ReactNode; +}; + +const PaginationProvider: FC = ({ children }) => { + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(5); + + const contextValue = useMemo( + () => ({ + page, + pageSize, + setPage, + setPageSize, + }), + [page, pageSize], + ); + + return ( + + {children} + + ); +}; + +function usePagination() { + const ctx = useContext(PaginationContext); + if (!ctx) { + throw new Error("usePagination must be used within a PaginationProvider"); + } + return ctx; +} + +export { PaginationProvider, usePagination }; diff --git a/govtool/frontend/src/hooks/queries/useGetDRepListInfiniteQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepListInfiniteQuery.ts new file mode 100644 index 000000000..51d6930e4 --- /dev/null +++ b/govtool/frontend/src/hooks/queries/useGetDRepListInfiniteQuery.ts @@ -0,0 +1,70 @@ +import { UseInfiniteQueryOptions, useInfiniteQuery } from "react-query"; + +import { QUERY_KEYS } from "@consts"; +import { useCardano } from "@context"; +import { GetDRepListArguments, getDRepList } from "@services"; +import { DRepData, Infinite } from "@/models"; + +export const useGetDRepListInfiniteQuery = ( + { + filters = [], + pageSize = 10, + searchPhrase, + sorting, + status, + }: GetDRepListArguments, + options?: UseInfiniteQueryOptions>, +) => { + const { pendingTransaction } = useCardano(); + + const { + data, + isLoading, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + isPreviousData, + } = useInfiniteQuery( + [ + QUERY_KEYS.useGetDRepListInfiniteKey, + ( + pendingTransaction.registerAsDirectVoter || + pendingTransaction.registerAsDrep || + pendingTransaction.retireAsDirectVoter || + pendingTransaction.retireAsDrep + )?.transactionHash ?? "noPendingTransaction", + filters.length ? filters : "", + searchPhrase ?? "", + sorting ?? "", + status?.length ? status : "", + ], + async ({ pageParam = 0 }) => + getDRepList({ + page: pageParam, + pageSize, + filters, + searchPhrase, + sorting, + status, + }), + { + getNextPageParam: (lastPage) => { + if (lastPage.elements.length === 0) return undefined; + return lastPage.page + 1; + }, + enabled: options?.enabled, + keepPreviousData: options?.keepPreviousData, + }, + ); + + return { + dRepListFetchNextPage: fetchNextPage, + dRepListHasNextPage: hasNextPage, + isDRepListFetching: isFetching, + isDRepListFetchingNextPage: isFetchingNextPage, + isDRepListLoading: isLoading, + dRepData: data?.pages.flatMap((page) => page.elements), + isPreviousData, + }; +}; diff --git a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts index c6f0f968b..db62f9c09 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts @@ -1,47 +1,64 @@ -import { UseInfiniteQueryOptions, useInfiniteQuery } from "react-query"; +import { useMemo } from "react"; +import { useQuery, UseQueryOptions, useQueryClient } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; import { GetDRepListArguments, getDRepList } from "@services"; import { DRepData, Infinite } from "@/models"; -export const useGetDRepListInfiniteQuery = ( - { - filters = [], - pageSize = 10, - searchPhrase, - sorting, - status, - }: GetDRepListArguments, - options?: UseInfiniteQueryOptions>, -) => { +const makeStatusKey = (status?: string[] | undefined) => + (status && status.length ? [...status].sort().join("|") : "__EMPTY__"); + +type PaginatedResult = { + dRepData: DRepData[] | undefined; + isLoading: boolean; + isFetching: boolean; + isPreviousData: boolean; + total: number | undefined; + baselineTotalForStatus: number | undefined; +}; + +type Args = GetDRepListArguments & { + page: number; + pageSize?: number; +}; + +export function useGetDRepListPaginatedQuery( + { page, pageSize = 10, filters = [], searchPhrase, sorting, status }: Args, + options?: UseQueryOptions>, +): PaginatedResult { const { pendingTransaction } = useCardano(); + const queryClient = useQueryClient(); - const { - data, - isLoading, - fetchNextPage, - hasNextPage, - isFetching, - isFetchingNextPage, - isPreviousData, - } = useInfiniteQuery( - [ - QUERY_KEYS.useGetDRepListInfiniteKey, - ( - pendingTransaction.registerAsDirectVoter || - pendingTransaction.registerAsDrep || - pendingTransaction.retireAsDirectVoter || - pendingTransaction.retireAsDrep - )?.transactionHash ?? "noPendingTransaction", - filters.length ? filters : "", - searchPhrase ?? "", - sorting ?? "", - status?.length ? status : "", - ], - async ({ pageParam = 0 }) => + const statusKey = useMemo(() => makeStatusKey(status), [status]); + + const listKey = [ + QUERY_KEYS.useGetDRepListInfiniteKey, + ( + pendingTransaction.registerAsDirectVoter || + pendingTransaction.registerAsDrep || + pendingTransaction.retireAsDirectVoter || + pendingTransaction.retireAsDrep + )?.transactionHash ?? "noPendingTransaction", + "paged", + page, + pageSize, + filters.length ? filters : "", + searchPhrase ?? "", + sorting ?? "", + status?.length ? status : "", + ]; + + const baselineKey = useMemo( + () => [QUERY_KEYS.useGetDRepListInfiniteKey, "baseline", statusKey], + [statusKey], + ); + + const { data, isLoading, isFetching, isPreviousData } = useQuery( + listKey, + () => getDRepList({ - page: pageParam, + page, pageSize, filters, searchPhrase, @@ -49,25 +66,45 @@ export const useGetDRepListInfiniteQuery = ( status, }), { - getNextPageParam: (lastPage) => { - if (lastPage.elements.length === 0) { - return undefined; + keepPreviousData: true, + enabled: options?.enabled, + onSuccess: (resp) => { + if (!searchPhrase && typeof resp?.total === "number") { + queryClient.setQueryData(baselineKey, resp); } - - return lastPage.page + 1; + options?.onSuccess?.(resp); }, - enabled: options?.enabled, - keepPreviousData: options?.keepPreviousData, + }, + ); + + const { data: baselineResp } = useQuery( + baselineKey, + async () => + getDRepList({ + page: 0, + pageSize: 1, + filters, + searchPhrase: "", + sorting, + status, + }), + { + initialData: () => + queryClient.getQueryData>(baselineKey), + enabled: + options?.enabled && + !queryClient.getQueryData(baselineKey) && + searchPhrase !== "", + staleTime: Infinity, }, ); return { - dRepListFetchNextPage: fetchNextPage, - dRepListHasNextPage: hasNextPage, - isDRepListFetching: isFetching, - isDRepListFetchingNextPage: isFetchingNextPage, - isDRepListLoading: isLoading, - dRepData: data?.pages.flatMap((page) => page.elements), + dRepData: data?.elements, + isLoading, + isFetching, isPreviousData, + total: data?.total, + baselineTotalForStatus: baselineResp?.total, }; -}; +} diff --git a/govtool/frontend/src/hooks/queries/useGetDrepDetailsQuery.ts b/govtool/frontend/src/hooks/queries/useGetDrepDetailsQuery.ts index 3c99959ff..d4e39aefa 100644 --- a/govtool/frontend/src/hooks/queries/useGetDrepDetailsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDrepDetailsQuery.ts @@ -1,7 +1,7 @@ import { UseInfiniteQueryOptions } from "react-query"; import { Infinite, DRepData } from "@/models"; -import { useGetDRepListInfiniteQuery } from "./useGetDRepListQuery"; +import { useGetDRepListInfiniteQuery } from "./useGetDRepListInfiniteQuery"; export const useGetDRepDetailsQuery = ( dRepId: string | null | undefined, diff --git a/govtool/frontend/src/hooks/useUpdateEffect.ts b/govtool/frontend/src/hooks/useUpdateEffect.ts new file mode 100644 index 000000000..192b2a96d --- /dev/null +++ b/govtool/frontend/src/hooks/useUpdateEffect.ts @@ -0,0 +1,16 @@ +import React, { useEffect, useRef } from "react"; + +export function useUpdateEffect( + effect: React.EffectCallback, + deps: React.DependencyList, +) { + const isFirst = useRef(true); + + useEffect(() => { + if (isFirst.current) { + isFirst.current = false; + return; + } + return effect(); + }, deps); +} diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index cca6a27e3..c4b054471 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -314,6 +314,7 @@ "myDelegationToYourself": "You have delegated ₳ {{ada}} to yourself", "myDRep": "You have delegated ₳ {{ada}} to this DRep", "listTitle": "Find a DRep", + "searchBarPlaceholder": "Search for a DRep name or ID", "noConfidenceDefaultDescription": "Select this to signal no confidence in the current constitutional committee by voting NO on every proposal and voting YES to no confidence proposals", "noConfidenceDefaultTitle": "Signal No Confidence on Every Vote", "noResultsForTheSearchTitle": "No DReps found", diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index 8ba3a6a32..4f48125a6 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -144,7 +144,7 @@ export enum DRepStatus { } export enum DRepListSort { - Random = "Random", + Activity = "Activity", VotingPower = "VotingPower", RegistrationDate = "RegistrationDate", Status = "Status", diff --git a/govtool/frontend/src/pages/DRepDirectoryContent.tsx b/govtool/frontend/src/pages/DRepDirectoryContent.tsx index 47b1b1006..d66588127 100644 --- a/govtool/frontend/src/pages/DRepDirectoryContent.tsx +++ b/govtool/frontend/src/pages/DRepDirectoryContent.tsx @@ -1,16 +1,16 @@ -import { FC, useEffect, useState } from "react"; +import React, { FC, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Box, CircularProgress } from "@mui/material"; -import { Button, Typography } from "@atoms"; +import { Typography } from "@atoms"; import { DREP_DIRECTORY_FILTERS, DREP_DIRECTORY_SORTING } from "@consts"; -import { useCardano, useDataActionsBar } from "@context"; +import { useCardano, useDataActionsBar, usePagination } from "@context"; import { useDelegateTodRep, useGetAdaHolderCurrentDelegationQuery, useGetAdaHolderVotingPowerQuery, useGetDRepDetailsQuery, - useGetDRepListInfiniteQuery, + useGetDRepListPaginatedQuery, } from "@hooks"; import { DataActionsBar, EmptyStateDrepDirectory } from "@molecules"; import { AutomatedVotingOptions, DRepCard } from "@organisms"; @@ -26,6 +26,8 @@ import { AutomatedVotingOptionDelegationId, } from "@/types/automatedVotingOptions"; import usePrevious from "@/hooks/usePrevious"; +import { PaginationFooter } from "@/components/molecules/PaginationFooter"; +import { useUpdateEffect } from "@/hooks/useUpdateEffect"; interface DRepDirectoryContentProps { isConnected?: boolean; @@ -55,9 +57,12 @@ export const DRepDirectoryContent: FC = ({ searchText, debouncedSearchText, setSearchText, + lastPath, ...dataActionsBarProps } = useDataActionsBar(); + const { page, pageSize, setPage, setPageSize } = usePagination(); + const { chosenFilters, chosenSorting, setChosenFilters, setChosenSorting } = dataActionsBarProps; @@ -66,14 +71,20 @@ export const DRepDirectoryContent: FC = ({ // Set initial filters and sort useEffect(() => { - setChosenFilters([DRepStatus.Active]); - setSearchText(""); // <--- Clear the search field on mount + if (!lastPath.includes("drep_directory")) { + setChosenFilters([DRepStatus.Active]); + setSearchText(""); + } }, []); useEffect(() => { - if (!chosenSorting) setChosenSorting(DRepListSort.Random); + if (!chosenSorting) setChosenSorting(DRepListSort.Activity); }, [chosenSorting, setChosenSorting]); + useUpdateEffect(() => { + setPage(1); + }, [debouncedSearchText, chosenSorting, JSON.stringify(chosenFilters)]); + const { delegate, isDelegating } = useDelegateTodRep(); const { votingPower } = useGetAdaHolderVotingPowerQuery(stakeKey); @@ -87,21 +98,26 @@ export const DRepDirectoryContent: FC = ({ const { dRepData: dRepList, - isPreviousData, - dRepListHasNextPage, - dRepListFetchNextPage, - } = useGetDRepListInfiniteQuery( + isFetching, + isPreviousData: isPrev, + total, + baselineTotalForStatus, + } = useGetDRepListPaginatedQuery( { + page: page - 1, // convert 1-based UI -> 0-based API + pageSize, searchPhrase: debouncedSearchText, sorting: chosenSorting as DRepListSort, status: chosenFilters as DRepStatus[], }, - { - enabled: !!chosenSorting, - keepPreviousData: true, - }, + { enabled: !!chosenSorting }, ); + const showSearchSummary = + searchText !== "" && + (!isFetching || !isPrev) && + total !== baselineTotalForStatus; + useEffect(() => { if (!inProgressDelegation && prevInProgressDelegation) { setInProgressDelegationDRepData(undefined); @@ -212,16 +228,42 @@ export const DRepDirectoryContent: FC = ({ filterOptions={DREP_DIRECTORY_FILTERS} filtersTitle={t("dRepDirectory.filterTitle")} sortOptions={DREP_DIRECTORY_SORTING} + placeholder={t("dRepDirectory.searchBarPlaceholder")} /> + + {showSearchSummary && ( + + 1 ? "s" : "", + total: baselineTotalForStatus ?? "", + }} + components={{ + total: + baselineTotalForStatus === undefined ? ( + + ) : ( + + ), + }} + /> + + )} = ({ ))} + + { + setPageSize(n); + setPage(1); + }} + /> - {dRepListHasNextPage && dRepList.length >= 10 && ( - - - - )} ); }; diff --git a/tests/govtool-frontend/playwright/lib/forms/dRepForm.ts b/tests/govtool-frontend/playwright/lib/forms/dRepForm.ts index 63516b43a..74d6fe61c 100644 --- a/tests/govtool-frontend/playwright/lib/forms/dRepForm.ts +++ b/tests/govtool-frontend/playwright/lib/forms/dRepForm.ts @@ -13,7 +13,7 @@ const formErrors = { ], linkDescription: "max-80-characters-error", email: "invalid-email-address-error", - image: "invalid-image-url-error", + image: "invalid-image-input-error", links: { url: "link-reference-description-1-error", description: "link-reference-description-1-error", @@ -304,7 +304,7 @@ export default class DRepForm { }).not.toEqual(dRepInfo.qualifications); await expect(this.form.getByTestId(formErrors.image), { - message: !isImageErrorVisible && `${dRepInfo.image} is a valid image`, + message: !isImageErrorVisible && `Invalid image URL or properly formatted base64-encoded image`, }).toBeVisible({ timeout: 60_000, }); diff --git a/tests/govtool-frontend/playwright/lib/helpers/waitedLoop.ts b/tests/govtool-frontend/playwright/lib/helpers/waitedLoop.ts index cbe537a37..45e04ad33 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/waitedLoop.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/waitedLoop.ts @@ -9,7 +9,7 @@ export async function waitedLoop( const startTime = Date.now(); while (Date.now() - startTime < timeout) { if (await conditionFn()) return true; - Logger.info("Retring the function"); + Logger.info("Retrying the function"); await new Promise((resolve) => setTimeout(resolve, interval)); } return false; @@ -36,9 +36,10 @@ export async function functionWaitedAssert( } catch (error) { if (Date.now() - startTime >= timeout) { const errorMessage = options.message || error.message; + console.log(errorMessage); expect(false, { message: errorMessage }).toBe(true); } - Logger.info(`Retring the function ${name}`); + Logger.info(`Retrying the function ${name}`); await new Promise((resolve) => setTimeout(resolve, interval)); } } diff --git a/tests/govtool-frontend/playwright/lib/pages/governanceActionDetailsPage.ts b/tests/govtool-frontend/playwright/lib/pages/governanceActionDetailsPage.ts index d6a8713a4..2dea0a34f 100644 --- a/tests/govtool-frontend/playwright/lib/pages/governanceActionDetailsPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/governanceActionDetailsPage.ts @@ -21,7 +21,6 @@ export default class GovernanceActionDetailsPage { readonly externalModalBtn = this.page.getByTestId("external-modal-button"); readonly governanceActionId = this.page.getByText("Governance Action ID:"); - readonly contextBtn = this.page.getByTestId("provide-context-button"); readonly metadataDownloadBtn = this.page.getByTestId( "metadata-download-button" ); @@ -31,6 +30,9 @@ export default class GovernanceActionDetailsPage { readonly continueModalBtn = this.page.getByTestId("continue-modal-button"); readonly confirmModalBtn = this.page.getByTestId("confirm-modal-button"); + readonly downloadAndStoreYourselfOptionBtn = this.page.getByTestId("download-and-store-yourself-option-button") + readonly govtoolPinsDatatoIpfsBtn = this.page.getByTestId("govtool-pins-data-to-ipfs-option-button") + readonly voteSuccessModal = this.page.getByTestId("alert-success"); readonly externalLinkModal = this.page.getByTestId("external-link-modal"); @@ -76,37 +78,41 @@ export default class GovernanceActionDetailsPage { } @withTxConfirmation - async vote(context?: string, isAlreadyVoted: boolean = false) { + async vote(context?: string, isAlreadyVoted: boolean = false , useIPFSforStorage : boolean = false ){ if (!isAlreadyVoted) { await this.yesVoteRadio.click(); } + await this.voteBtn.click() + if (context) { - await this.contextBtn.click(); await this.contextInput.fill(context); - await this.confirmModalBtn.click(); - await this.page.getByRole("checkbox").click(); - await this.confirmModalBtn.click(); - - this.metadataDownloadBtn.click(); - const voteMetadata = await this.downloadVoteMetadata(); - const url = await metadataBucketService.uploadMetadata( - voteMetadata.name, - voteMetadata.data - ); - await this.metadataUrlInput.fill(url); + this.confirmModalBtn.click() + + if (useIPFSforStorage) { + await this.govtoolPinsDatatoIpfsBtn.click() + } else { + + await this.downloadAndStoreYourselfOptionBtn.click() + await this.page.getByRole("checkbox").click(); + await this.confirmModalBtn.click(); + + this.metadataDownloadBtn.click(); + const voteMetadata = await this.downloadVoteMetadata(); + const url = await metadataBucketService.uploadMetadata( + voteMetadata.name, + voteMetadata.data + ); + await this.metadataUrlInput.fill(url); + } await this.confirmModalBtn.click(); await this.page.getByTestId("go-to-vote-modal-button").click(); - } - - const isVoteButtonEnabled = await this.voteBtn.isEnabled(); - - await expect(this.voteBtn, { - message: !isVoteButtonEnabled && "Vote button is not enabled", - }).toBeEnabled({ timeout: 60_000 }); - await this.voteBtn.click(); + } + else { + await this.confirmModalBtn.click() + } } async getDRepNotVoted( @@ -165,5 +171,6 @@ export default class GovernanceActionDetailsPage { async reVote() { await this.noVoteRadio.click(); await this.changeVoteBtn.click(); + await this.confirmModalBtn.click(); } } diff --git a/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts b/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts index d096b2965..132b22479 100644 --- a/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts @@ -55,9 +55,9 @@ export default class GovernanceActionsPage { async viewFirstProposalByGovernanceAction( governanceAction: GovernanceActionType ): Promise { - const proposalCard = this.page - .getByTestId(`govaction-${governanceAction}-card`) - .first(); + const proposalCard = this.page + .locator('[data-testid^="govaction-"][data-testid$="-card"]') + .first(); const isVisible = await proposalCard.isVisible(); @@ -76,6 +76,20 @@ export default class GovernanceActionsPage { } } + async getFirstProposal( + ) { + await functionWaitedAssert( + async () => { + const proposalCard = this.page + .locator('[data-testid^="govaction-"][data-testid$="-card"]') + .first(); + + await expect(proposalCard + .locator('[data-testid^="govaction-"][data-testid$="-view-detail"]') + .first()).toBeVisible() + }, { name: "Retrying to get the first proposal" }); + } + async viewVotedProposal( proposal: IProposal ): Promise { @@ -122,7 +136,7 @@ export default class GovernanceActionsPage { expect( hasFilter, hasFilter == false && - `A proposal card does not contain any of the ${filters}` + `A proposal card does not contain any of the ${filters}` ).toBe(true); } } diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts index cbf39d9ca..6ead1d8be 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts @@ -111,7 +111,7 @@ export default class OutcomeDetailsPage { } const outcomeResponse = await outcomeResponsePromise; - const proposalToCheck = (await outcomeResponse.json())[0]; + const proposalToCheck = (await outcomeResponse.json()); const metricsResponse = await metricsResponsePromise; @@ -188,7 +188,7 @@ export default class OutcomeDetailsPage { filterKey === "NoConfidence" ? proposalToCheck.pool_no_votes : parseInt(sPosNoConfidence.replace(/,/g, "")) * 1000000 + - parseInt(proposalToCheck.pool_no_votes); + parseInt(proposalToCheck.pool_no_votes); const totalSposYesVotesForNoConfidence = parseInt(sPosNoConfidence.replace(/,/g, "")) * 1000000 + @@ -295,19 +295,24 @@ export default class OutcomeDetailsPage { url: string; hash: string; }) { - await this.page.route(/.*\/governance-actions\/[a-f0-9]{64}\?.*/, (route) => - route.fulfill({ body: JSON.stringify([outcomeResponse]) }) + let governanceActionPromise = this.page.route("**/governance-actions/*", async (route) => { + if (route.request().url().includes("/governance-actions/metadata")) { + await route.continue(); + } else { + await route.fulfill({ body: JSON.stringify(outcomeResponse)}); + } + } ); - const outcomePage = new OutComesPage(this.page); await outcomePage.goto(); await outcomePage.viewFirstOutcomes(); + await governanceActionPromise; const outcomeTitle = await outcomePage.title.textContent(); await expect( outcomePage.title, outcomeTitle.toLowerCase() !== type.toLowerCase() && - `The URL "${url}" and hash "${hash}" do not match the expected properties for type "${type}".` + `The URL "${url}" and hash "${hash}" do not match the expected properties for type "${type}".` ).toHaveText(type, { ignoreCase: true, timeout: 60_000, diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts index 97d146349..32fe6dd98 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -579,7 +579,7 @@ export default class OutComesPage { const metricsResponsePromise = page.waitForResponse( (response) => response.url().includes(`/misc/network/metrics?epoch`), - { timeout: 60_000 } + { timeout: 120_000 } ); expect( @@ -597,7 +597,7 @@ export default class OutComesPage { .includes( `governance-actions/${governanceTransactionHash}?index=${governanceActionIndex}` ), - { timeout: 60_000 } + { timeout: 120_000 } ); const govActionDetailsPage = await outcomePage.viewFirstOutcomes(); diff --git a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts index 1e1bdcbfa..3d37d3733 100644 --- a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts @@ -42,18 +42,12 @@ test.describe("Logged in DReps", () => { timeout: 60_000, }); - await expect( - page.getByTestId("dRep-id-display-card-dashboard") - ).toContainText(dRep01Wallet.dRepId, { timeout: 60_000 }); - + const governanceActionsPage = new GovernanceActionsPage(page); - + await governanceActionsPage.goto(); - - await expect(page.getByText(/info action/i).first()).toBeVisible({ - timeout: 60_000, - }); - + + await governanceActionsPage.getFirstProposal(); const governanceActionDetailsPage = await governanceActionsPage.viewFirstProposalByGovernanceAction( GovernanceActionType.InfoAction diff --git a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts index 7e6623005..ff40cf000 100644 --- a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts @@ -67,7 +67,6 @@ test.describe("Logged in DRep", () => { ) : await govActionsPage.viewFirstProposal(); - await govActionDetailsPage.contextBtn.click(); await govActionDetailsPage.contextInput.fill(faker.lorem.sentence(200)); await govActionDetailsPage.confirmModalBtn.click(); await page.getByRole("checkbox").click(); @@ -101,40 +100,6 @@ test.describe("Logged in DRep", () => { }); }); -test.describe("Temporary DReps", async () => { - let dRepPage: Page; - - test.beforeEach(async ({ page, browser }) => { - const wallet = await walletManager.popWallet("registeredDRep"); - - const tempDRepAuth = await createTempDRepAuth(page, wallet); - - dRepPage = await createNewPageWithWallet(browser, { - storageState: tempDRepAuth, - wallet, - enableDRepSigning: true, - }); - }); - - test("4J. Should include metadata anchor in the vote transaction", async ({}, testInfo) => { - test.setTimeout(testInfo.timeout + environments.txTimeOut); - - const govActionsPage = new GovernanceActionsPage(dRepPage); - await govActionsPage.goto(); - - const govActionDetailsPage = await govActionsPage.viewFirstProposal(); - await govActionDetailsPage.vote(faker.lorem.sentence(200)); - - await dRepPage.waitForTimeout(5_000); - - await govActionsPage.votedTab.click(); - await govActionsPage.viewFirstVotedProposal(); - - // Vote context is not displayed in UI to validate - expect(false, "No vote context displayed").toBe(true); - }); -}); - test.describe("Check vote count", () => { test.use({ storageState: dRep01AuthFile, wallet: dRep01Wallet }); diff --git a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts index adab72a92..740935a99 100644 --- a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts +++ b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts @@ -189,7 +189,7 @@ test("4M. Should show view-all categorized governance actions", async ({ const governanceActionPage = new GovernanceActionsPage(page); await governanceActionPage.goto(); - await page.getByRole("button", { name: "Show All" }).click(); + await page.getByRole("link", { name: "Show All" }).click(); const proposalCards = await governanceActionPage.getAllProposals(); diff --git a/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.dRep.spec.ts index eb805f417..4d8052543 100644 --- a/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.dRep.spec.ts @@ -72,7 +72,6 @@ test.describe("Proposal checks", () => { currentPage.getByTestId(`${cip129GovActionId}-id`) ).toBeVisible(); - await expect(govActionDetailsPage.contextBtn).toBeVisible(); await expect(govActionDetailsPage.showVotesBtn).toBeVisible(); await expect(govActionDetailsPage.voteBtn).toBeVisible(); @@ -87,12 +86,12 @@ test.describe("Proposal checks", () => { await expect(govActionDetailsPage.noVoteRadio).toBeVisible(); await expect(govActionDetailsPage.abstainRadio).toBeVisible(); - await govActionDetailsPage.contextBtn.click(); + await govActionDetailsPage.yesVoteRadio.click(); + await govActionDetailsPage.voteBtn.click(); await expect(govActionDetailsPage.contextInput).toBeVisible(); await govActionDetailsPage.cancelModalBtn.click(); - await govActionDetailsPage.yesVoteRadio.click(); await expect(govActionDetailsPage.voteBtn).toBeEnabled(); }); @@ -100,7 +99,9 @@ test.describe("Proposal checks", () => { const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; test("5D_1. Should accept valid data in provide context", async () => { - await govActionDetailsPage.contextBtn.click(); + + await govActionDetailsPage.yesVoteRadio.click() + await govActionDetailsPage.voteBtn.click() await expect(govActionDetailsPage.contextInput).toBeVisible(); @@ -119,7 +120,8 @@ test.describe("Proposal checks", () => { }); test("5D_2. Should reject invalid data in provide context", async () => { - await govActionDetailsPage.contextBtn.click(); + await govActionDetailsPage.yesVoteRadio.click() + await govActionDetailsPage.voteBtn.click() await expect(govActionDetailsPage.contextInput).toBeVisible(); @@ -136,6 +138,8 @@ test.describe("Proposal checks", () => { test.describe("Perform voting", () => { let govActionDetailsPage: GovernanceActionDetailsPage; + let dRepPage: Page; + let govActionsPage: GovernanceActionsPage; test.beforeEach(async ({ page, browser }) => { test.slow(); // Due to queue in pop wallets @@ -144,14 +148,14 @@ test.describe("Perform voting", () => { const tempDRepAuth = await createTempDRepAuth(page, wallet); - const dRepPage = await createNewPageWithWallet(browser, { + dRepPage = await createNewPageWithWallet(browser, { storageState: tempDRepAuth, wallet, enableDRepSigning: true, }); - const govActionsPage = new GovernanceActionsPage(dRepPage); - await govActionsPage.goto(); + govActionsPage = new GovernanceActionsPage(dRepPage); + govActionsPage.goto(); // assert to wait until the loading button is hidden await expect(dRepPage.getByTestId("to-vote-tab")).toBeVisible({ @@ -245,32 +249,23 @@ test.describe("Perform voting", () => { ).toBeVisible(); govActionDetailsPage = await governanceActionsPage.viewFirstVotedProposal(); - await govActionDetailsPage.vote(faker.lorem.sentence(200), true); + + const fakerContext = faker.lorem.sentence(200) + await govActionDetailsPage.vote(fakerContext, true); await govActionDetailsPage.currentPage.reload(); await governanceActionsPage.votedTab.click(); - const isYesVoteVisible = await govActionDetailsPage.currentPage - .getByTestId("my-vote") - .getByText("Yes") - .isVisible(); - - const textContent = await govActionDetailsPage.currentPage - .getByTestId("my-vote") - .textContent(); - - await govActionDetailsPage.currentPage.evaluate(() => - window.scrollTo(0, 500) - ); - await expect( - govActionDetailsPage.currentPage.getByTestId("my-vote").getByText("Yes"), - { - message: - !isYesVoteVisible && - `"Yes" vote not visible, current vote status: ${textContent.match(/My Vote:(Yes|No)/)[1]}`, - } - ).toBeVisible({ timeout: 60_000 }); + govActionDetailsPage = await governanceActionsPage.viewFirstVotedProposal(); + + await govActionDetailsPage.currentPage.getByTestId("yes-radio").isVisible(); + + await govActionDetailsPage.currentPage.getByTestId("show-more-button").click(); + await govActionDetailsPage.currentPage.waitForTimeout(2000); + + const voteRationaleContext = await govActionDetailsPage.currentPage.getByTestId("vote-rationale-context"); + await expect(voteRationaleContext).toContainText(fakerContext); }); test("5I. Should view the vote details,when viewing governance action already voted by the DRep", async ({}, testInfo) => { @@ -282,13 +277,43 @@ test.describe("Perform voting", () => { govActionDetailsPage.currentPage ); - await governanceActionsPage.currentPage.waitForTimeout(5_000); + await governanceActionsPage.getFirstProposal(); await governanceActionsPage.votedTab.click(); await expect( govActionDetailsPage.currentPage.getByTestId("my-vote").getByText("Yes") ).toBeVisible(); }); + + const verifyVoteWithMetadata = async (testInfo: any, useGovToolIPFS: boolean = false) => { + test.setTimeout(testInfo.timeout + environments.txTimeOut); + const fakerContext = faker.lorem.sentence(200); + + if (useGovToolIPFS) { + await govActionDetailsPage.vote(fakerContext, false, true); + } else { + await govActionDetailsPage.vote(fakerContext); + } + + await dRepPage.reload(); + await dRepPage.waitForTimeout(5_000); + await govActionsPage.votedTab.click(); + + const votedGovActionDetailsPage = await govActionsPage.viewFirstVotedProposal(); + await votedGovActionDetailsPage.currentPage.getByTestId("show-more-button").click(); + await votedGovActionDetailsPage.currentPage.waitForTimeout(2000); + + const voteRationaleContext = await votedGovActionDetailsPage.currentPage.getByTestId("vote-rationale-context"); + await expect(voteRationaleContext).toContainText(fakerContext); + }; + + test("5M_1. Should vote with Context (Download and store yourself)", async ({}, testInfo) => { + await verifyVoteWithMetadata(testInfo, false); + }); + + test("5M_2. Should vote with Context (GovTool pins data to IPFS)", async ({}, testInfo) => { + await verifyVoteWithMetadata(testInfo, true ); + }); }); test.describe("Check voting power", () => { diff --git a/tests/govtool-frontend/playwright/tests/dRep.setup.ts b/tests/govtool-frontend/playwright/tests/dRep.setup.ts index 584120038..d4c2e9936 100644 --- a/tests/govtool-frontend/playwright/tests/dRep.setup.ts +++ b/tests/govtool-frontend/playwright/tests/dRep.setup.ts @@ -14,7 +14,7 @@ import { functionWaitedAssert } from "@helpers/waitedLoop"; import { StaticWallet } from "@types"; const REGISTER_DREP_WALLETS_COUNT = 6; -const DREP_WALLETS_COUNT = 10; +const DREP_WALLETS_COUNT = 11; let dRepDeposit: number; @@ -73,6 +73,7 @@ setup("Register DRep of static wallets", async () => { }); setup("Setup temporary DRep wallets", async () => { + const totalRequiredBalanceForDRepSetup = (DREP_WALLETS_COUNT + REGISTER_DREP_WALLETS_COUNT) * (dRepDeposit / 1000000 + 22);