From 4d181469808f3f00cf81b1b6493aeae3ecdd4bf9 Mon Sep 17 00:00:00 2001 From: Niraj Date: Thu, 20 Feb 2025 10:20:42 +0545 Subject: [PATCH 01/18] feat: add outcomes page and related tests for outcomes governance actions --- .../playwright/lib/helpers/string.ts | 22 ++- .../playwright/lib/pages/outcomesPage.ts | 170 +++++++++++++++++ .../govtool-frontend/playwright/lib/types.ts | 20 ++ .../tests/9-outcomes/outcomes.spec.ts | 174 ++++++++++++++++++ 4 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts create mode 100644 tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts 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/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts new file mode 100644 index 000000000..d20065b32 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -0,0 +1,170 @@ +import environments from "@constants/environments"; +import { toCamelCase } from "@helpers/string"; +import { functionWaitedAssert, waitedLoop } from "@helpers/waitedLoop"; +import { expect, Locator, Page } from "@playwright/test"; +import { outcomeProposal } from "@types"; + +export default class OutComesPage { + // Buttons + readonly filterBtn = this.page.getByRole("button", { name: "Filter" }); // BUG missing test id + readonly sortBtn = this.page.getByRole("button", { name: "Sort:" }); // BUG missing test id + readonly viewDetailsBtn = this.page.getByRole("button", { + name: "View Details", + }); // BUG missing test id + readonly clearBtn = this.page.getByText("clear"); // BUG missing test id + //inputs + readonly searchInput = this.page.getByTestId("search-input"); + + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto(`${environments.frontendUrl}/outcomes`); + } + + async getAllOutcomes(): Promise { + await waitedLoop(async () => { + return ( + (await this.page.locator('[data-testid$="-card"]').count()) > 0 || + this.page.getByText("No results for the search.") + ); + }); + return this.page.locator('[data-testid$="-card"]').all(); // BUG missing testid + } + + 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 filterProposalByNames(names: string[]) { + for (const name of names) { + await this.page.getByLabel(name).click(); + } + } + + async unFilterProposalByNames(names: string[]) { + for (const name of names) { + await this.page.getByLabel(name).click(); + } + } + + async validateFilters( + filters: string[], + validateFunction: (proposalCard: any, filters: string[]) => Promise + ) { + let errorMessage = ""; + await functionWaitedAssert( + async () => { + const proposalCards = await this.getAllOutcomes(); + + for (const proposalCard of proposalCards) { + const type = await proposalCard + .getByTestId("governance-action-type") + .textContent(); + const hasFilter = await validateFunction(proposalCard, filters); + errorMessage = `A governance action type ${type} does not contain on ${filters}`; + expect(hasFilter).toBe(true); + } + }, + { message: errorMessage } + ); + } + + getSortType(sortOption: string) { + let sortType = sortOption; + if (sortOption === "Highest amount of yes votes") { + sortType = "Highest yes votes"; + } + return toCamelCase(sortType); + } + + 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.getByLabel(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] + ); + + console.log(isValid); + + expect(isValid).toBe(true); + } + + await expect( + this.page.getByRole("progressbar").getByRole("img") + ).toBeHidden({ timeout: 20_000 }); + + // TODO Frontend validation + const outcomeCards = await this.getAllOutcomes(); + for (const outcomeCard of outcomeCards) { + const proposalTitle = await outcomeCard + .getByTestId("governance-action-card-title") + .textContent(); + expect(proposalTitle).toContain( + outcomeProposalList[outcomeCards.indexOf(outcomeCard)].title + ); + } + } + + async _validateFiltersInOutcomeCard( + proposalCard: Locator, + filters: string[] + ): Promise { + const govActionType = await proposalCard + .getByTestId("governance-action-type") + .textContent(); + + return filters.includes(govActionType); + } + + async _validateStatusFiltersInOutcomeCard( + proposalCard: Locator, + filters: string[] + ): Promise { + let govActionType = await proposalCard + .locator('[data-testid$="-status"]') + .textContent(); + return filters.includes(govActionType); + } +} diff --git a/tests/govtool-frontend/playwright/lib/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index b4c439ced..27fd4cc24 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -251,3 +251,23 @@ 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; + proposal_params: any; + title: string | null; + abstract: string | null; +} 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..d76a7ad54 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -0,0 +1,174 @@ +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import { skipIfNotHardFork } from "@helpers/cardano"; +import OutComesPage from "@pages/outcomesPage"; +import { expect } from "@playwright/test"; +import { GovernanceActionType, outcomeProposal } from "@types"; + +test.beforeEach(async () => { + await setAllureEpic("9. Outcomes"); + await skipIfNotHardFork(); +}); + +const filterOptionNames = [ + "Protocol Parameter Change", + "Update Committee", + "Hard-Fork Initiation", + "Motion of no Confidence", + "Info", + "Treasury Withdrawals", + "New Constitution", +]; + +const status = ["Expired", "Ratified", "Enacted"]; + +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("/"); + + await page.getByTestId("governance-actions-outcomes-link").click(); + + await expect(page.getByText(/outcomes/i)).toHaveCount(2); +}); + +test("9B. Should search outcomes proposal by title and id", async ({ + page, +}) => { + let governanceActionId: string | undefined; + let governanceActionTitle: string | undefined; + + await page.route("**/api/governance-actions?search=&sort=", async (route) => { + const response = await route.fetch(); + const data = await response.json(); + if (!governanceActionTitle) { + const elementsWithTitle: outcomeProposal[] = data.filter( + (element: outcomeProposal) => element.title != null + ); + if (elementsWithTitle.length > 0) { + const randomIndex = Math.floor( + Math.random() * elementsWithTitle.length + ); + governanceActionId = elementsWithTitle[randomIndex].tx_hash + "#"; + governanceActionTitle = elementsWithTitle[randomIndex].title; + } + } + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(data), + }); + }); + + const responsePromise = page.waitForResponse( + "**/api/governance-actions?search=&sort=" + ); + + const outcomesPage = new OutComesPage(page); + await outcomesPage.goto(); + + await responsePromise; + // search by title + await outcomesPage.searchInput.fill(governanceActionTitle); + + await expect(page.getByRole("progressbar").getByRole("img")).toBeVisible(); + + await expect(outcomesPage.viewDetailsBtn.first()).toBeVisible(); + const titleContentListLength = ( + await page.getByText(governanceActionTitle).all() + ).length; + expect(titleContentListLength).toBeGreaterThanOrEqual(1); + + // search by id + await outcomesPage.searchInput.fill(governanceActionId); + await expect(page.getByRole("progressbar").getByRole("img")).toBeVisible(); + + await expect(outcomesPage.viewDetailsBtn.first()).toBeVisible(); + const idContentListLength = (await page.getByText(governanceActionId).all()) + .length; + expect(idContentListLength).toBeGreaterThanOrEqual(1); +}); + +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(); + + // proposal type filter + await outcomePage.applyAndValidateFilters( + filterOptionNames, + outcomePage._validateFiltersInOutcomeCard + ); + + // proposal status filter + await outcomePage.applyAndValidateFilters( + status, + outcomePage._validateFiltersInOutcomeCard + ); +}); + +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.clearBtn.click(); + + await outcomePage.sortAndValidate( + SortOption.NewestFirst, + (p1, p2) => p1.expiry_date >= p2.time + ); + + await outcomePage.sortAndValidate( + SortOption.OldestFirst, + (p1, p2) => p1.expiry_date <= p2.expiry_date + ); + + 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, +}) => { + test.slow(); + + const outcomePage = new OutComesPage(page); + await outcomePage.goto(); + + await outcomePage.filterBtn.click(); + + const choice = Math.floor(Math.random() * filterOptionNames.length); + await outcomePage.filterProposalByNames([filterOptionNames[choice]]); + + await outcomePage.sortBtn.click({ force: true }); + await outcomePage.clearBtn.click(); + + await outcomePage.sortAndValidate( + SortOption.NewestFirst, + (p1, p2) => p1.expiry_date >= p2.expiry_date, + Object.values(GovernanceActionType)[choice] + ); + + await outcomePage.validateFilters( + [filterOptionNames[choice]], + outcomePage._validateFiltersInOutcomeCard + ); +}); From a7e75d18b2499b9ffa98f6dfda899baad90152a4 Mon Sep 17 00:00:00 2001 From: Niraj Date: Fri, 28 Feb 2025 15:16:05 +0545 Subject: [PATCH 02/18] feat: add outcome and outcomeMetadata types --- .../playwright/lib/constants/index.ts | 8 +++++++ .../govtool-frontend/playwright/lib/types.ts | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+) 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/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index 27fd4cc24..d135d546a 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", @@ -271,3 +281,16 @@ export interface outcomeProposal { title: string | null; abstract: string | null; } + +export interface outcomeMetadata { + authors: any[]; + hashAlgorithm: string; + body: outcomeMetadataBody; +} + +interface outcomeMetadataBody { + abstract: string; + motivation: "string"; + rationale: string; + title: string; +} From 25e2be812a2d5b59cd9404353bf85601bdb58ab7 Mon Sep 17 00:00:00 2001 From: Niraj Date: Fri, 28 Feb 2025 15:17:03 +0545 Subject: [PATCH 03/18] fix: update test IDs and filtering functionality --- .../lib/pages/governanceActionsPage.ts | 2 +- .../playwright/lib/pages/outcomesPage.ts | 127 +++++++++++------- 2 files changed, 80 insertions(+), 49 deletions(-) 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/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts index d20065b32..5c85f8159 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -1,17 +1,14 @@ 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 } from "@types"; +import { outcomeProposal, outcomeType } from "@types"; export default class OutComesPage { // Buttons - readonly filterBtn = this.page.getByRole("button", { name: "Filter" }); // BUG missing test id - readonly sortBtn = this.page.getByRole("button", { name: "Sort:" }); // BUG missing test id - readonly viewDetailsBtn = this.page.getByRole("button", { - name: "View Details", - }); // BUG missing test id - readonly clearBtn = this.page.getByText("clear"); // BUG missing test id + readonly filterBtn = this.page.getByTestId("filters-button"); + readonly sortBtn = this.page.getByTestId("sort-button"); //inputs readonly searchInput = this.page.getByTestId("search-input"); @@ -24,11 +21,30 @@ export default class OutComesPage { async getAllOutcomes(): Promise { await waitedLoop(async () => { return ( - (await this.page.locator('[data-testid$="-card"]').count()) > 0 || - this.page.getByText("No results for the search.") + (await this.page.locator('[data-testid$="-outcome-card"]').count()) > + 0 || + (await this.page.getByText("No results for the search.").isVisible()) ); }); - return this.page.locator('[data-testid$="-card"]').all(); // BUG missing testid + 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( @@ -53,37 +69,29 @@ export default class OutComesPage { } } - async filterProposalByNames(names: string[]) { - for (const name of names) { - await this.page.getByLabel(name).click(); - } - } - - async unFilterProposalByNames(names: string[]) { - for (const name of names) { - await this.page.getByLabel(name).click(); - } - } - async validateFilters( filters: string[], validateFunction: (proposalCard: any, filters: string[]) => Promise ) { - let errorMessage = ""; await functionWaitedAssert( async () => { const proposalCards = await this.getAllOutcomes(); - for (const proposalCard of proposalCards) { const type = await proposalCard - .getByTestId("governance-action-type") + .locator('[data-testid$="-type"]') .textContent(); + const outcomeType = type.replace(/^.*Type/, ""); const hasFilter = await validateFunction(proposalCard, filters); - errorMessage = `A governance action type ${type} does not contain on ${filters}`; + if (!hasFilter) { + const errorMessage = `A outcomne type ${outcomeType} does not contain on ${filters}`; + throw errorMessage; + } expect(hasFilter).toBe(true); } }, - { message: errorMessage } + { + name: "validateFilters", + } ); } @@ -95,6 +103,11 @@ export default class OutComesPage { 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, @@ -106,12 +119,12 @@ export default class OutComesPage { .url() .includes( filterKey - ? `&filters[]=${filterKey}&sort=${sortType}` + ? `&filters=${filterKey}&sort=${sortType}` : `&sort=${sortType}` ) ); - await this.page.getByLabel(sortOption).click(); + await this.page.getByTestId(this.getSortTestId(sortOption)).click(); const response = await responsePromise; const data = await response.json(); @@ -125,9 +138,6 @@ export default class OutComesPage { outcomeProposalList[i], outcomeProposalList[i + 1] ); - - console.log(isValid); - expect(isValid).toBe(true); } @@ -135,36 +145,57 @@ export default class OutComesPage { this.page.getByRole("progressbar").getByRole("img") ).toBeHidden({ timeout: 20_000 }); - // TODO Frontend validation - const outcomeCards = await this.getAllOutcomes(); - for (const outcomeCard of outcomeCards) { - const proposalTitle = await outcomeCard - .getByTestId("governance-action-card-title") - .textContent(); - expect(proposalTitle).toContain( - outcomeProposalList[outcomeCards.indexOf(outcomeCard)].title - ); - } + 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 govActionType = await proposalCard - .getByTestId("governance-action-type") + const type = await proposalCard + .locator('[data-testid$="-type"]') .textContent(); - - return filters.includes(govActionType); + const outcomeType = type.replace(/^.*Type/, ""); + return filters.includes(outcomeType); } async _validateStatusFiltersInOutcomeCard( proposalCard: Locator, filters: string[] ): Promise { - let govActionType = await proposalCard + const status = await proposalCard .locator('[data-testid$="-status"]') .textContent(); - return filters.includes(govActionType); + const outcomeStatus = outcomeStatusType.filter((statusType) => { + return status.includes(statusType); + }); + return outcomeStatus.some((status) => filters.includes(status)); } } From f7ac54b92d21bc3bd8fe8d54486d17f943dd920e Mon Sep 17 00:00:00 2001 From: Niraj Date: Fri, 28 Feb 2025 15:17:46 +0545 Subject: [PATCH 04/18] fix: search by id and title logic --- .../tests/9-outcomes/outcomes.spec.ts | 128 ++++++++++-------- 1 file changed, 75 insertions(+), 53 deletions(-) diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index d76a7ad54..b9fe58109 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -1,26 +1,17 @@ import { test } from "@fixtures/walletExtension"; import { setAllureEpic } from "@helpers/allure"; import { skipIfNotHardFork } from "@helpers/cardano"; +import { functionWaitedAssert } from "@helpers/waitedLoop"; import OutComesPage from "@pages/outcomesPage"; import { expect } from "@playwright/test"; -import { GovernanceActionType, outcomeProposal } from "@types"; +import { outcomeMetadata, outcomeProposal, outcomeType } from "@types"; test.beforeEach(async () => { await setAllureEpic("9. Outcomes"); await skipIfNotHardFork(); }); -const filterOptionNames = [ - "Protocol Parameter Change", - "Update Committee", - "Hard-Fork Initiation", - "Motion of no Confidence", - "Info", - "Treasury Withdrawals", - "New Constitution", -]; - -const status = ["Expired", "Ratified", "Enacted"]; +const status = ["Expired", "Ratified", "Enacted", "Live"]; enum SortOption { SoonToExpire = "Soon to expire", @@ -42,22 +33,36 @@ test("9B. Should search outcomes proposal by title and id", async ({ page, }) => { let governanceActionId: string | undefined; - let governanceActionTitle: string | undefined; + let governanceActionTitle: string | undefined = "asd"; + + // 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), + }); + } + ); - await page.route("**/api/governance-actions?search=&sort=", async (route) => { + // intercept metadata for title + await page.route("**/governance-actions/metadata?**", async (route) => { const response = await route.fetch(); - const data = await response.json(); - if (!governanceActionTitle) { - const elementsWithTitle: outcomeProposal[] = data.filter( - (element: outcomeProposal) => element.title != null - ); - if (elementsWithTitle.length > 0) { - const randomIndex = Math.floor( - Math.random() * elementsWithTitle.length - ); - governanceActionId = elementsWithTitle[randomIndex].tx_hash + "#"; - governanceActionTitle = elementsWithTitle[randomIndex].title; - } + if (response.status() !== 200) return response; + const data: outcomeMetadata = await response.json(); + if (!governanceActionTitle && data.body.title != null) { + governanceActionTitle = data.body.title; } await route.fulfill({ status: 200, @@ -67,32 +72,51 @@ test("9B. Should search outcomes proposal by title and id", async ({ }); const responsePromise = page.waitForResponse( - "**/api/governance-actions?search=&sort=" + "**/governance-actions?search=&filters=&sort=**" + ); + const metadataResponsePromise = page.waitForResponse( + "**/governance-actions/metadata?**" ); const outcomesPage = new OutComesPage(page); await outcomesPage.goto(); await responsePromise; - // search by title - await outcomesPage.searchInput.fill(governanceActionTitle); + await metadataResponsePromise; + // search by id + await outcomesPage.searchInput.fill(governanceActionId); await expect(page.getByRole("progressbar").getByRole("img")).toBeVisible(); - await expect(outcomesPage.viewDetailsBtn.first()).toBeVisible(); - const titleContentListLength = ( - await page.getByText(governanceActionTitle).all() - ).length; - expect(titleContentListLength).toBeGreaterThanOrEqual(1); + await functionWaitedAssert( + async () => { + const idSearchOutcomeCards = await outcomesPage.getAllOutcomes(); + 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 id - await outcomesPage.searchInput.fill(governanceActionId); + // search by title + await outcomesPage.searchInput.fill(governanceActionTitle); await expect(page.getByRole("progressbar").getByRole("img")).toBeVisible(); - await expect(outcomesPage.viewDetailsBtn.first()).toBeVisible(); - const idContentListLength = (await page.getByText(governanceActionId).all()) - .length; - expect(idContentListLength).toBeGreaterThanOrEqual(1); + await functionWaitedAssert( + async () => { + const titleSearchOutcomeCards = await outcomesPage.getAllOutcomes(); + for (const outcomeCard of titleSearchOutcomeCards) { + const title = await outcomeCard + .locator('[data-testid$="-card-title"]') + .textContent(); + expect(title).toContain(governanceActionTitle); + } + }, + { name: "search by title" } + ); }); test("9C_1. Should filter Governance Action Type on governance actions page", async ({ @@ -104,6 +128,7 @@ test("9C_1. Should filter Governance Action Type on governance actions page", as await outcomePage.goto(); await outcomePage.filterBtn.click(); + const filterOptionNames = Object.values(outcomeType); // proposal type filter await outcomePage.applyAndValidateFilters( @@ -114,7 +139,7 @@ test("9C_1. Should filter Governance Action Type on governance actions page", as // proposal status filter await outcomePage.applyAndValidateFilters( status, - outcomePage._validateFiltersInOutcomeCard + outcomePage._validateStatusFiltersInOutcomeCard ); }); @@ -127,16 +152,15 @@ test("9C_2. Should sort Governance Action Type on outcomes page", async ({ await outcomePage.goto(); await outcomePage.sortBtn.click(); - await outcomePage.clearBtn.click(); await outcomePage.sortAndValidate( - SortOption.NewestFirst, - (p1, p2) => p1.expiry_date >= p2.time + SortOption.OldestFirst, + (p1, p2) => p1.expiry_date <= p2.expiry_date ); await outcomePage.sortAndValidate( - SortOption.OldestFirst, - (p1, p2) => p1.expiry_date <= p2.expiry_date + SortOption.NewestFirst, + (p1, p2) => p1.expiry_date >= p2.time ); await outcomePage.sortAndValidate( @@ -148,23 +172,21 @@ test("9C_2. Should sort Governance Action Type on outcomes page", async ({ test("9C_3. Should filter and sort Governance Action Type on outcomes page", async ({ page, }) => { - test.slow(); - const outcomePage = new OutComesPage(page); await outcomePage.goto(); await outcomePage.filterBtn.click(); + const filterOptionNames = Object.values(outcomeType); const choice = Math.floor(Math.random() * filterOptionNames.length); await outcomePage.filterProposalByNames([filterOptionNames[choice]]); - await outcomePage.sortBtn.click({ force: true }); - await outcomePage.clearBtn.click(); + await outcomePage.filterBtn.click({ force: true }); + await outcomePage.sortBtn.click(); await outcomePage.sortAndValidate( - SortOption.NewestFirst, - (p1, p2) => p1.expiry_date >= p2.expiry_date, - Object.values(GovernanceActionType)[choice] + SortOption.OldestFirst, + (p1, p2) => p1.expiry_date <= p2.expiry_date ); await outcomePage.validateFilters( From 10a26a52100e61bd20ef443b9a21f7d8a7d7edcc Mon Sep 17 00:00:00 2001 From: Niraj Date: Fri, 28 Feb 2025 15:41:06 +0545 Subject: [PATCH 05/18] test: 9D. copy governance actionId --- .../tests/9-outcomes/outcomes.spec.ts | 198 +++++++++++------- 1 file changed, 118 insertions(+), 80 deletions(-) diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index b9fe58109..8eb8bbf7f 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -3,7 +3,7 @@ import { setAllureEpic } from "@helpers/allure"; import { skipIfNotHardFork } from "@helpers/cardano"; import { functionWaitedAssert } from "@helpers/waitedLoop"; import OutComesPage from "@pages/outcomesPage"; -import { expect } from "@playwright/test"; +import { expect, Page } from "@playwright/test"; import { outcomeMetadata, outcomeProposal, outcomeType } from "@types"; test.beforeEach(async () => { @@ -29,94 +29,132 @@ test("9A. Should access Outcomes page in disconnect state", async ({ await expect(page.getByText(/outcomes/i)).toHaveCount(2); }); -test("9B. Should search outcomes proposal by title and id", async ({ - page, -}) => { +test.describe("outcome details dependent test", () => { let governanceActionId: string | undefined; - let governanceActionTitle: string | undefined = "asd"; - - // 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; + 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; } - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(data), - }); - } - ); - - // intercept metadata for title - await page.route("**/governance-actions/metadata?**", async (route) => { - const response = await route.fetch(); - if (response.status() !== 200) return response; - 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), }); - }); - - const responsePromise = page.waitForResponse( - "**/governance-actions?search=&filters=&sort=**" - ); - const metadataResponsePromise = page.waitForResponse( - "**/governance-actions/metadata?**" - ); - const outcomesPage = new OutComesPage(page); - await outcomesPage.goto(); + const responsePromise = page.waitForResponse( + "**/governance-actions?search=&filters=&sort=**" + ); + const metadataResponsePromise = page.waitForResponse( + "**/governance-actions/metadata?**" + ); - await responsePromise; - await metadataResponsePromise; + const outcomesPage = new OutComesPage(page); + await outcomesPage.goto(); - // search by id - await outcomesPage.searchInput.fill(governanceActionId); - await expect(page.getByRole("progressbar").getByRole("img")).toBeVisible(); + await responsePromise; + await metadataResponsePromise; + currentPage = page; + }); - await functionWaitedAssert( - async () => { - const idSearchOutcomeCards = await outcomesPage.getAllOutcomes(); - 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" } - ); + 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(); + 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(); + for (const outcomeCard of titleSearchOutcomeCards) { + const title = await outcomeCard + .locator('[data-testid$="-card-title"]') + .textContent(); + expect(title).toContain(governanceActionTitle); + } + }, + { name: "search by title" } + ); + }); - // search by title - await outcomesPage.searchInput.fill(governanceActionTitle); - await expect(page.getByRole("progressbar").getByRole("img")).toBeVisible(); - - await functionWaitedAssert( - async () => { - const titleSearchOutcomeCards = await outcomesPage.getAllOutcomes(); - 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 ({ From 59608e946b54b57e991607f2d319d7d334b8248d Mon Sep 17 00:00:00 2001 From: Niraj Date: Fri, 28 Feb 2025 15:57:44 +0545 Subject: [PATCH 06/18] test: 9E. display only expired governance action --- .../lib/helpers/extractExpiryDateFromText.ts | 13 +++++++++---- .../tests/9-outcomes/outcomes.spec.ts | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts b/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts index 7f42d8817..614e96985 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts @@ -13,14 +13,19 @@ const monthNames = [ "Dec", ]; -export default function extractExpiryDateFromText(text: string): Date | null { - const regex = /(\d{1,2})(st|nd|rd|th) ([\w]{3}) (\d{4})/; +export default function extractExpiryDateFromText( + text: string, + isOutcome = false +): Date | null { + const regex = isOutcome + ? /(\d{1,2}) (\w{3}) (\d{4})/ + : /(\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 = isOutcome ? match[2] : match[3]; + const year = parseInt(isOutcome ? match[3] : match[4]); return new Date(year, monthNames.indexOf(month), day); } else { diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index 8eb8bbf7f..640e29be4 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -1,6 +1,7 @@ import { test } from "@fixtures/walletExtension"; import { setAllureEpic } from "@helpers/allure"; import { skipIfNotHardFork } from "@helpers/cardano"; +import extractExpiryDateFromText from "@helpers/extractExpiryDateFromText"; import { functionWaitedAssert } from "@helpers/waitedLoop"; import OutComesPage from "@pages/outcomesPage"; import { expect, Page } from "@playwright/test"; @@ -232,3 +233,20 @@ test("9C_3. Should filter and sort Governance Action Type on outcomes page", asy 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, true); + const today = new Date(); + expect(today >= expiryDate).toBeTruthy(); + } +}); From 92435555c5c1e336efc5496e2807c096ed7580ca Mon Sep 17 00:00:00 2001 From: Niraj Date: Mon, 3 Mar 2025 15:09:05 +0545 Subject: [PATCH 07/18] test: 9F. add load more outcomes test --- .../playwright/lib/pages/outcomesPage.ts | 16 +++++++ .../tests/9-outcomes/outcomes.spec.ts | 43 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts index 5c85f8159..3e1c5670b 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -9,6 +9,8 @@ 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"); @@ -18,6 +20,20 @@ export default class OutComesPage { await this.page.goto(`${environments.frontendUrl}/outcomes`); } + 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 getAllOutcomes(): Promise { await waitedLoop(async () => { return ( diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index 640e29be4..75fd71a77 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -250,3 +250,46 @@ test("9E. Should verify all of the displayed governance actions have expired", a 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(); + } +}); From bb28bf6dd1763bf88a9d95f17cdc8cf247f67bdf Mon Sep 17 00:00:00 2001 From: Niraj Date: Mon, 3 Mar 2025 20:13:29 +0545 Subject: [PATCH 08/18] feat: extend outcomes type with more details --- tests/govtool-frontend/playwright/lib/types.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/govtool-frontend/playwright/lib/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index d135d546a..ef277f297 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -277,9 +277,17 @@ export interface outcomeProposal { epoch_no: number; url: string; data_hash: string; - proposal_params: any; 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 { From c7218b85f7b66ba667120e8f72a277437bc46dd4 Mon Sep 17 00:00:00 2001 From: Niraj Date: Mon, 3 Mar 2025 20:15:19 +0545 Subject: [PATCH 09/18] refactor: enhance vote flag for both outcome and governance action --- .../playwright/lib/helpers/featureFlag.ts | 105 ++++++++++++++---- .../proposalVisibility.dRep.spec.ts | 4 +- .../proposalVisibility.spec.ts | 4 +- 3 files changed, 85 insertions(+), 28 deletions(-) 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/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts index c10950fe0..c7c14366f 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 @@ -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.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts index 5c953622b..d5ff84b07 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 @@ -110,9 +110,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}` ); From cee1d295558b653580271ac437e19e708b38df09 Mon Sep 17 00:00:00 2001 From: Niraj Date: Mon, 3 Mar 2025 20:15:47 +0545 Subject: [PATCH 10/18] feat: add OutcomeDetailsPage class --- .../lib/pages/outcomeDetailsPage.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts 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); + } + } +} From 5bf7da39dad9935a0a12fcea2db4c3486c4fe93d Mon Sep 17 00:00:00 2001 From: Niraj Date: Mon, 3 Mar 2025 20:16:07 +0545 Subject: [PATCH 11/18] feat: add filtering to outcomes page and implement view details functionality --- .../playwright/lib/pages/outcomesPage.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts index 3e1c5670b..62510f8b2 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -4,6 +4,7 @@ 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 @@ -16,8 +17,12 @@ export default class OutComesPage { constructor(private readonly page: Page) {} - async goto() { - await this.page.goto(`${environments.frontendUrl}/outcomes`); + async goto(filter?: string): Promise { + await this.page.goto( + filter + ? `${environments.frontendUrl}/outcomes?sort=newestFirst&type=${filter}` + : `${environments.frontendUrl}/outcomes?sort=newestFirst` + ); } async getAllListedCIP105GovernanceIds(): Promise { @@ -34,6 +39,11 @@ export default class OutComesPage { 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 ( From a8785cf87bb0506004fa6f98bcf4750bb5563625 Mon Sep 17 00:00:00 2001 From: Niraj Date: Mon, 3 Mar 2025 20:16:30 +0545 Subject: [PATCH 12/18] test: 9G. add verification for vote counts on outcome details page --- .../tests/9-outcomes/outcomes.spec.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index 75fd71a77..fce0199b0 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -1,7 +1,14 @@ 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 { injectLogger } from "@helpers/page"; import { functionWaitedAssert } from "@helpers/waitedLoop"; import OutComesPage from "@pages/outcomesPage"; import { expect, Page } from "@playwright/test"; @@ -293,3 +300,94 @@ test("9F. Should load more Outcomes on show more", async ({ page }) => { 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(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}` + ); + } + }) + ); +}); From b441c6dc3132ef2e38794c630841cbd6acc517d1 Mon Sep 17 00:00:00 2001 From: Niraj Date: Tue, 4 Mar 2025 09:56:26 +0545 Subject: [PATCH 13/18] fix: outcomes page visibility test for mobile access and improve filtering logic --- .../playwright/tests/9-outcomes/outcomes.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index fce0199b0..607971638 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -8,6 +8,7 @@ import { 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"; @@ -32,6 +33,9 @@ test("9A. Should access Outcomes page in disconnect state", async ({ }) => { 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); @@ -219,15 +223,11 @@ test("9C_3. Should filter and sort Governance Action Type on outcomes page", asy page, }) => { const outcomePage = new OutComesPage(page); - await outcomePage.goto(); - - await outcomePage.filterBtn.click(); + const filterOptionKeys = Object.keys(outcomeType); const filterOptionNames = Object.values(outcomeType); - const choice = Math.floor(Math.random() * filterOptionNames.length); - await outcomePage.filterProposalByNames([filterOptionNames[choice]]); - - await outcomePage.filterBtn.click({ force: true }); + const choice = Math.floor(Math.random() * filterOptionKeys.length); + await outcomePage.goto(filterOptionKeys[choice]); await outcomePage.sortBtn.click(); await outcomePage.sortAndValidate( From 996c61da6567c1059b664f35b97bfa5b08a6204a Mon Sep 17 00:00:00 2001 From: Niraj Date: Tue, 4 Mar 2025 10:44:42 +0545 Subject: [PATCH 14/18] fix: add assertions for governance actions presence outcome search --- .../playwright/lib/pages/outcomesPage.ts | 2 +- .../playwright/tests/9-outcomes/outcomes.spec.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts index 62510f8b2..322ca3d3f 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -49,7 +49,7 @@ export default class OutComesPage { return ( (await this.page.locator('[data-testid$="-outcome-card"]').count()) > 0 || - (await this.page.getByText("No results for the search.").isVisible()) + (await this.page.getByText("No governance actions found").isVisible()) ); }); return await this.page.locator('[data-testid$="-outcome-card"]').all(); diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index 607971638..62296246d 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -117,6 +117,10 @@ test.describe("outcome details dependent test", () => { 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"]') @@ -136,6 +140,11 @@ test.describe("outcome details dependent test", () => { 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"]') From aca5d7a53a4d1abefb525469febe8edd4353d9ae Mon Sep 17 00:00:00 2001 From: Niraj Date: Tue, 4 Mar 2025 12:44:17 +0545 Subject: [PATCH 15/18] refactor: simplify expiry date extraction logic and remove unused parameter --- .../lib/helpers/extractExpiryDateFromText.ts | 13 ++++--------- .../playwright/tests/9-outcomes/outcomes.spec.ts | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts b/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts index 614e96985..547a40cc0 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/extractExpiryDateFromText.ts @@ -13,19 +13,14 @@ const monthNames = [ "Dec", ]; -export default function extractExpiryDateFromText( - text: string, - isOutcome = false -): Date | null { - const regex = isOutcome - ? /(\d{1,2}) (\w{3}) (\d{4})/ - : /(\d{1,2})(st|nd|rd|th) ([\w]{3}) (\d{4})/; +export default function extractExpiryDateFromText(text: string): Date | null { + 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 = isOutcome ? match[2] : match[3]; - const year = parseInt(isOutcome ? match[3] : 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/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index 62296246d..9b4824cec 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -261,7 +261,7 @@ test("9E. Should verify all of the displayed governance actions have expired", a for (const proposalCard of proposalCards) { const expiryDateEl = proposalCard.locator('[data-testid$="-Expired-date"]'); const expiryDateTxt = await expiryDateEl.innerText(); - const expiryDate = extractExpiryDateFromText(expiryDateTxt, true); + const expiryDate = extractExpiryDateFromText(expiryDateTxt); const today = new Date(); expect(today >= expiryDate).toBeTruthy(); } From 87e7519ced5cb24547f0470e2d43ee220eb1fe3f Mon Sep 17 00:00:00 2001 From: Niraj Date: Tue, 4 Mar 2025 12:47:26 +0545 Subject: [PATCH 16/18] refactor : shift possible proposal visibility test from logged-in to non-logged-in --- .../proposalVisibility.loggedin.spec.ts | 174 ------------------ .../proposalVisibility.spec.ts | 174 ++++++++++++++++++ 2 files changed, 174 insertions(+), 174 deletions(-) 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 d5ff84b07..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, From d5bd10235d1aa73092c658bf4c1a7dc36e4ff132 Mon Sep 17 00:00:00 2001 From: Niraj Date: Tue, 4 Mar 2025 12:56:46 +0545 Subject: [PATCH 17/18] refactor: enhance outcomes page navigation by filter and sort --- .../playwright/lib/pages/outcomesPage.ts | 14 ++++++++------ .../playwright/tests/9-outcomes/outcomes.spec.ts | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts index 322ca3d3f..3fad8b410 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -17,12 +17,14 @@ export default class OutComesPage { constructor(private readonly page: Page) {} - async goto(filter?: string): Promise { - await this.page.goto( - filter - ? `${environments.frontendUrl}/outcomes?sort=newestFirst&type=${filter}` - : `${environments.frontendUrl}/outcomes?sort=newestFirst` - ); + 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 { diff --git a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts index 9b4824cec..7e4cba31f 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -236,7 +236,7 @@ test("9C_3. Should filter and sort Governance Action Type on outcomes page", asy const filterOptionNames = Object.values(outcomeType); const choice = Math.floor(Math.random() * filterOptionKeys.length); - await outcomePage.goto(filterOptionKeys[choice]); + await outcomePage.goto({ filter: filterOptionKeys[choice] }); await outcomePage.sortBtn.click(); await outcomePage.sortAndValidate( @@ -323,7 +323,7 @@ test("9G. Should display correct vote counts on outcome details page", async ({ .includes(`governance-actions?search=&filters=${filterKey}`) ); const outcomePage = new OutComesPage(page); - await outcomePage.goto(filterKey); + await outcomePage.goto({ filter: filterKey }); const outcomeListResponse = await outcomeListResponsePromise; const proposals = await outcomeListResponse.json(); From 3791cbdcbf34736ab65e09fff4a4c65e9c5a536a Mon Sep 17 00:00:00 2001 From: Niraj Date: Tue, 4 Mar 2025 12:57:34 +0545 Subject: [PATCH 18/18] refactor: capatilize test describe block titles --- .../tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts | 2 +- .../7-proposal-submission/proposalSubmission.loggedin.spec.ts | 2 +- .../playwright/tests/9-outcomes/outcomes.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 c7c14366f..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); 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 index 7e4cba31f..6a8108728 100644 --- a/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts +++ b/tests/govtool-frontend/playwright/tests/9-outcomes/outcomes.spec.ts @@ -41,7 +41,7 @@ test("9A. Should access Outcomes page in disconnect state", async ({ await expect(page.getByText(/outcomes/i)).toHaveCount(2); }); -test.describe("outcome details dependent test", () => { +test.describe("Outcome details dependent test", () => { let governanceActionId: string | undefined; let governanceActionTitle: string | undefined; let currentPage: Page;