diff --git a/tests/govtool-frontend/playwright/lib/constants/index.ts b/tests/govtool-frontend/playwright/lib/constants/index.ts index b8ab0f1da..e27231cee 100644 --- a/tests/govtool-frontend/playwright/lib/constants/index.ts +++ b/tests/govtool-frontend/playwright/lib/constants/index.ts @@ -23,3 +23,11 @@ export const guardrailsScript = { export const guardrailsScriptHash = "914d97d63e2b7113465739faddd82362b1deaeedbcc4d01016c35c6e"; + +export const outcomeStatusType = [ + "Expired", + "Not Ratified", + "Ratified", + "Enacted", + "Live", +]; diff --git a/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts b/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts index 7f42d8817..547a40cc0 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts @@ -14,13 +14,13 @@ const monthNames = [ ]; export default function extractExpiryDateFromText(text: string): Date | null { - const regex = /(\d{1,2})(st|nd|rd|th) ([\w]{3}) (\d{4})/; + const regex = /(\d{1,2})(?:st|nd|rd|th)? (\w{3}) (\d{4})/; const match = text.match(regex); if (match) { const day = parseInt(match[1]); - const month = match[3]; - const year = parseInt(match[4]); + const month = match[2]; + const year = parseInt(match[3]); return new Date(year, monthNames.indexOf(month), day); } else { diff --git a/tests/govtool-frontend/playwright/lib/helpers/featureFlag.ts b/tests/govtool-frontend/playwright/lib/helpers/featureFlag.ts index 936b36bc0..0b20d09d4 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/featureFlag.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/featureFlag.ts @@ -1,51 +1,112 @@ -import { GovernanceActionType, IProposal } from "@types"; +import { + GovernanceActionType, + IProposal, + outcomeProposal, + outcomeType, +} from "@types"; import { isBootStrapingPhase } from "./cardano"; import { SECURITY_RELEVANT_PARAMS_MAP } from "@constants/index"; -export const areDRepVoteTotalsDisplayed = async (proposal: IProposal) => { +const getProposalType = ( + type: keyof typeof outcomeType, + fallback: GovernanceActionType, + proposal: IProposal | outcomeProposal +) => + "proposal_params" in proposal + ? Object.keys(outcomeType).find( + (key) => outcomeType[key] === outcomeType[type] + ) + : fallback; + +export const areDRepVoteTotalsDisplayed = async ( + proposal: IProposal | outcomeProposal +) => { const isInBootstrapPhase = await isBootStrapingPhase(); const isSecurityGroup = Object.values(SECURITY_RELEVANT_PARAMS_MAP).some( - (paramKey) => - proposal.protocolParams?.[ - paramKey as keyof typeof proposal.protocolParams - ] !== null + (paramKey) => { + const params = + "protocolParams" in proposal + ? proposal.protocolParams + : proposal.proposal_params; + return params?.[paramKey as keyof typeof params] !== null; + } ); + if (isInBootstrapPhase) { + const HardForkInitiation = getProposalType( + "HardForkInitiation", + GovernanceActionType.HardFork, + proposal + ); + + const ProtocolParameterChange = getProposalType( + "ParameterChange", + GovernanceActionType.ProtocolParameterChange, + proposal + ); + return !( - proposal.type === GovernanceActionType.HardFork || - (proposal.type === GovernanceActionType.ProtocolParameterChange && - !isSecurityGroup) + proposal.type === HardForkInitiation || + (proposal.type === ProtocolParameterChange && !isSecurityGroup) ); } return true; }; -export const areSPOVoteTotalsDisplayed = async (proposal: IProposal) => { +export const areSPOVoteTotalsDisplayed = async ( + proposal: IProposal | outcomeProposal +) => { const isInBootstrapPhase = await isBootStrapingPhase(); const isSecurityGroup = Object.values(SECURITY_RELEVANT_PARAMS_MAP).some( - (paramKey) => - proposal.protocolParams?.[ - paramKey as keyof typeof proposal.protocolParams - ] !== null + (paramKey) => { + const params = + "protocolParams" in proposal + ? proposal.protocolParams + : proposal.proposal_params; + return params?.[paramKey as keyof typeof params] !== null; + } ); + + const ProtocolParameterChange = getProposalType( + "ParameterChange", + GovernanceActionType.ProtocolParameterChange, + proposal + ); + const UpdatetotheConstitution = getProposalType( + "NewConstitution", + GovernanceActionType.UpdatetotheConstitution, + proposal + ); + const TreasuryWithdrawal = getProposalType( + "TreasuryWithdrawals", + GovernanceActionType.TreasuryWithdrawal, + proposal + ); + if (isInBootstrapPhase) { - return proposal.type !== GovernanceActionType.ProtocolParameterChange; + return proposal.type !== ProtocolParameterChange; } return !( - proposal.type === GovernanceActionType.UpdatetotheConstitution || - proposal.type === GovernanceActionType.TreasuryWithdrawal || - (proposal.type === GovernanceActionType.ProtocolParameterChange && - !isSecurityGroup) + proposal.type === UpdatetotheConstitution || + proposal.type === TreasuryWithdrawal || + (proposal.type === ProtocolParameterChange && !isSecurityGroup) ); }; export const areCCVoteTotalsDisplayed = ( - governanceActionType: GovernanceActionType + proposal: IProposal | outcomeProposal ) => { - return ![ + const NoConfidence = getProposalType( + "NoConfidence", GovernanceActionType.NoConfidence, + proposal + ); + const NewCommittee = getProposalType( + "NewCommittee", GovernanceActionType.NewCommittee, - ].includes(governanceActionType); + proposal + ); + return ![NewCommittee, NoConfidence].includes(proposal.type); }; diff --git a/tests/govtool-frontend/playwright/lib/helpers/string.ts b/tests/govtool-frontend/playwright/lib/helpers/string.ts index 26ec9453c..745928196 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/string.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/string.ts @@ -4,14 +4,24 @@ export function extractProposalIdFromUrl(url: string) { return parseInt(url.split("/").pop()); } -export function generateExactLengthText(characterLength:number) { - let text = ''; - +export function generateExactLengthText(characterLength: number) { + let text = ""; + // Keep generating paragraphs until we exceed the required length while (text.length < characterLength) { - text += faker.lorem.paragraphs(10); + text += faker.lorem.paragraphs(10); } - + // Truncate to the exact number of characters needed return text.substring(0, characterLength); -} \ No newline at end of file +} + +export function toCamelCase(str: string) { + return str + .toLowerCase() + .split(" ") + .map((word, index) => + index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1) + ) + .join(""); +} diff --git a/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts b/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts index 2e093f769..66ffd34f8 100644 --- a/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts @@ -99,7 +99,7 @@ export default class GovernanceActionsPage { await waitedLoop(async () => { return ( (await this.page.locator('[data-testid$="-card"]').count()) > 0 || - this.page.getByText("No results for the search.") + (await this.page.getByText("No results for the search.").isVisible()) ); }); return this.page.locator('[data-testid$="-card"]').all(); diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts new file mode 100644 index 000000000..8a69e5bf3 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts @@ -0,0 +1,62 @@ +import environments from "@constants/environments"; +import { Page, Response } from "@playwright/test"; +import { outcomeProposal } from "@types"; + +export default class OutcomeDetailsPage { + readonly dRepYesVotes = this.page.getByTestId("submitted-votes-dReps-yes"); + readonly dRepNoVotes = this.page.getByTestId("submitted-votes-dReps-no"); + readonly dRepNotVoted = this.page.getByTestId( + "submitted-votes-dReps-notVoted" + ); + readonly dRepAbstainVotes = this.page.getByTestId( + "submitted-votes-dReps-abstain" + ); + + readonly sPosYesVotes = this.page.getByTestId("submitted-votes-sPos-yes"); + readonly sPosNoVotes = this.page.getByTestId("submitted-votes-sPos-no"); + readonly sPosAbstainVotes = this.page.getByTestId( + "submitted-votes-sPos-abstain" + ); + + readonly ccCommitteeYesVotes = this.page.getByTestId( + "submitted-votes-ccCommittee-yes" + ); + readonly ccCommitteeNoVotes = this.page.getByTestId( + "submitted-votes-ccCommittee-no" + ); + readonly ccCommitteeAbstainVotes = this.page.getByTestId( + "submitted-votes-ccCommittee-abstain" + ); + + constructor(private readonly page: Page) {} + + get currentPage(): Page { + return this.page; + } + + async goto(proposalId: string) { + await this.page.goto( + `${environments.frontendUrl}/outcomes/governance_actions/${proposalId}` + ); + } + + async getDRepTotalAbstainVoted( + proposal: outcomeProposal, + metricsResponses: Response + ): Promise { + const alwaysAbstainVotingPower = await metricsResponses + .json() + .then((res) => res.alwaysAbstainVotingPower); + if ( + alwaysAbstainVotingPower && + typeof alwaysAbstainVotingPower === "number" + ) { + const totalAbstainVoted = + alwaysAbstainVotingPower + parseInt(proposal.abstain_votes); + + return totalAbstainVoted; + } else { + return parseInt(proposal.abstain_votes); + } + } +} diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts new file mode 100644 index 000000000..3fad8b410 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -0,0 +1,229 @@ +import environments from "@constants/environments"; +import { outcomeStatusType } from "@constants/index"; +import { toCamelCase } from "@helpers/string"; +import { functionWaitedAssert, waitedLoop } from "@helpers/waitedLoop"; +import { expect, Locator, Page } from "@playwright/test"; +import { outcomeProposal, outcomeType } from "@types"; +import OutcomeDetailsPage from "./outcomeDetailsPage"; + +export default class OutComesPage { + // Buttons + readonly filterBtn = this.page.getByTestId("filters-button"); + readonly sortBtn = this.page.getByTestId("sort-button"); + readonly showMoreBtn = this.page.getByTestId("show-more-button"); + + //inputs + readonly searchInput = this.page.getByTestId("search-input"); + + constructor(private readonly page: Page) {} + + async goto(params: { filter?: string; sort?: string } = {}): Promise { + const { filter, sort = "newestFirst" } = params; + const url = new URL(`${environments.frontendUrl}/outcomes`); + url.searchParams.append("sort", sort); + if (filter) { + url.searchParams.append("type", filter); + } + await this.page.goto(url.toString()); + } + + async getAllListedCIP105GovernanceIds(): Promise { + const dRepCards = await this.getAllOutcomes(); + const dRepIds = []; + + for (const dRep of dRepCards) { + const dRepIdTextContent = await dRep + .locator('[data-testid$="-CIP-105-id"]') + .textContent(); + dRepIds.push(dRepIdTextContent.replace(/^.*ID/, "")); + } + + return dRepIds; + } + + async viewFirstOutcomes(): Promise { + await this.page.locator('[data-testid$="-view-details"]').first().click(); + return new OutcomeDetailsPage(this.page); + } + + async getAllOutcomes(): Promise { + await waitedLoop(async () => { + return ( + (await this.page.locator('[data-testid$="-outcome-card"]').count()) > + 0 || + (await this.page.getByText("No governance actions found").isVisible()) + ); + }); + return await this.page.locator('[data-testid$="-outcome-card"]').all(); + } + + async clickCheckboxByNames(names: string[]) { + const formattedNames = names.map((name) => + name === "Info Action" ? "Info" : name + ); + for (const name of formattedNames) { + const testId = name.toLowerCase().replace(/ /g, "-"); + await this.page.getByTestId(`${testId}-checkbox`).click(); + } + } + + async filterProposalByNames(names: string[]) { + await this.clickCheckboxByNames(names); + } + + async unFilterProposalByNames(names: string[]) { + await this.clickCheckboxByNames(names); + } + + async applyAndValidateFilters( + filters: string[], + validateFunction: (proposalCard: any, filters: string[]) => Promise + ) { + await this.page.waitForTimeout(4_000); // wait for the proposals to load + // single filter + for (const filter of filters) { + await this.filterProposalByNames([filter]); + await this.validateFilters([filter], validateFunction); + await this.unFilterProposalByNames([filter]); + } + + // multiple filter + const multipleFilters = [...filters]; + while (multipleFilters.length > 1) { + await this.filterProposalByNames(multipleFilters); + await this.validateFilters(multipleFilters, validateFunction); + await this.unFilterProposalByNames(multipleFilters); + multipleFilters.pop(); + } + } + + async validateFilters( + filters: string[], + validateFunction: (proposalCard: any, filters: string[]) => Promise + ) { + await functionWaitedAssert( + async () => { + const proposalCards = await this.getAllOutcomes(); + for (const proposalCard of proposalCards) { + const type = await proposalCard + .locator('[data-testid$="-type"]') + .textContent(); + const outcomeType = type.replace(/^.*Type/, ""); + const hasFilter = await validateFunction(proposalCard, filters); + if (!hasFilter) { + const errorMessage = `A outcomne type ${outcomeType} does not contain on ${filters}`; + throw errorMessage; + } + expect(hasFilter).toBe(true); + } + }, + { + name: "validateFilters", + } + ); + } + + getSortType(sortOption: string) { + let sortType = sortOption; + if (sortOption === "Highest amount of yes votes") { + sortType = "Highest yes votes"; + } + return toCamelCase(sortType); + } + + getSortTestId(sortOption: string) { + const sortType = this.getSortType(sortOption); + return sortType.toLowerCase().replace(/[\s.]/g, "") + "-radio"; + } + + async sortAndValidate( + sortOption: string, + validationFn: (p1: outcomeProposal, p2: outcomeProposal) => boolean, + filterKey?: string + ) { + const sortType = this.getSortType(sortOption); + const responsePromise = this.page.waitForResponse((response) => + response + .url() + .includes( + filterKey + ? `&filters=${filterKey}&sort=${sortType}` + : `&sort=${sortType}` + ) + ); + + await this.page.getByTestId(this.getSortTestId(sortOption)).click(); + + const response = await responsePromise; + const data = await response.json(); + let outcomeProposalList: outcomeProposal[] = data.length != 0 ? data : null; + + // API validation + if (outcomeProposalList.length <= 1) return; + + for (let i = 0; i <= outcomeProposalList.length - 2; i++) { + const isValid = validationFn( + outcomeProposalList[i], + outcomeProposalList[i + 1] + ); + expect(isValid).toBe(true); + } + + await expect( + this.page.getByRole("progressbar").getByRole("img") + ).toBeHidden({ timeout: 20_000 }); + + await functionWaitedAssert( + async () => { + const outcomeCards = await this.getAllOutcomes(); + for (const [index, outcomeCard] of outcomeCards.entries()) { + const outcomeProposalFromAPI = outcomeProposalList[index]; + const proposalTypeFromUI = await outcomeCard + .locator('[data-testid$="-type"]') + .textContent(); + const proposalTypeFromApi = outcomeType[outcomeProposalFromAPI.type]; + + const cip105IdFromUI = await outcomeCard + .locator('[data-testid$="-CIP-105-id"]') + .textContent(); + const cip105IdFromApi = `${outcomeProposalFromAPI.tx_hash}#${outcomeProposalFromAPI.index}`; + + expect(proposalTypeFromUI.replace(/^.*Type/, "")).toContain( + proposalTypeFromApi + ); + + expect(cip105IdFromUI.replace(/^.*ID/, "")).toContain( + cip105IdFromApi + ); + } + }, + { + name: `frontend sort validation of ${sortOption} and filter ${filterKey}`, + } + ); + } + + async _validateFiltersInOutcomeCard( + proposalCard: Locator, + filters: string[] + ): Promise { + const type = await proposalCard + .locator('[data-testid$="-type"]') + .textContent(); + const outcomeType = type.replace(/^.*Type/, ""); + return filters.includes(outcomeType); + } + + async _validateStatusFiltersInOutcomeCard( + proposalCard: Locator, + filters: string[] + ): Promise { + const status = await proposalCard + .locator('[data-testid$="-status"]') + .textContent(); + const outcomeStatus = outcomeStatusType.filter((statusType) => { + return status.includes(statusType); + }); + return outcomeStatus.some((status) => filters.includes(status)); + } +} diff --git a/tests/govtool-frontend/playwright/lib/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index b4c439ced..ef277f297 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -92,6 +92,16 @@ export enum GovernanceActionType { UpdatetotheConstitution = "NewConstitution", } +export enum outcomeType { + NewConstitution = "New Constitution", + NewCommittee = "Update Committee", + HardForkInitiation = "Hard-Fork Initiation", + NoConfidence = "Motion of no Confidence", + InfoAction = "Info Action", + TreasuryWithdrawals = "Treasury Withdrawals", + ParameterChange = "Protocol Parameter Change", +} + export enum FullGovernanceDRepVoteActionsType { ProtocolParameterChange = "ParameterChange", InfoAction = "InfoAction", @@ -251,3 +261,44 @@ export interface imageObject { contentUrl: string; sha256: string; } + +export interface outcomeProposal { + id: string; + tx_hash: string; + index: string; + type: string; + yes_votes: string; + no_votes: string; + abstain_votes: string; + description: any; + expiry_date: string; + expiration: number; + time: string; + epoch_no: number; + url: string; + data_hash: string; + title: string | null; + abstract: string | null; + motivation?: string | null; + rationale?: string | null; + pool_yes_votes?: string; + pool_no_votes?: string; + pool_abstain_votes?: string; + cc_yes_votes?: string; + cc_no_votes?: string; + cc_abstain_votes?: string; + proposal_params: EpochParams | null; +} + +export interface outcomeMetadata { + authors: any[]; + hashAlgorithm: string; + body: outcomeMetadataBody; +} + +interface outcomeMetadataBody { + abstract: string; + motivation: "string"; + rationale: string; + title: string; +} 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 c10950fe0..409ff036d 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 @@ -49,7 +49,7 @@ test.describe("Logged in DRep", () => { ); }); - test.describe("vote context metadata anchor validation", () => { + test.describe("Vote context metadata anchor validation", () => { let govActionDetailsPage: GovernanceActionDetailsPage; test.beforeEach(async ({ page }) => { const govActionsPage = new GovernanceActionsPage(page); @@ -219,9 +219,7 @@ test.describe("Check vote count", () => { } // check ccCommittee votes - if ( - areCCVoteTotalsDisplayed(proposalToCheck.type as GovernanceActionType) - ) { + if (areCCVoteTotalsDisplayed(proposalToCheck)) { await expect(govActionDetailsPage.ccCommitteeYesVotes).toHaveText( `${proposalToCheck.ccYesVotes}` ); diff --git a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.loggedin.spec.ts index c209df88f..a907f7f1e 100644 --- a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.loggedin.spec.ts +++ b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.loggedin.spec.ts @@ -2,31 +2,10 @@ import { user01Wallet } from "@constants/staticWallets"; import { test } from "@fixtures/walletExtension"; import { setAllureEpic } from "@helpers/allure"; import { skipIfNotHardFork } from "@helpers/cardano"; -import extractExpiryDateFromText from "@helpers/extractExpiryDateFromText"; import { isMobile, openDrawer } from "@helpers/mobile"; -import removeAllSpaces from "@helpers/removeAllSpaces"; -import { functionWaitedAssert } from "@helpers/waitedLoop"; import GovernanceActionsPage from "@pages/governanceActionsPage"; import { expect } from "@playwright/test"; -const infoTypeProposal = require("../../lib/_mock/infoTypeProposal.json"); - -const filterOptionNames = [ - "Protocol Parameter Change", - "New Committee", - "Hard Fork", - "No Confidence", - "Info Action", - "Treasury Withdrawal", - "Update to the Constitution", -]; - -enum SortOption { - SoonToExpire = "SoonestToExpire", - NewestFirst = "NewestCreated", - HighestYesVotes = "MostYesVotes", -} - test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); test.beforeEach(async () => { @@ -55,156 +34,3 @@ test("4B_1. Should restrict voting for users who are not registered as DReps (wi const govActionDetailsPage = await govActionsPage.viewFirstProposal(); await expect(govActionDetailsPage.voteBtn).not.toBeVisible(); }); - -test("4C_1. Should filter Governance Action Type on governance actions page", async ({ - page, -}) => { - test.slow(); - - const govActionsPage = new GovernanceActionsPage(page); - await govActionsPage.goto(); - - await govActionsPage.filterBtn.click(); - - // Single filter - for (const option of filterOptionNames) { - await govActionsPage.filterProposalByNames([option]); - await govActionsPage.validateFilters([option]); - await govActionsPage.unFilterProposalByNames([option]); - } - - // Multiple filters - const multipleFilterOptionNames = [...filterOptionNames]; - while (multipleFilterOptionNames.length > 1) { - await govActionsPage.filterProposalByNames(multipleFilterOptionNames); - await govActionsPage.validateFilters(multipleFilterOptionNames); - await govActionsPage.unFilterProposalByNames(multipleFilterOptionNames); - multipleFilterOptionNames.pop(); - } -}); - -test("4C_2. Should sort Governance Action Type on governance actions page", async ({ - page, -}) => { - test.slow(); - - const govActionsPage = new GovernanceActionsPage(page); - await govActionsPage.goto(); - - await govActionsPage.sortBtn.click(); - - await govActionsPage.sortAndValidate( - SortOption.SoonToExpire, - (p1, p2) => p1.expiryDate <= p2.expiryDate - ); - - await govActionsPage.sortAndValidate( - SortOption.NewestFirst, - (p1, p2) => p1.createdDate >= p2.createdDate - ); - - await govActionsPage.sortAndValidate( - SortOption.HighestYesVotes, - (p1, p2) => p1.dRepYesVotes >= p2.dRepYesVotes - ); -}); - -test("4C_3. Should filter and sort Governance Action Type on governance actions page", async ({ - page, -}) => { - test.slow(); - - const govActionsPage = new GovernanceActionsPage(page); - await govActionsPage.goto(); - - await govActionsPage.filterBtn.click(); - - const choice = Math.floor(Math.random() * filterOptionNames.length); - await govActionsPage.filterProposalByNames([filterOptionNames[choice]]); - - await govActionsPage.sortBtn.click(); - await govActionsPage.sortAndValidate( - SortOption.SoonToExpire, - (p1, p2) => p1.expiryDate <= p2.expiryDate, - [removeAllSpaces(filterOptionNames[choice])] - ); - await govActionsPage.validateFilters([filterOptionNames[choice]]); -}); - -test("4L. Should search governance actions", async ({ page }) => { - let governanceActionId: string; - await page.route("**/proposal/list?**", async (route) => { - const response = await route.fetch(); - const data = await response.json(); - const elementsWithIds = data["elements"].map( - (element) => element["txHash"] + "#" + element["index"] - ); - if (elementsWithIds.length !== 0) { - governanceActionId = elementsWithIds[0]; - } - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(data), - }); - }); - const responsePromise = page.waitForResponse("**/proposal/list?**"); - - const governanceActionPage = new GovernanceActionsPage(page); - await governanceActionPage.goto(); - - await responsePromise; - - await governanceActionPage.searchInput.fill(governanceActionId); - - await functionWaitedAssert( - async () => { - const proposalCards = await governanceActionPage.getAllProposals(); - - for (const proposalCard of proposalCards) { - await expect( - proposalCard.getByTestId(`${governanceActionId}-id`) - ).toBeVisible(); - } - }, - { message: `${governanceActionId} not found` } - ); -}); - -test("4M. Should show view-all categorized governance actions", async ({ - page, -}) => { - await page.route("**/proposal/list?**", async (route) => - route.fulfill({ - body: JSON.stringify(infoTypeProposal), - }) - ); - - const governanceActionPage = new GovernanceActionsPage(page); - await governanceActionPage.goto(); - - await page.getByRole("button", { name: "Show All" }).click(); - - const proposalCards = await governanceActionPage.getAllProposals(); - - for (const proposalCard of proposalCards) { - await expect(proposalCard.getByTestId("InfoAction-type")).toBeVisible(); - } -}); - -test("4H. Should verify none of the displayed governance actions have expired", async ({ - page, -}) => { - const govActionsPage = new GovernanceActionsPage(page); - await govActionsPage.goto(); - - const proposalCards = await govActionsPage.getAllProposals(); - - for (const proposalCard of proposalCards) { - const expiryDateEl = proposalCard.getByTestId("expiry-date"); - const expiryDateTxt = await expiryDateEl.innerText(); - const expiryDate = extractExpiryDateFromText(expiryDateTxt); - const today = new Date(); - expect(today <= expiryDate).toBeTruthy(); - } -}); 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 5c953622b..ab36868fa 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 @@ -12,12 +12,33 @@ import { expect } from "@playwright/test"; import { test } from "@fixtures/walletExtension"; import { GovernanceActionType, IProposal } from "@types"; import { injectLogger } from "@helpers/page"; +import removeAllSpaces from "@helpers/removeAllSpaces"; +import { functionWaitedAssert } from "@helpers/waitedLoop"; +import extractExpiryDateFromText from "@helpers/extractExpiryDateFromText"; test.beforeEach(async () => { await setAllureEpic("4. Proposal visibility"); await skipIfNotHardFork(); }); +const infoTypeProposal = require("../../lib/_mock/infoTypeProposal.json"); + +const filterOptionNames = [ + "Protocol Parameter Change", + "New Committee", + "Hard Fork", + "No Confidence", + "Info Action", + "Treasury Withdrawal", + "Update to the Constitution", +]; + +enum SortOption { + SoonToExpire = "SoonestToExpire", + NewestFirst = "NewestCreated", + HighestYesVotes = "MostYesVotes", +} + test("4A_2. Should access Governance Actions page without connecting wallet", async ({ page, }) => { @@ -37,6 +58,159 @@ test("4B_2. Should restrict voting for users who are not registered as DReps (wi await expect(govActionDetailsPage.voteBtn).not.toBeVisible(); }); +test("4C_1. Should filter Governance Action Type on governance actions page", async ({ + page, +}) => { + test.slow(); + + const govActionsPage = new GovernanceActionsPage(page); + await govActionsPage.goto(); + + await govActionsPage.filterBtn.click(); + + // Single filter + for (const option of filterOptionNames) { + await govActionsPage.filterProposalByNames([option]); + await govActionsPage.validateFilters([option]); + await govActionsPage.unFilterProposalByNames([option]); + } + + // Multiple filters + const multipleFilterOptionNames = [...filterOptionNames]; + while (multipleFilterOptionNames.length > 1) { + await govActionsPage.filterProposalByNames(multipleFilterOptionNames); + await govActionsPage.validateFilters(multipleFilterOptionNames); + await govActionsPage.unFilterProposalByNames(multipleFilterOptionNames); + multipleFilterOptionNames.pop(); + } +}); + +test("4C_2. Should sort Governance Action Type on governance actions page", async ({ + page, +}) => { + test.slow(); + + const govActionsPage = new GovernanceActionsPage(page); + await govActionsPage.goto(); + + await govActionsPage.sortBtn.click(); + + await govActionsPage.sortAndValidate( + SortOption.SoonToExpire, + (p1, p2) => p1.expiryDate <= p2.expiryDate + ); + + await govActionsPage.sortAndValidate( + SortOption.NewestFirst, + (p1, p2) => p1.createdDate >= p2.createdDate + ); + + await govActionsPage.sortAndValidate( + SortOption.HighestYesVotes, + (p1, p2) => p1.dRepYesVotes >= p2.dRepYesVotes + ); +}); + +test("4C_3. Should filter and sort Governance Action Type on governance actions page", async ({ + page, +}) => { + test.slow(); + + const govActionsPage = new GovernanceActionsPage(page); + await govActionsPage.goto(); + + await govActionsPage.filterBtn.click(); + + const choice = Math.floor(Math.random() * filterOptionNames.length); + await govActionsPage.filterProposalByNames([filterOptionNames[choice]]); + + await govActionsPage.sortBtn.click(); + await govActionsPage.sortAndValidate( + SortOption.SoonToExpire, + (p1, p2) => p1.expiryDate <= p2.expiryDate, + [removeAllSpaces(filterOptionNames[choice])] + ); + await govActionsPage.validateFilters([filterOptionNames[choice]]); +}); + +test("4L. Should search governance actions", async ({ page }) => { + let governanceActionId: string | undefined; + await page.route("**/proposal/list?**", async (route) => { + const response = await route.fetch(); + const data = await response.json(); + const elementsWithIds = data["elements"].map( + (element) => element["txHash"] + "#" + element["index"] + ); + if (elementsWithIds.length !== 0 && governanceActionId === undefined) { + governanceActionId = elementsWithIds[0]; + } + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(data), + }); + }); + const responsePromise = page.waitForResponse("**/proposal/list?**"); + + const governanceActionPage = new GovernanceActionsPage(page); + await governanceActionPage.goto(); + + await responsePromise; + + await governanceActionPage.searchInput.fill(governanceActionId); + + await functionWaitedAssert( + async () => { + const proposalCards = await governanceActionPage.getAllProposals(); + + for (const proposalCard of proposalCards) { + await expect( + proposalCard.getByTestId(`${governanceActionId}-id`) + ).toBeVisible(); + } + }, + { message: `${governanceActionId} not found` } + ); +}); + +test("4M. Should show view-all categorized governance actions", async ({ + page, +}) => { + await page.route("**/proposal/list?**", async (route) => + route.fulfill({ + body: JSON.stringify(infoTypeProposal), + }) + ); + + const governanceActionPage = new GovernanceActionsPage(page); + await governanceActionPage.goto(); + + await page.getByRole("button", { name: "Show All" }).click(); + + const proposalCards = await governanceActionPage.getAllProposals(); + + for (const proposalCard of proposalCards) { + await expect(proposalCard.getByTestId("InfoAction-type")).toBeVisible(); + } +}); + +test("4H. Should verify none of the displayed governance actions have expired", async ({ + page, +}) => { + const govActionsPage = new GovernanceActionsPage(page); + await govActionsPage.goto(); + + const proposalCards = await govActionsPage.getAllProposals(); + + for (const proposalCard of proposalCards) { + const expiryDateEl = proposalCard.getByTestId("expiry-date"); + const expiryDateTxt = await expiryDateEl.innerText(); + const expiryDate = extractExpiryDateFromText(expiryDateTxt); + const today = new Date(); + expect(today <= expiryDate).toBeTruthy(); + } +}); + test("4K. Should display correct vote counts on governance details page for disconnect state", async ({ page, browser, @@ -110,9 +284,7 @@ test("4K. Should display correct vote counts on governance details page for disc } // check ccCommittee votes - if ( - areCCVoteTotalsDisplayed(proposalToCheck.type as GovernanceActionType) - ) { + if (areCCVoteTotalsDisplayed(proposalToCheck)) { await expect(govActionDetailsPage.ccCommitteeYesVotes).toHaveText( `${proposalToCheck.ccYesVotes}` ); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.spec.ts index 9143f0f95..a018136ef 100644 --- a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.spec.ts +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.spec.ts @@ -302,7 +302,7 @@ test.describe("Proposal created logged state", () => { }); }); - test.describe("proposed as a governance action", () => { + test.describe("Proposed as a governance action", () => { let proposalSubmissionPage: ProposalSubmissionPage; test.beforeEach(async ({ page, proposalId }) => { const proposalDiscussionDetailsPage = new ProposalDiscussionDetailsPage( diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts new file mode 100644 index 000000000..6a8108728 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -0,0 +1,402 @@ +import { test } from "@fixtures/walletExtension"; +import { correctVoteAdaFormat } from "@helpers/adaFormat"; +import { setAllureEpic } from "@helpers/allure"; +import { skipIfNotHardFork } from "@helpers/cardano"; +import extractExpiryDateFromText from "@helpers/extractExpiryDateFromText"; +import { + areCCVoteTotalsDisplayed, + areDRepVoteTotalsDisplayed, + areSPOVoteTotalsDisplayed, +} from "@helpers/featureFlag"; +import { isMobile } from "@helpers/mobile"; +import { injectLogger } from "@helpers/page"; +import { functionWaitedAssert } from "@helpers/waitedLoop"; +import OutComesPage from "@pages/outcomesPage"; +import { expect, Page } from "@playwright/test"; +import { outcomeMetadata, outcomeProposal, outcomeType } from "@types"; + +test.beforeEach(async () => { + await setAllureEpic("9. Outcomes"); + await skipIfNotHardFork(); +}); + +const status = ["Expired", "Ratified", "Enacted", "Live"]; + +enum SortOption { + SoonToExpire = "Soon to expire", + NewestFirst = "Newest first", + OldestFirst = "Oldest first", + HighestAmountYesVote = "Highest amount of yes votes", +} +test("9A. Should access Outcomes page in disconnect state", async ({ + page, +}) => { + await page.goto("/"); + + if (isMobile(page)) { + await page.getByTestId("open-drawer-button").click(); + } + await page.getByTestId("governance-actions-outcomes-link").click(); + + await expect(page.getByText(/outcomes/i)).toHaveCount(2); +}); + +test.describe("Outcome details dependent test", () => { + let governanceActionId: string | undefined; + let governanceActionTitle: string | undefined; + let currentPage: Page; + test.beforeEach(async ({ page }) => { + // intercept outcomes data for id + await page.route( + "**/governance-actions?search=&filters=&sort=**", + async (route) => { + const response = await route.fetch(); + const data: outcomeProposal[] = await response.json(); + if (!governanceActionId) { + if (data.length > 0) { + const randomIndexForId = Math.floor(Math.random() * data.length); + governanceActionId = + data[randomIndexForId].tx_hash + + "#" + + data[randomIndexForId].index; + } + } + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(data), + }); + } + ); + + // intercept metadata for title + await page.route("**/governance-actions/metadata?**", async (route) => { + try { + const response = await route.fetch(); + if (response.status() !== 200) { + await route.continue(); + return; + } + const data: outcomeMetadata = await response.json(); + if (!governanceActionTitle && data.body.title != null) { + governanceActionTitle = data.body.title; + } + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(data), + }); + } catch (error) { + return; + } + }); + + const responsePromise = page.waitForResponse( + "**/governance-actions?search=&filters=&sort=**" + ); + const metadataResponsePromise = page.waitForResponse( + "**/governance-actions/metadata?**" + ); + + const outcomesPage = new OutComesPage(page); + await outcomesPage.goto(); + + await responsePromise; + await metadataResponsePromise; + currentPage = page; + }); + + test("9B. Should search outcomes proposal by title and id", async ({}) => { + const outcomesPage = new OutComesPage(currentPage); + // search by id + await outcomesPage.searchInput.fill(governanceActionId); + await expect( + currentPage.getByRole("progressbar").getByRole("img") + ).toBeVisible(); + + await functionWaitedAssert( + async () => { + const idSearchOutcomeCards = await outcomesPage.getAllOutcomes(); + expect(idSearchOutcomeCards.length, { + message: + idSearchOutcomeCards.length == 0 && "No governance actions found", + }).toBeGreaterThan(0); + for (const outcomeCard of idSearchOutcomeCards) { + const id = await outcomeCard + .locator('[data-testid$="-CIP-105-id"]') + .textContent(); + expect(id.replace(/^.*ID/, "")).toContain(governanceActionId); + } + }, + { name: "search by id" } + ); + + // search by title + await outcomesPage.searchInput.fill(governanceActionTitle); + await expect( + currentPage.getByRole("progressbar").getByRole("img") + ).toBeVisible(); + + await functionWaitedAssert( + async () => { + const titleSearchOutcomeCards = await outcomesPage.getAllOutcomes(); + expect(titleSearchOutcomeCards.length, { + message: + titleSearchOutcomeCards.length == 0 && + "No governance actions found", + }).toBeGreaterThan(0); + for (const outcomeCard of titleSearchOutcomeCards) { + const title = await outcomeCard + .locator('[data-testid$="-card-title"]') + .textContent(); + expect(title).toContain(governanceActionTitle); + } + }, + { name: "search by title" } + ); + }); + + test("9D. Should copy governanceActionId", async ({ page, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + const outcomesPage = new OutComesPage(currentPage); + + await outcomesPage.searchInput.fill(governanceActionId); + await expect( + currentPage.getByRole("progressbar").getByRole("img") + ).toBeVisible(); + await page + .getByTestId(`${governanceActionId}-CIP-105-id`) + .getByTestId("copy-button") + .click(); + await expect(page.getByText("Copied to clipboard")).toBeVisible({ + timeout: 10_000, + }); + const copiedTextDRepDirectory = await page.evaluate(() => + navigator.clipboard.readText() + ); + expect(copiedTextDRepDirectory).toEqual(governanceActionId); + }); +}); + +test("9C_1. Should filter Governance Action Type on governance actions page", async ({ + page, +}) => { + test.slow(); + + const outcomePage = new OutComesPage(page); + await outcomePage.goto(); + + await outcomePage.filterBtn.click(); + const filterOptionNames = Object.values(outcomeType); + + // proposal type filter + await outcomePage.applyAndValidateFilters( + filterOptionNames, + outcomePage._validateFiltersInOutcomeCard + ); + + // proposal status filter + await outcomePage.applyAndValidateFilters( + status, + outcomePage._validateStatusFiltersInOutcomeCard + ); +}); + +test("9C_2. Should sort Governance Action Type on outcomes page", async ({ + page, +}) => { + test.slow(); + + const outcomePage = new OutComesPage(page); + await outcomePage.goto(); + + await outcomePage.sortBtn.click(); + + await outcomePage.sortAndValidate( + SortOption.OldestFirst, + (p1, p2) => p1.expiry_date <= p2.expiry_date + ); + + await outcomePage.sortAndValidate( + SortOption.NewestFirst, + (p1, p2) => p1.expiry_date >= p2.time + ); + + await outcomePage.sortAndValidate( + SortOption.HighestAmountYesVote, + (p1, p2) => parseInt(p1.yes_votes) >= parseInt(p2.yes_votes) + ); +}); + +test("9C_3. Should filter and sort Governance Action Type on outcomes page", async ({ + page, +}) => { + const outcomePage = new OutComesPage(page); + const filterOptionKeys = Object.keys(outcomeType); + const filterOptionNames = Object.values(outcomeType); + + const choice = Math.floor(Math.random() * filterOptionKeys.length); + await outcomePage.goto({ filter: filterOptionKeys[choice] }); + await outcomePage.sortBtn.click(); + + await outcomePage.sortAndValidate( + SortOption.OldestFirst, + (p1, p2) => p1.expiry_date <= p2.expiry_date + ); + + await outcomePage.validateFilters( + [filterOptionNames[choice]], + outcomePage._validateFiltersInOutcomeCard + ); +}); + +test("9E. Should verify all of the displayed governance actions have expired", async ({ + page, +}) => { + const outcomePage = new OutComesPage(page); + await outcomePage.goto(); + + const proposalCards = await outcomePage.getAllOutcomes(); + + for (const proposalCard of proposalCards) { + const expiryDateEl = proposalCard.locator('[data-testid$="-Expired-date"]'); + const expiryDateTxt = await expiryDateEl.innerText(); + const expiryDate = extractExpiryDateFromText(expiryDateTxt); + const today = new Date(); + expect(today >= expiryDate).toBeTruthy(); + } +}); + +test("9F. Should load more Outcomes on show more", async ({ page }) => { + const responsePromise = page.waitForResponse((response) => + response + .url() + .includes(`governance-actions?search=&filters=&sort=newestFirst&page=2`) + ); + const outcomePage = new OutComesPage(page); + await outcomePage.goto(); + + let governanceActionIdsBefore: String[]; + let governanceActionIdsAfter: String[]; + + await functionWaitedAssert( + async () => { + governanceActionIdsBefore = + await outcomePage.getAllListedCIP105GovernanceIds(); + await outcomePage.showMoreBtn.click(); + }, + { message: "Show more button not visible" } + ); + + const response = await responsePromise; + const governanceActionListAfter = await response.json(); + + await functionWaitedAssert( + async () => { + governanceActionIdsAfter = + await outcomePage.getAllListedCIP105GovernanceIds(); + expect(governanceActionIdsAfter.length).toBeGreaterThan( + governanceActionIdsBefore.length + ); + }, + { message: "Outcomes not loaded after clicking show more" } + ); + + if (governanceActionListAfter.length >= governanceActionIdsBefore.length) { + await expect(outcomePage.showMoreBtn).toBeVisible(); + expect(true).toBeTruthy(); + } else { + await expect(outcomePage.showMoreBtn).not.toBeVisible(); + } +}); + +test("9G. Should display correct vote counts on outcome details page", async ({ + browser, +}) => { + await Promise.all( + Object.keys(outcomeType).map(async (filterKey) => { + const page = await browser.newPage(); + injectLogger(page); + const outcomeListResponsePromise = page.waitForResponse((response) => + response + .url() + .includes(`governance-actions?search=&filters=${filterKey}`) + ); + const outcomePage = new OutComesPage(page); + await outcomePage.goto({ filter: filterKey }); + + const outcomeListResponse = await outcomeListResponsePromise; + const proposals = await outcomeListResponse.json(); + + expect( + proposals.length, + proposals.length == 0 && "No proposals found!" + ).toBeGreaterThan(0); + const { + index: governanceActionIndex, + tx_hash: governanceTransactionHash, + } = proposals[0]; + + const govActionDetailsPage = await outcomePage.viewFirstOutcomes(); + + const outcomeResponse = await page.waitForResponse((response) => + response + .url() + .includes( + `governance-actions/${governanceTransactionHash}?index=${governanceActionIndex}` + ) + ); + const proposalToCheck = (await outcomeResponse.json())[0]; + + const metricsResponse = await page.waitForResponse( + (response) => + response.url().includes(`/network/metrics`) && + !response.url().includes(`/misc/network/metrics`) + ); + + const dRepTotalAbstainVote = + await govActionDetailsPage.getDRepTotalAbstainVoted( + proposalToCheck, + metricsResponse + ); + + // check dRep votes + if (await areDRepVoteTotalsDisplayed(proposalToCheck)) { + await expect(govActionDetailsPage.dRepYesVotes).toHaveText( + `₳ ${correctVoteAdaFormat(parseInt(proposalToCheck.yes_votes))}` + ); + await expect(govActionDetailsPage.dRepAbstainVotes).toHaveText( + `₳ ${correctVoteAdaFormat(dRepTotalAbstainVote)}` + ); + await expect(govActionDetailsPage.dRepNoVotes).toHaveText( + `₳ ${correctVoteAdaFormat(parseInt(proposalToCheck.no_votes))}` + ); + } + // check sPos votes + if (await areSPOVoteTotalsDisplayed(proposalToCheck)) { + await expect(govActionDetailsPage.sPosYesVotes).toHaveText( + `₳ ${correctVoteAdaFormat(parseInt(proposalToCheck.pool_yes_votes))}` + ); + await expect(govActionDetailsPage.sPosAbstainVotes).toHaveText( + `₳ ${correctVoteAdaFormat(parseInt(proposalToCheck.pool_abstain_votes))}` + ); + await expect(govActionDetailsPage.sPosNoVotes).toHaveText( + `₳ ${correctVoteAdaFormat(parseInt(proposalToCheck.pool_no_votes))}` + ); + } + + // check ccCommittee votes + if (areCCVoteTotalsDisplayed(proposalToCheck)) { + await expect(govActionDetailsPage.ccCommitteeYesVotes).toHaveText( + `${proposalToCheck.cc_yes_votes}` + ); + await expect(govActionDetailsPage.ccCommitteeAbstainVotes).toHaveText( + `${proposalToCheck.cc_abstain_votes}` + ); + await expect(govActionDetailsPage.ccCommitteeNoVotes).toHaveText( + `${proposalToCheck.cc_no_votes}` + ); + } + }) + ); +});