diff --git a/tests/govtool-frontend/playwright/lib/_mock/index.ts b/tests/govtool-frontend/playwright/lib/_mock/index.ts new file mode 100644 index 00000000..4c687204 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/_mock/index.ts @@ -0,0 +1,49 @@ +import { faker } from "@faker-js/faker"; + +export const invalid = { + url: () => { + const invalidSchemes = ["ftp", "unsupported", "unknown-scheme"]; + const invalidCharacters = "<>@!#$%^&*()"; + const invalidTlds = [".invalid", ".example", ".test"]; + + const scheme = + invalidSchemes[Math.floor(Math.random() * invalidSchemes.length)]; + const invalidChar = + invalidCharacters[Math.floor(Math.random() * invalidCharacters.length)]; + const invalidTld = + invalidTlds[Math.floor(Math.random() * invalidTlds.length)]; + + const randomDomain = `example${invalidChar}domain${invalidTld}`; + return `${scheme}://${randomDomain}`; + }, + + proposalTitle: () => { + const choice = faker.number.int({ min: 1, max: 2 }); + if (choice === 1) { + // maximum 80 words invalid + return faker.lorem.paragraphs(4).replace(/\s+/g, ""); + } + // empty invalid + return " "; + }, + + paragraph: () => { + const choice = faker.number.int({ min: 1, max: 2 }); + if (choice === 1) { + // maximum 500 words + return faker.lorem.paragraphs(40); + } + // empty invalid + return " "; + }, + + amount: () => { + const choice = faker.number.int({ min: 1, max: 2 }); + if (choice === 1) { + // only number is allowed + return faker.lorem.word(); + } + // empty invalid + return " "; + }, +}; diff --git a/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts b/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts index 2525ed5d..0b23467d 100644 --- a/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts +++ b/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts @@ -34,7 +34,9 @@ export async function createTempAdaHolderAuth( return tempAdaHolderAuth; } -export async function createTempUserAuth(page: Page) { +export async function createTempUserAuth(page: Page, wallet: ShelleyWallet) { + await importWallet(page, wallet.json()); + const loginPage = new LoginPage(page); await loginPage.login(); await loginPage.isLoggedIn(); diff --git a/tests/govtool-frontend/playwright/lib/helpers/proposalSubmission.ts b/tests/govtool-frontend/playwright/lib/helpers/proposalSubmission.ts new file mode 100644 index 00000000..89cd2ca1 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/helpers/proposalSubmission.ts @@ -0,0 +1,71 @@ +import environments from "@constants/environments"; +import { faker } from "@faker-js/faker"; +import { invalid } from "@mock/index"; +import ProposalSubmission from "@pages/proposalSubmissionPage"; +import { IGovernanceProposal, ProposalType } from "@types"; +import { ShelleyWallet } from "./crypto"; + +export function generateValidProposalFormField( + proposalType: ProposalType, + receivingAddress?: string +) { + const proposal: IGovernanceProposal = { + title: faker.lorem.sentence(6), + abstract: faker.lorem.paragraph(2), + motivation: faker.lorem.paragraphs(2), + rationale: faker.lorem.paragraphs(2), + + extraContentLinks: [faker.internet.url()], + type: proposalType, + }; + if (proposalType === "Treasury") { + (proposal.receivingAddress = receivingAddress), + (proposal.amount = faker.number.int({ min: 100, max: 1000 }).toString()); + } + return proposal; +} + +export function generateInValidProposalFormField(proposalType: ProposalType) { + const proposal: IGovernanceProposal = { + title: invalid.proposalTitle(), + abstract: invalid.paragraph(), + motivation: invalid.paragraph(), + rationale: invalid.paragraph(), + + extraContentLinks: [invalid.url()], + type: proposalType, + }; + if (proposalType === "Treasury") { + (proposal.receivingAddress = faker.location.streetAddress()), + (proposal.amount = invalid.amount()); + } + return proposal; +} + +export async function submitInfoProposal( + proposalSubmissionPage: ProposalSubmission, + proposalType: ProposalType +) { + await proposalSubmissionPage.infoRadioButton.click(); + await proposalSubmissionPage.continueBtn.click(); + + const infoProposal: IGovernanceProposal = + generateValidProposalFormField(proposalType); + await proposalSubmissionPage.register({ ...infoProposal }); +} + +export async function submitTreasuryProposal( + proposalSubmissionPage: ProposalSubmission, + proposalType: ProposalType, + wallet: ShelleyWallet +) { + await proposalSubmissionPage.treasuryRadioButton.click(); + await proposalSubmissionPage.continueBtn.click(); + + const treasuryProposal: IGovernanceProposal = generateValidProposalFormField( + proposalType, + wallet.rewardAddressBech32(environments.networkId) + ); + + await proposalSubmissionPage.register({ ...treasuryProposal }); +} diff --git a/tests/govtool-frontend/playwright/lib/pages/proposalSubmissionPage.ts b/tests/govtool-frontend/playwright/lib/pages/proposalSubmissionPage.ts new file mode 100644 index 00000000..4f55afdb --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/proposalSubmissionPage.ts @@ -0,0 +1,207 @@ +import { downloadMetadata } from "@helpers/metadata"; +import { Download, Page, expect } from "@playwright/test"; +import metadataBucketService from "@services/metadataBucketService"; +import { IGovernanceProposal, ProposalType } from "@types"; +import environments from "lib/constants/environments"; +import { withTxConfirmation } from "lib/transaction.decorator"; +const formErrors = { + proposalTitle: ["max-80-characters-error", "this-field-is-required-error"], + abstract: "this-field-is-required-error", + motivation: "this-field-is-required-error", + Rationale: "this-field-is-required-error", + receivingAddress: "invalid-bech32-address-error", + amount: ["only-number-is-allowed-error", "this-field-is-required-error"], + link: "invalid-url-error", +}; + +export default class ProposalSubmission { + // modals + readonly registrationSuccessModal = this.page.getByTestId( + "create-governance-action-submitted-modal" + ); + + // buttons + readonly registerBtn = this.page.getByTestId("register-button"); + readonly skipBtn = this.page.getByTestId("skip-button"); + readonly confirmBtn = this.page.getByTestId("confirm-modal-button"); + + readonly continueBtn = this.page.getByTestId("continue-button"); + readonly addLinkBtn = this.page.getByRole("button", { name: "+ Add link" }); // BUG testid= add-link-button + readonly infoRadioButton = this.page.getByTestId("Info-radio"); + readonly treasuryRadioButton = this.page.getByTestId("Treasury-radio"); + + // input fields + readonly titleInput = this.page.getByPlaceholder("A name for this Action"); // BUG testid = title-input + readonly abstractInput = this.page.getByPlaceholder("Summary"); // BUG testid = abstract-input + readonly motivationInput = this.page.getByPlaceholder( + "Problem this GA will solve" + ); // BUG testid = motivation-input + readonly rationaleInput = this.page.getByPlaceholder( + "Content of Governance Action" + ); // BUG testid = rationale-input + readonly linkInput = this.page.getByPlaceholder("https://website.com/"); // BUG testid = link-input + readonly receivingAddressInput = this.page.getByPlaceholder( + "The address to receive funds" + ); + readonly amountInput = this.page.getByPlaceholder("e.g."); + + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto( + `${environments.frontendUrl}/create_governance_action` + ); + await this.continueBtn.click(); + } + + @withTxConfirmation + async register(governanceProposal: IGovernanceProposal) { + await this.fillupForm(governanceProposal); + + await this.continueBtn.click(); + await this.continueBtn.click(); + await this.page.getByRole("checkbox").click(); + await this.continueBtn.click(); + + this.page + .getByRole("button", { name: `${governanceProposal.type}.jsonld` }) + .click(); // BUG test id = metadata-download-button + + const dRepMetadata = await this.downloadVoteMetadata(); + const url = await metadataBucketService.uploadMetadata( + dRepMetadata.name, + dRepMetadata.data + ); + await this.page.getByPlaceholder("URL").fill(url); + await this.continueBtn.click(); + } + + async downloadVoteMetadata() { + const download: Download = await this.page.waitForEvent("download"); + return downloadMetadata(download); + } + + async fillupForm(governanceProposal: IGovernanceProposal) { + await this.titleInput.fill(governanceProposal.title); + await this.abstractInput.fill(governanceProposal.abstract); + await this.motivationInput.fill(governanceProposal.motivation); + await this.rationaleInput.fill(governanceProposal.rationale); + + if (governanceProposal.type === "Treasury") { + await this.receivingAddressInput.fill( + governanceProposal.receivingAddress + ); + await this.amountInput.fill(governanceProposal.amount); + } + + if (governanceProposal.extraContentLinks != null) { + for (let i = 0; i < governanceProposal.extraContentLinks.length; i++) { + if (i > 0) { + this.page + .getByRole("button", { + name: "+ Add link", + }) + .click(); + } + await this.linkInput + .nth(i) + .fill(governanceProposal.extraContentLinks[i]); + } + } + } + + async validateForm(governanceProposal: IGovernanceProposal) { + await this.fillupForm(governanceProposal); + + for (const err of formErrors.proposalTitle) { + await expect( + this.page.getByTestId(err), + `Invalid title: ${governanceProposal.title}` + ).toBeHidden(); + } + + expect(await this.abstractInput.textContent()).toEqual( + governanceProposal.abstract + ); + + expect(await this.rationaleInput.textContent()).toEqual( + governanceProposal.rationale + ); + + expect(await this.motivationInput.textContent()).toEqual( + governanceProposal.motivation + ); + + if (governanceProposal.type === "Treasury") { + await expect( + this.page.getByTestId(formErrors.receivingAddress) + ).toBeHidden(); + + for (const err of formErrors.amount) { + await expect(this.page.getByTestId(err)).toBeHidden(); + } + } + + await expect(this.page.getByTestId(formErrors.link)).toBeHidden(); + + await expect(this.continueBtn).toBeEnabled(); + } + + async inValidateForm(governanceProposal: IGovernanceProposal) { + await this.fillupForm(governanceProposal); + + function convertTestIdToText(testId: string) { + let text = testId.replace("-error", ""); + text = text.replace(/-/g, " "); + return text[0].toUpperCase() + text.substring(1); + } + + // Helper function to generate regex pattern from form errors + function generateRegexPattern(errors: string[]) { + return new RegExp(errors.map(convertTestIdToText).join("|")); + } + + // Helper function to get errors based on regex pattern + async function getErrorsByPattern(page: Page, regexPattern: RegExp) { + return await page + .locator('[data-testid$="-error"]') + .filter({ hasText: regexPattern }) + .all(); + } + + const proposalTitlePattern = generateRegexPattern(formErrors.proposalTitle); + const proposalTitleErrors = await getErrorsByPattern( + this.page, + proposalTitlePattern + ); + expect(proposalTitleErrors.length).toEqual(1); + + if (governanceProposal.type === "Treasury") { + const receiverAddressErrors = await getErrorsByPattern( + this.page, + new RegExp(convertTestIdToText(formErrors.receivingAddress)) + ); + expect(receiverAddressErrors.length).toEqual(1); + + const amountPattern = generateRegexPattern(formErrors.amount); + const amountErrors = await getErrorsByPattern(this.page, amountPattern); + expect(amountErrors.length).toEqual(1); + } + + expect(await this.abstractInput.textContent()).not.toEqual( + governanceProposal.abstract + ); + + expect(await this.motivationInput.textContent()).not.toEqual( + governanceProposal.motivation + ); + + expect(await this.rationaleInput.textContent()).not.toEqual( + governanceProposal.rationale + ); + + await expect(this.page.getByTestId(formErrors.link)).toBeVisible(); + + await expect(this.continueBtn).toBeDisabled(); + } +} diff --git a/tests/govtool-frontend/playwright/lib/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index 5a18dc0a..e04e2a40 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -52,6 +52,19 @@ export type IDRepInfo = { extraContentLinks?: string[]; }; +export type ProposalType = "Info" | "Treasury"; + +export type IGovernanceProposal = { + title: string; + abstract: string; + motivation: string; + rationale: string; + extraContentLinks?: string[]; + type: ProposalType; + receivingAddress?: string; + amount?: string; +}; + export enum FilterOption { ProtocolParameterChange = "ParameterChange", InfoAction = "InfoAction", diff --git a/tests/govtool-frontend/playwright/playwright.config.ts b/tests/govtool-frontend/playwright/playwright.config.ts index 8f476bd8..23722f66 100644 --- a/tests/govtool-frontend/playwright/playwright.config.ts +++ b/tests/govtool-frontend/playwright/playwright.config.ts @@ -27,14 +27,7 @@ export default defineConfig({ /*use Allure Playwright's testPlanFilter() to determine the grep parameter*/ grep: testPlanFilter(), /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI - ? [ - ["line"], - [ - "allure-playwright" - ], - ] - : [["line"]], + reporter: process.env.CI ? [["line"], ["allure-playwright"]] : [["line"]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ @@ -68,12 +61,12 @@ export default defineConfig({ testMatch: "**/wallet.bootstrap.ts", dependencies: ["faucet setup"], }, - // { - // name: "transaction", - // use: { ...devices["Desktop Chrome"] }, - // testMatch: "**/*.tx.spec.ts", - // dependencies: process.env.CI ? ["auth setup", "wallet bootstrap"] : [], - // }, + { + name: "transaction", + use: { ...devices["Desktop Chrome"] }, + testMatch: "**/*.tx.spec.ts", + dependencies: process.env.CI ? ["auth setup", "wallet bootstrap"] : [], + }, { name: "loggedin (desktop)", use: { ...devices["Desktop Chrome"] }, @@ -108,6 +101,7 @@ export default defineConfig({ "**/*.delegation.spec.ts", "**/*.loggedin.spec.ts", "**/*.dRep.spec.ts", + "**/*.tx.spec.ts", ], }, { @@ -117,6 +111,7 @@ export default defineConfig({ "**/*.loggedin.spec.ts", "**/*.dRep.spec.ts", "**/*.delegation.spec.ts", + "**/*.tx.spec.ts", ], }, { 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 new file mode 100644 index 00000000..642fb21c --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.spec.ts @@ -0,0 +1,74 @@ +import environments from "@constants/environments"; +import { user01Wallet } from "@constants/staticWallets"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import { + generateInValidProposalFormField, + generateValidProposalFormField, +} from "@helpers/proposalSubmission"; +import ProposalSubmission from "@pages/proposalSubmissionPage"; +import { expect } from "@playwright/test"; +import { ProposalType } from "@types"; +import { bech32 } from "bech32"; + +test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); + +test.beforeEach(async () => { + await setAllureEpic("7. Proposal submission"); +}); + +test.describe("Should accept valid data in Proposal form", () => { + const type: Array = ["Info", "Treasury"]; + const buttons = ["infoRadioButton", "treasuryRadioButton"]; + + for (let j = 0; j < type.length; j++) { + test(`7E.${j + 1} Should accept valid data in ${type[j].toLowerCase()} proposal form`, async ({ + page, + }) => { + test.slow(); + const proposalSubmissionPage = new ProposalSubmission(page); + await proposalSubmissionPage.goto(); + await proposalSubmissionPage[buttons[j]].click(); + await proposalSubmissionPage.continueBtn.click(); + + for (let i = 0; i < 100; i++) { + const prefix = environments.networkId == 0 ? "addr_test" : "addr"; + const randomBytes = new Uint8Array(10); + const bech32Address = bech32.encode(prefix, randomBytes); + await proposalSubmissionPage.validateForm( + generateValidProposalFormField(type[j], bech32Address) + ); + } + + for (let i = 0; i < 7; i++) { + await expect(proposalSubmissionPage.addLinkBtn).toBeVisible(); + await proposalSubmissionPage.addLinkBtn.click(); + } + + await expect(proposalSubmissionPage.addLinkBtn).toBeHidden(); + }); + } +}); + +test.describe("Should reject invalid data in Proposal form", () => { + const type: Array = ["Info", "Treasury"]; + const buttons = ["infoRadioButton", "treasuryRadioButton"]; + + for (let j = 0; j < type.length; j++) { + test(`7F.${j + 1} Should reject invalid data in ${type[j].toLowerCase()} Proposal form`, async ({ + page, + }) => { + test.slow(); + const proposalSubmissionPage = new ProposalSubmission(page); + await proposalSubmissionPage.goto(); + await proposalSubmissionPage[buttons[j]].click(); + await proposalSubmissionPage.continueBtn.click(); + + for (let i = 0; i < 100; i++) { + await proposalSubmissionPage.inValidateForm( + generateInValidProposalFormField(type[j]) + ); + } + }); + } +}); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.spec.ts new file mode 100644 index 00000000..d5c74e90 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.spec.ts @@ -0,0 +1,14 @@ +import { setAllureEpic } from "@helpers/allure"; +import { expect, test } from "@playwright/test"; + +test.beforeEach(async () => { + await setAllureEpic("7. Proposal submission"); +}); + +test("7A. Should open wallet connection popup, when propose a governance action in disconnected state.", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("propose-a-governance-action-button").click(); + await expect(page.getByTestId("connect-your-wallet-modal")).toBeVisible(); +}); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionFunctionality.tx.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionFunctionality.tx.spec.ts new file mode 100644 index 00000000..8752a397 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionFunctionality.tx.spec.ts @@ -0,0 +1,49 @@ +import environments from "@constants/environments"; +import { createTempUserAuth } from "@datafactory/createAuth"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import { ShelleyWallet } from "@helpers/crypto"; +import { + submitInfoProposal, + submitTreasuryProposal, +} from "@helpers/proposalSubmission"; +import { createNewPageWithWallet } from "@helpers/page"; +import { transferAdaForWallet } from "@helpers/transaction"; +import ProposalSubmission from "@pages/proposalSubmissionPage"; +import { expect } from "@playwright/test"; + +test.describe("Should create proper proposal submission request, when registered with data", () => { + let globalProposalSubmissionPage: ProposalSubmission; + let wallet: ShelleyWallet; + + test.beforeEach(async ({ browser, page }, testInfo) => { + await setAllureEpic("7. Proposal submission"); + test.setTimeout(testInfo.timeout + environments.txTimeOut); + wallet = await ShelleyWallet.generate(); + await transferAdaForWallet(wallet, 100000); + const tempUserAuth = await createTempUserAuth(page, wallet); + const governancePage = await createNewPageWithWallet(browser, { + storageState: tempUserAuth, + wallet, + enableStakeSigning: true, + }); + + const proposalSubmissionPage = new ProposalSubmission(governancePage); + await proposalSubmissionPage.goto(); + await expect(proposalSubmissionPage.continueBtn).toBeDisabled(); + + globalProposalSubmissionPage = proposalSubmissionPage; + }); + + test("7G.1: Should create info proposal", async ({}) => { + await submitInfoProposal(globalProposalSubmissionPage, "Info"); + }); + + test("7G.2: Should create treasury proposal", async ({}) => { + await submitTreasuryProposal( + globalProposalSubmissionPage, + "Treasury", + wallet + ); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionVisibility.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionVisibility.spec.ts new file mode 100644 index 00000000..9bd4d004 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionVisibility.spec.ts @@ -0,0 +1,57 @@ +import { user01Wallet } from "@constants/staticWallets"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import ProposalSubmission from "@pages/proposalSubmissionPage"; +import { expect } from "@playwright/test"; +import { ProposalType } from "@types"; + +test.beforeEach(async () => { + await setAllureEpic("7. Proposal submission"); +}); + +test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); + +test("7B. Should access proposal submission page", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("propose-governance-actions-button").click(); + + await expect( + page.getByText("Create a Governance Action", { exact: true }) + ).toBeVisible(); +}); + +test("7C. Should list governance action types", async ({ page }) => { + const proposalSubmissionPage = new ProposalSubmission(page); + await proposalSubmissionPage.goto(); + + await expect(proposalSubmissionPage.infoRadioButton).toBeVisible(); + await expect(proposalSubmissionPage.treasuryRadioButton).toBeVisible(); +}); +test.describe("Verify Proposal form", () => { + const type: Array = ["Info", "Treasury"]; + const buttons = ["infoRadioButton", "treasuryRadioButton"]; + for (let j = 0; j < type.length; j++) { + test(`7D.${j + 1}: Verify ${type[j].toLocaleLowerCase()} proposal form`, async ({ + page, + }) => { + const proposalSubmissionPage = new ProposalSubmission(page); + await proposalSubmissionPage.goto(); + + await proposalSubmissionPage[buttons[j]].click(); + await proposalSubmissionPage.continueBtn.click(); + + await expect(proposalSubmissionPage.titleInput).toBeVisible(); + await expect(proposalSubmissionPage.abstractInput).toBeVisible(); + await expect(proposalSubmissionPage.motivationInput).toBeVisible(); + await expect(proposalSubmissionPage.rationaleInput).toBeVisible(); + await expect(proposalSubmissionPage.addLinkBtn).toBeVisible(); + if (type[j] === "Treasury") { + await expect( + proposalSubmissionPage.receivingAddressInput + ).toBeVisible(); + + await expect(proposalSubmissionPage.amountInput).toBeVisible(); + } + }); + } +}); diff --git a/tests/govtool-frontend/playwright/tests/faucet.setup.ts b/tests/govtool-frontend/playwright/tests/faucet.setup.ts index 947e8241..eca65a56 100644 --- a/tests/govtool-frontend/playwright/tests/faucet.setup.ts +++ b/tests/govtool-frontend/playwright/tests/faucet.setup.ts @@ -15,7 +15,7 @@ setup.beforeEach(async () => { setup("Fund faucet wallet", async () => { const balance = await kuberService.getBalance(faucetWallet.address); - if (balance > 2000) return; + if (balance > 10000) return; const res = await loadAmountFromFaucet(faucetWallet.address); await pollTransaction(res.txid);