diff --git a/tests/govtool-frontend/playwright/lib/_mock/budgetProposal.json b/tests/govtool-frontend/playwright/lib/_mock/budgetProposal.json new file mode 100644 index 000000000..af582a4ce --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/_mock/budgetProposal.json @@ -0,0 +1,171 @@ +{ + "data": { + "id": 49, + "attributes": { + "privacy_policy": true, + "intersect_named_administrator": true, + "prop_comments_number": 2, + "is_active": true, + "createdAt": "2025-03-31T12:08:38.908Z", + "updatedAt": "2025-04-01T08:09:46.924Z", + "intersect_admin_further_text": null, + "creator": { + "data": { + "id": 1300, + "attributes": { + "username": "e0d7ccd5f411be1bb732a3770fd4c79b74bd5fc75ac76bdfe73f2d8893", + "email": "e0d7ccd5f411be1bb732a3770fd4c79b74bd5fc75ac76bdfe73f2d8893@example.com", + "provider": "local", + "confirmed": true, + "blocked": false, + "govtool_username": "intersectadmin", + "createdAt": "2025-03-31T09:43:04.102Z", + "updatedAt": "2025-03-31T09:43:17.516Z" + } + } + }, + "bd_costing": { + "data": { + "id": 49, + "attributes": { + "ada_amount": "0", + "amount_in_preferred_currency": "300000", + "usd_to_ada_conversion_rate": "0", + "cost_breakdown": "25% up front, 75% on delivery.\n\nUp front: $75,000\nOn delivery: $225,000", + "createdAt": "2025-03-31T12:08:38.879Z", + "updatedAt": "2025-03-31T12:08:38.879Z", + "preferred_currency": { + "data": { + "id": 1, + "attributes": { + "currency_name": "United States Dollar", + "currency_letter_code": "USD", + "currency_number_code": "840", + "createdAt": "2025-03-31T08:49:25.530Z", + "updatedAt": "2025-03-31T08:49:25.530Z", + "publishedAt": "2025-03-31T08:49:25.529Z" + } + } + } + } + } + }, + "bd_proposal_detail": { + "data": { + "id": 49, + "attributes": { + "experience": "Wanchain has already deployed cross-chain value bridges to Cardano. Wanchain has also already deployed XPort on other networks.", + "proposal_name": "Deploy XPort, Wanchain's cross-chain data transfer protocol, to Cardano", + "key_dependencies": "No dependencies", + "maintain_and_support": "Wanchain will provide ongoing maintenance.", + "proposal_description": "Blockchain interoperability is essential for the growth and adoption of decentralized technologies. As a leading blockchain network, Cardano has established itself as a secure, scalable, and research-driven ecosystem. However, interoperability with other blockchains remains limited. Wanchain’s XPort protocol offers a decentralized, secure, and efficient cross-chain data transfer solution, making it an ideal candidate to bridge Cardano with external blockchain ecosystems. This proposal is for the deployment of XPort to Cardano, enabling seamless cross-chain data transfers between Cardano and other blockchain networks.\n\nAbout Cross-Chain Data Transfer Protocols\n\nCross-Chain Data Transfer Protocols enable data to be passed from one blockchain to another. Rather than only moving fungible and non-fungible tokens, which are a specific type of data structure, Cross-Chain Data Transfer Protocols can move any type of data. Importantly, Cross-Chain Data Transfer Protocols can feed data into 3rd party smart contracts to seamlessly execute on-chain logic and create novel cross-chain applications.\n\nThe basic flow of all Cross-Chain Data Transfers Protocols is as follows:\n\n- A user or smart contract records arbitrary data on the source chain\n- The off-chain component detects these data\n- The off-chain component records these data on the destination chain\n- A smart contract executes on-chain logic on the destination chain using these data\n\nAbout XPort\n\nTrue to its legacy, XPort is deceptively simple. It essentially just detects data on a source blockchain then exports it to the destination chain in the correct format.\n\nXPort is composed of two basic elements: one robust off-chain relayer and a set of rudimentary on-chain smart contracts called Cross-Chain Gateways.\n\n1. The off-chain relayer is the same Bridge Node Group that secures all cross-chain transactions executed using the Wanchain Bridge. These permissionless Bridge Nodes are rotated and re-elected monthly. They use Multiparty Computation and Shamir’s Secret Sharing cryptography to transfer messages and arbitrary data across chains.\n\n2. A smart contract, called a Cross-Chain Gateway, is deployed on each supported blockchain. These Cross-Chain Gateways have limited functionality — they can essentially only send and receive data. They serve as the point of contact for all 3rd party developers.\n\nImportantly, XPort is designed to be compliant with the Enterprise Ethereum Alliance’s Distributed Ledger Technology Interoperability Specification, co-authored by Wanchain’s VP on Engineering Dr. Weijia Zhang.\n\nMore technical information about XPort: https://docs.wanchain.org/latest-major-updates/xport-wanchains-cross-chain-data-transfer-protocol-development-handbook", + "key_proposal_deliverables": "- XPort interface definition adjustment to support Cardano\n\n- XPort deployed on Cardano Pre-Production\n\n- XPort deployed on Cardano Mainnet: developers will be able to build applications that seamlessly span multiple networks using XPort", + "resourcing_duration_estimates": "Budget: 400,000 USD\nTeam size: ~30\nDuration: 9 months", + "other_contract_type": "", + "createdAt": "2025-03-31T12:08:38.864Z", + "updatedAt": "2025-03-31T12:08:38.864Z", + "contract_type_name": { + "data": { + "id": 3, + "attributes": { + "contract_type_name": "Service Level Agreement", + "createdAt": "2025-03-31T08:49:37.711Z", + "updatedAt": "2025-03-31T08:49:37.711Z", + "publishedAt": "2025-03-31T08:49:37.710Z" + } + } + } + } + } + }, + "bd_further_information": { + "data": { + "id": 49, + "attributes": { + "createdAt": "2025-03-31T12:08:38.891Z", + "updatedAt": "2025-03-31T12:08:38.891Z", + "proposal_links": [] + } + } + }, + "bd_psapb": { + "data": { + "id": 49, + "attributes": { + "problem_statement": "Seamless cross-chain communication is a challenge for permissionless blockchains due to their inherent lack of interoperability. This limitation is rooted in their trustless nature, as blockchains need a mechanism to verify the authenticity of data before processing it. When dealing with heterogeneous blockchain networks, each with their own distinct ruleset and security guarantees, cross-chain communication is currently impossible without the intervention of an off-chain component.\n\nIn many ways, this problem is just the oracle problem. The oracle problem, for those who are unfamiliar, refers to a blockchain’s inability to access external data, rendering it isolated. An additional piece of infrastructure — whether you want to call it a bridge, an oracle or a relayer — is needed to connect the blockchain and the off-chain data. With cross-chain communication, the problem is the same. The data that needs to be accessed just happens to be on another blockchain!\n\nCross-Chain Data Transfer Protocols enable data to be passed from one blockchain to another. Rather than only moving fungible and non-fungible tokens, which are a specific type of data structure, Cross-Chain Data Transfer Protocols can move any type of data. Importantly, Cross-Chain Data Transfer Protocols can feed data into 3rd party smart contracts to seamlessly execute on-chain logic and create novel cross-chain applications.\n\nThis proposal is to deploy XPort, Wanchain's cross-chain data transfer protocol, to Cardano.", + "proposal_benefit": "Once deployed, XPort will allow arbitrary data to flow between Cardano, any EVM, and select non-EVM networks. This will enable developers to develop applications that span multiple blockchains. It will also empower developers to shift their focus from extracting value out of a blockchain to importing execution logic into one. \n\nOther potential benefits include but are not limited to:\n\n- Improved interoperability between Cardano and the of the industry\n- Expanded access to liquidity and assets on other chains\n- Greater abstraction to improve UX\n- Reduced bridge risk\n- Improved developer experience\n- More modular application design/New types of applications", + "supplementary_endorsement": "Wanchain is the longest running cross-chain bridge in the blockchain industry and is the primary cross-chain value bridge currently servicing the Cardano mainnet. It has received good support from the Cardano community (multiple approved Catalyst proposals) and Cardano Dapps (like Liqwid).", + "explain_proposal_roadmap": "", + "createdAt": "2025-03-31T12:08:38.846Z", + "updatedAt": "2025-03-31T12:08:38.846Z", + "committee_name": { + "data": { + "id": 2, + "attributes": { + "committee_name": "Product Committee", + "createdAt": "2025-03-31T08:50:27.431Z", + "updatedAt": "2025-03-31T08:50:27.431Z", + "publishedAt": "2025-03-31T08:50:27.429Z" + } + } + }, + "roadmap_name": { + "data": { + "id": 2, + "attributes": { + "roadmap_name": "Architectural Excellence", + "createdAt": "2025-03-31T08:49:40.879Z", + "updatedAt": "2025-03-31T08:49:40.879Z", + "publishedAt": "2025-03-31T08:49:40.877Z" + } + } + }, + "type_name": { + "data": { + "id": 6, + "attributes": { + "type_name": "Core", + "createdAt": "2025-03-31T10:40:44.559Z", + "updatedAt": "2025-03-31T10:40:45.976Z", + "publishedAt": "2025-03-31T10:40:45.971Z" + } + } + } + } + } + }, + "bd_proposal_ownership": { + "data": { + "id": 50, + "attributes": { + "agreed": true, + "group_name": "", + "company_name": "Wanchain", + "type_of_group": "", + "social_handles": "https://x.com/wanchain_org, https://x.com/TemujinLouie", + "submited_on_behalf": "Company", + "company_domain_name": "wanchain.org", + "proposal_public_champion": "Submission lead listed above", + "key_info_to_identify_group": "", + "createdAt": "2025-03-31T12:08:38.826Z", + "updatedAt": "2025-03-31T12:08:38.826Z", + "be_country": { + "data": { + "id": 32, + "attributes": { + "country_name": "British Virgin Islands", + "alfa_2_code": "VG", + "alfa_3_code": "VGB", + "createdAt": "2025-03-31T08:49:04.220Z", + "updatedAt": "2025-03-31T08:49:04.220Z", + "publishedAt": "2025-03-31T08:49:04.219Z" + } + } + } + } + } + } + } + }, + "meta": {} +} diff --git a/tests/govtool-frontend/playwright/lib/_mock/budgetProposalComments.json b/tests/govtool-frontend/playwright/lib/_mock/budgetProposalComments.json new file mode 100644 index 000000000..0668996f9 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/_mock/budgetProposalComments.json @@ -0,0 +1,66 @@ +{ + "data": [ + { + "id": 427, + "attributes": { + "proposal_id": null, + "comment_parent_id": null, + "user_id": "1187", + "comment_text": "test comment 2", + "createdAt": "2025-04-03T06:04:32.388Z", + "updatedAt": "2025-04-03T06:04:32.388Z", + "bd_proposal_id": "49", + "drep_id": "34b1eb01917db5d3f39757c94e85004c5e3d41462fd1d82da01264a9", + "comments_reports": { + "data": [] + }, + "user_govtool_username": "testeternl1", + "subcommens_number": 0 + } + }, + { + "id": 426, + "attributes": { + "proposal_id": null, + "comment_parent_id": null, + "user_id": "1187", + "comment_text": "test comment 1", + "createdAt": "2025-04-03T06:04:23.380Z", + "updatedAt": "2025-04-03T06:04:23.380Z", + "bd_proposal_id": "49", + "drep_id": "34b1eb01917db5d3f39757c94e85004c5e3d41462fd1d82da01264a9", + "comments_reports": { + "data": [] + }, + "user_govtool_username": "testeternl1", + "subcommens_number": 0 + } + }, + { + "id": 384, + "attributes": { + "proposal_id": null, + "comment_parent_id": null, + "user_id": "38", + "comment_text": "test comment", + "createdAt": "2025-04-01T08:09:39.135Z", + "updatedAt": "2025-04-01T08:09:39.135Z", + "bd_proposal_id": "49", + "drep_id": null, + "comments_reports": { + "data": [] + }, + "user_govtool_username": "testlace", + "subcommens_number": 1 + } + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 25, + "pageCount": 1, + "total": 3 + } + } +} diff --git a/tests/govtool-frontend/playwright/lib/_mock/budgetProposalPoll.json b/tests/govtool-frontend/playwright/lib/_mock/budgetProposalPoll.json new file mode 100644 index 000000000..82cd4f535 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/_mock/budgetProposalPoll.json @@ -0,0 +1,23 @@ +{ + "data": [ + { + "id": 48, + "attributes": { + "bd_proposal_id": "49", + "poll_yes": 0, + "poll_no": 0, + "is_poll_active": true, + "createdAt": "2025-03-31T12:08:38.928Z", + "updatedAt": "2025-03-31T12:08:38.928Z" + } + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 1, + "pageCount": 1, + "total": 1 + } + } +} diff --git a/tests/govtool-frontend/playwright/lib/_mock/index.ts b/tests/govtool-frontend/playwright/lib/_mock/index.ts index 79214f128..692d74859 100644 --- a/tests/govtool-frontend/playwright/lib/_mock/index.ts +++ b/tests/govtool-frontend/playwright/lib/_mock/index.ts @@ -149,7 +149,11 @@ export const valid = { return `ipfs://${randomCID}`; }, - metadata: (paymentAddress: string, imageObject: imageObject) => ({ + metadata: ( + paymentAddress: string, + imageObject: imageObject, + givenName: string + ) => ({ "@context": { "@language": "en-us", CIP100: @@ -212,7 +216,7 @@ export const valid = { authors: [], hashAlgorithm: "blake2b-256", body: { - givenName: faker.person.firstName(), + givenName: givenName, image: imageObject, motivations: faker.lorem.paragraph(2), objectives: faker.lorem.paragraph(2), diff --git a/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts b/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts index b1b1edd5d..17f486606 100644 --- a/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts +++ b/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts @@ -5,27 +5,33 @@ export const faucetWallet = staticWallets[0]; export const dRep01Wallet = staticWallets[1]; export const dRep02Wallet = staticWallets[2]; +export const dRep03Wallet = staticWallets[3]; -export const adaHolder01Wallet = staticWallets[3]; -export const adaHolder02Wallet = staticWallets[4]; +export const adaHolder01Wallet = staticWallets[4]; +export const adaHolder02Wallet = staticWallets[5]; export const adaHolder03Wallet = staticWallets[6]; export const adaHolder04Wallet = staticWallets[7]; export const adaHolder05Wallet = staticWallets[8]; export const adaHolder06Wallet = staticWallets[9]; // Does not takes part in transaction -export const user01Wallet: StaticWallet = staticWallets[5]; +export const user01Wallet: StaticWallet = staticWallets[10]; // Username is already set -export const proposal01Wallet: StaticWallet = staticWallets[10]; -export const proposal02Wallet: StaticWallet = staticWallets[11]; -export const proposal03Wallet: StaticWallet = staticWallets[12]; -export const proposal04Wallet: StaticWallet = staticWallets[13]; -export const proposal05Wallet: StaticWallet = staticWallets[14]; -export const proposal06Wallet: StaticWallet = staticWallets[15]; -export const proposal07Wallet: StaticWallet = staticWallets[16]; -export const proposal08Wallet: StaticWallet = staticWallets[17]; -export const proposal09Wallet: StaticWallet = staticWallets[18]; +export const proposal01Wallet: StaticWallet = staticWallets[11]; +export const proposal02Wallet: StaticWallet = staticWallets[12]; +export const proposal03Wallet: StaticWallet = staticWallets[13]; +export const proposal04Wallet: StaticWallet = staticWallets[14]; +export const proposal05Wallet: StaticWallet = staticWallets[15]; +export const proposal06Wallet: StaticWallet = staticWallets[16]; +export const proposal07Wallet: StaticWallet = staticWallets[17]; +export const proposal08Wallet: StaticWallet = staticWallets[18]; +export const proposal09Wallet: StaticWallet = staticWallets[19]; + +export const budgetProposal01Wallet: StaticWallet = staticWallets[20]; +export const budgetProposal02Wallet: StaticWallet = staticWallets[21]; +export const budgetProposal03Wallet: StaticWallet = staticWallets[22]; +export const budgetProposal04Wallet: StaticWallet = staticWallets[23]; export const adaHolderWallets = [ adaHolder01Wallet, @@ -38,7 +44,7 @@ export const adaHolderWallets = [ export const userWallets = [user01Wallet]; -export const dRepWallets = [dRep01Wallet, dRep02Wallet]; +export const dRepWallets = [dRep01Wallet, dRep02Wallet, dRep03Wallet]; export const proposalWallets = [ proposal01Wallet, diff --git a/tests/govtool-frontend/playwright/lib/fixtures/budgetProposal.ts b/tests/govtool-frontend/playwright/lib/fixtures/budgetProposal.ts new file mode 100644 index 000000000..f620d523a --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/fixtures/budgetProposal.ts @@ -0,0 +1,36 @@ +import { budgetProposal01Wallet } from "@constants/staticWallets"; +import { test as base } from "@fixtures/walletExtension"; +import { createNewPageWithWallet } from "@helpers/page"; +import BudgetDiscussionDetailsPage from "@pages/budgetDiscussionDetailsPage"; +import BudgetDiscussionSubmissionPage from "@pages/budgetDiscussionSubmissionPage"; + +type TestOptions = { + proposalId: number; +}; + +export const test = base.extend({ + proposalId: async ({ browser }, use) => { + // setup + const budgetProposalPage = await createNewPageWithWallet(browser, { + storageState: ".auth/budgetProposal01.json", + wallet: budgetProposal01Wallet, + }); + + const budgetProposalCreationPage = new BudgetDiscussionSubmissionPage( + budgetProposalPage + ); + await budgetProposalCreationPage.goto(); + + const { proposalId } = + await budgetProposalCreationPage.createBudgetProposal(); + + const budgetProposalDetailsPage = new BudgetDiscussionDetailsPage( + budgetProposalPage + ); + + await use(proposalId); + + // cleanup + await budgetProposalDetailsPage.deleteProposal(); + }, +}); diff --git a/tests/govtool-frontend/playwright/lib/helpers/metadata.ts b/tests/govtool-frontend/playwright/lib/helpers/metadata.ts index 267d9e177..98e908456 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/metadata.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/metadata.ts @@ -39,8 +39,9 @@ async function calculateMetadataHash() { contentUrl: imageUrl, sha256: imageSHA256, }; + const givenName = faker.person.firstName(); const data = JSON.stringify( - mockValid.metadata(paymentAddress, imageObject), + mockValid.metadata(paymentAddress, imageObject, givenName), null, 2 ); @@ -48,19 +49,20 @@ async function calculateMetadataHash() { const hexDigest = calculateHash(data); const jsonData = JSON.parse(data); - return { hexDigest, jsonData }; + return { hexDigest, jsonData, givenName }; } catch (error) { console.error("Error reading file:", error); } } export async function uploadMetadataAndGetJsonHash() { - const { hexDigest: dataHash, jsonData } = await calculateMetadataHash(); - const url = await metadataBucketService.uploadMetadata( - faker.person.firstName(), - jsonData - ); - return { dataHash, url }; + const { + hexDigest: dataHash, + jsonData, + givenName, + } = await calculateMetadataHash(); + const url = await metadataBucketService.uploadMetadata(givenName, jsonData); + return { dataHash, url, givenName }; } export async function uploadScriptAndGenerateUrl(payload: Object) { diff --git a/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionDetailsPage.ts b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionDetailsPage.ts new file mode 100644 index 000000000..b86872428 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionDetailsPage.ts @@ -0,0 +1,226 @@ +import { expect, Page } from "@playwright/test"; +import { BudgetProposalProps, CommentResponse } from "@types"; +import environments from "lib/constants/environments"; + +export default class BudgetDiscussionDetailsPage { + // buttons + readonly shareBtn = this.page.getByTestId("share-button"); + readonly copyLinkBtn = this.page.getByTestId("copy-link"); + readonly commentBtn = this.page.getByTestId("comment-button"); + readonly replyBtn = this.page.getByTestId("reply-button"); + readonly replyCommentBtn = this.page.getByTestId("reply-comment-button"); + readonly pollYesBtn = this.page.getByTestId("poll-yes-button"); + readonly pollNoBtn = this.page.getByTestId("poll-no-button"); + readonly sortCommentsBtn = this.page.getByTestId("sort-comments"); + readonly changeVoteBtn = this.page.getByTestId("change-vote-button"); + readonly changeVoteYesBtn = this.page.getByTestId( + "change-poll-vote-yes-button" + ); + readonly verifyIdentityBtn = this.page.getByTestId("verify-identity-button"); + readonly readMoreBtn = this.page.getByTestId("read-more-button"); + + // content + readonly copyLinkText = this.page.getByTestId("copy-link-text"); + readonly pollVoteCard = this.page.getByTestId("poll-vote-card"); + readonly totalComments = this.page.getByTestId("total-comments"); + readonly linkTextContent = this.page.getByTestId("link-0-text-content"); + readonly linkUrlContent = this.page.getByTestId("link-0-url-content"); + readonly budgetDiscussionTypeContent = this.page + .getByTestId("budget-discussion-type") + .first(); + readonly publicProposalChampionContent = this.page.getByTestId( + "public-proposal-champion" + ); + readonly socialHandlesContent = this.page.getByTestId("social-handles"); + readonly problemStatementContent = this.page.getByTestId("problem-statement"); + readonly proposalBenefitsContent = this.page.getByTestId("problem-benefit"); + readonly productRoadMapContent = this.page.getByTestId("product-roadmap"); + readonly alignProposalComittesContent = this.page.getByTestId( + "align-proposal-committees" + ); + readonly evidenceContent = this.page.getByTestId("evidence"); + readonly proposalNameContent = this.page.getByTestId("proposal-name"); + readonly proposalDescriptionContent = this.page.getByTestId( + "proposal-description" + ); + readonly proposalKeyDependenciesContent = this.page.getByTestId( + "proposal-key-dependencies" + ); + readonly milestonesContent = this.page.getByTestId("proposal-milestone"); + readonly proposalResourcesAndEstimates = this.page.getByTestId( + "proposal-resources-&-duration-estimates" + ); + readonly projectExperienceContent = + this.page.getByTestId("project-experience"); + readonly proposalContractingContent = this.page.getByTestId( + "proposal-contracting" + ); + readonly costingAmountContent = this.page.getByTestId("consting-amount"); // BUG typo + readonly costingConversionRateContent = this.page.getByTestId( + "costing-conversion-rate" + ); + readonly constingPreferedCurrencyContent = this.page.getByTestId( + "costing-preferred-currency" + ); + readonly costingPreferedCurrencyAmountContent = this.page.getByTestId( + "costing-prefereed-currency-amount" + ); + readonly costBreakdownContent = this.page.getByTestId("cost-breakdown"); + readonly includeAsAuditorContent = + this.page.getByTestId("include-as-auditor"); + + // Input + readonly commentInput = this.page.getByTestId("comment-input"); + readonly replyInput = this.page.getByTestId("reply-input"); + + constructor(private readonly page: Page) {} + + get currentPage(): Page { + return this.page; + } + + async goto(proposalId: number) { + await this.page.goto( + `${environments.frontendUrl}/budget_discussion/${proposalId}` + ); + } + + async sortAndValidate( + order: string, + validationFn: (date1: string, date2: string) => boolean + ) { + const responsePromise = this.page.waitForResponse((response) => + response.url().includes(`&sort[createdAt]=${order}`) + ); + + await this.sortCommentsBtn.click(); + const response = await responsePromise; + + const comments: CommentResponse[] = (await response.json()).data; + + // API validation + for (let i = 0; i < comments.length - 1; i++) { + const isValid = validationFn( + comments[i].attributes.updatedAt, + comments[i + 1].attributes.updatedAt + ); + expect(isValid).toBe(true); + } + } + + async addComment(comment: string) { + await this.commentInput.fill(comment); + await this.commentBtn.click(); + } + + async replyComment(reply: string) { + await this.page + .locator('[data-testid^="comment-"][data-testid$="-content-card"]') + .first() + .getByTestId("reply-button") + .click(); + await this.replyInput.fill(reply); + await this.replyCommentBtn.click(); + } + + async voteOnPoll(vote: string) { + await this.page.getByTestId(`poll-${vote.toLowerCase()}-button`).click(); + } + + async changePollVote() { + await this.changeVoteBtn.click(); + await this.changeVoteYesBtn.click(); + } + + async deleteProposal() { + await this.page.waitForTimeout(2_000); + + await this.page.getByTestId("menu-button").click(); + await this.page.getByTestId("delete-proposal").click(); + await this.page.getByTestId("delete-proposal-yes-button").click(); + } + + async validateProposalDetails(budgetProposal: BudgetProposalProps) { + await this.readMoreBtn.click(); + + // proposal ownership validation + await expect(this.publicProposalChampionContent).toHaveText( + budgetProposal.proposalOwnership.publicChampion + ); + await expect(this.socialHandlesContent).toHaveText( + budgetProposal.proposalOwnership.contactDetails + ); + + // problem statement and benefits validation + await expect(this.problemStatementContent).toHaveText( + budgetProposal.problemStatementAndBenefits.problemStatement + ); + await expect(this.proposalBenefitsContent).toHaveText( + budgetProposal.problemStatementAndBenefits.proposalBenefits + ); + await expect(this.productRoadMapContent).toHaveText( + budgetProposal.problemStatementAndBenefits.roadmapName + ); + await expect(this.budgetDiscussionTypeContent).toHaveText( + budgetProposal.problemStatementAndBenefits.budgetDiscussionType + ); + await expect(this.alignProposalComittesContent).toHaveText( + budgetProposal.problemStatementAndBenefits.committeeAlignmentType + ); + await expect(this.evidenceContent).toHaveText( + budgetProposal.problemStatementAndBenefits.suplimentaryEndorsement + ); + + // proposal details validation + await expect(this.proposalNameContent).toHaveText( + budgetProposal.proposalDetails.proposalName + ); + await expect(this.proposalDescriptionContent).toHaveText( + budgetProposal.proposalDetails.proposalDescription + ); + await expect(this.proposalKeyDependenciesContent).toHaveText( + budgetProposal.proposalDetails.proposalKeyDependencies + ); + await expect(this.milestonesContent).toHaveText( + budgetProposal.proposalDetails.milestones + ); + await expect(this.proposalResourcesAndEstimates).toHaveText( + budgetProposal.proposalDetails.teamSizeAndDuration + ); + await expect(this.projectExperienceContent).toHaveText( + budgetProposal.proposalDetails.previousExperience + ); + await expect(this.proposalContractingContent).toHaveText( + budgetProposal.proposalDetails.contracting + ); + + // costing validation + await expect(this.costingAmountContent).toHaveText( + budgetProposal.costing.adaAmount.toString() + ); + await expect(this.costingConversionRateContent).toHaveText( + budgetProposal.costing.usaToAdaCnversionRate.toString() + ); + await expect(this.constingPreferedCurrencyContent).toHaveText( + budgetProposal.costing.preferredCurrency + ); + await expect(this.costingPreferedCurrencyAmountContent).toHaveText( + budgetProposal.costing.AmountInPreferredCurrency.toString() + ); + await expect(this.costBreakdownContent).toHaveText( + budgetProposal.costing.costBreakdown + ); + + // further information validation + await expect(this.linkTextContent).toHaveText( + budgetProposal.furtherInformation[0].prop_link_text + ); + + // administration and auditing validation + await expect(this.includeAsAuditorContent).toHaveText( + budgetProposal.administrationAndAuditing.intersectAdministration + ? "Yes" + : "No" + ); + } +} diff --git a/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionPage.ts b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionPage.ts new file mode 100644 index 000000000..0d6264e8a --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionPage.ts @@ -0,0 +1,167 @@ +import { functionWaitedAssert, waitedLoop } from "@helpers/waitedLoop"; +import { expect, Locator, Page } from "@playwright/test"; +import { BudgetDiscussionEnum, ProposedGovAction } from "@types"; +import environments from "lib/constants/environments"; +import BudgetDiscussionDetailsPage from "./budgetDiscussionDetailsPage"; + +export default class BudgetDiscussionPage { + // Buttons + readonly drawerBtn = this.page.getByTestId("open-drawer-button"); + readonly proposalBudgetDiscussionBtn = this.page.getByTestId( + "propose-a-budget-discussion-button" + ); + readonly verifyIdentityBtn = this.page.getByTestId("verify-identity-button"); + readonly filterBtn = this.page.getByTestId("filter-button"); + readonly sortBtn = this.page.getByTestId("sort-button"); + + // input + readonly searchInput = this.page.getByTestId("search-input"); + + constructor(private readonly page: Page) {} + + get currentPage(): Page { + return this.page; + } + + async goto() { + await this.page.goto(`${environments.frontendUrl}/budget_discussion`); + // wait for the proposal cards to load + await this.page.waitForTimeout(2_000); + } + + async viewFirstProposal(): Promise { + await this.page + .locator( + '[data-testid^="budget-discussion-"][data-testid$="-view-details"]' + ) + .first() + .click(); + return new BudgetDiscussionDetailsPage(this.page); + } + + async getAllProposals() { + const proposalCardSelector = + '[data-testid^="budget-discussion-"][data-testid$="-card"]'; + + await waitedLoop(async () => { + const count = await this.page.locator(proposalCardSelector).count(); + return count > 0; + }); + const proposalCards = await this.page.locator(proposalCardSelector).all(); + + expect(true, "No budget proposals found.").toBeTruthy(); + + return proposalCards; + } + + async clickRadioButtonsByNames(names: string[]) { + for (const name of names) { + const budgetProposalValue = Object.values(BudgetDiscussionEnum).includes( + name as BudgetDiscussionEnum + ); + if (budgetProposalValue) { + await this.page.getByLabel(name).click(); + } + } + } + + async filterProposalByNames(names: string[]) { + await this.clickRadioButtonsByNames(names); + } + + async unFilterProposalByNames(names: string[]) { + await this.clickRadioButtonsByNames(names); + } + + async applyAndValidateFilters( + filters: string[], + validateFunction: (proposalCard: any, filters: string[]) => Promise + ) { + await this.page.waitForTimeout(4_000); // wait for the proposals to load + // single filter + for (const filter of filters) { + await this.filterProposalByNames([filter]); + await this.validateFilters([filter], validateFunction); + await this.unFilterProposalByNames([filter]); + } + + // multiple filter + const multipleFilters = [...filters]; + while (multipleFilters.length > 1) { + await this.filterProposalByNames(multipleFilters); + await this.validateFilters(multipleFilters, validateFunction); + await this.unFilterProposalByNames(multipleFilters); + multipleFilters.pop(); + } + } + + async validateFilters( + filters: string[], + validateFunction: (proposalCard: any, filters: string[]) => Promise + ) { + await functionWaitedAssert(async () => { + const proposalCards = await this.getAllProposals(); + + for (const proposalCard of proposalCards) { + if (await proposalCard.isVisible()) { + const type = await proposalCard + .getByTestId("budget-discussion-type") + .textContent(); + const hasFilter = await validateFunction(proposalCard, filters); + + expect( + hasFilter, + !hasFilter && + `A budget proposal type ${type} does not contain on ${filters}` + ).toBe(true); + } + } + }); + } + + async _validateTypeFiltersInProposalCard( + proposalCard: Locator, + filters: string[] + ): Promise { + const govActionType = await proposalCard + .getByTestId("budget-discussion-type") + .textContent(); + + if (govActionType === "None of these") { + return filters.includes(BudgetDiscussionEnum.NoCategory); + } + return filters.includes(govActionType); + } + + async sortAndValidate( + option: "asc" | "desc", + validationFn: (p1: ProposedGovAction, p2: ProposedGovAction) => boolean + ) { + const responsePromise = this.page.waitForResponse((response) => + response + .url() + .includes(`&sort[createdAt]=${option}&populate[0]=bd_costing`) + ); + + await this.sortBtn.click(); + const response = await responsePromise; + + let proposals: ProposedGovAction[] = (await response.json()).data; + + // API validation + for (let i = 0; i <= proposals.length - 2; i++) { + const isValid = validationFn(proposals[i], proposals[i + 1]); + expect(isValid).toBe(true); + } + } + + async setUsername(name: string) { + await this.page.getByTestId("username-input").fill(name); + + const proceedBtn = this.page.getByTestId("proceed-button"); + await proceedBtn.click(); + await proceedBtn.click(); + + await this.page.getByTestId("close-button").click(); + } +} diff --git a/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionSubmissionPage.ts b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionSubmissionPage.ts new file mode 100644 index 000000000..968426c2f --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionSubmissionPage.ts @@ -0,0 +1,590 @@ +import environments from "@constants/environments"; +import { fa, faker } from "@faker-js/faker"; +import { extractProposalIdFromUrl } from "@helpers/string"; +import { Page, expect } from "@playwright/test"; +import { + AdministrationAndAuditingProps, + BudgetCostingProps, + BudgetDiscussionEnum, + BudgetProposalContactInformationProps, + BudgetProposalDetailsProps, + BudgetProposalOwnershipProps, + BudgetProposalProblemStatementAndBenefitProps, + BudgetProposalProps, + CommitteeAlignmentEnum, + CompanyEnum, + LocationEnum, + PreferredCurrencyEnum, + ProposalChampionEnum, + ProposalContractingEnum, + ProposalLink, + RoadmapNameEnum, +} from "@types"; + +const formErrors = { + proposalTitle: "title-input-error", + abstract: "abstract-helper-error", + motivation: "motivation-helper-error", + rationale: "rationale-helper-error", + receivingAddress: "receiving-address-0-text-error", + amount: "amount-0-text-error", + constitutionalUrl: "prop-constitution-url-text-error", + guardrailsScriptUrl: "prop-guardrails-script-url-input-error", + link: "link-0-url-input-error", +}; + +export default class BudgetDiscussionSubmissionPage { + // buttons + readonly skipBtn = this.page.getByTestId("skip-button"); + readonly createBudgetProposalBtn = this.page.getByTestId( + "propose-a-budget-discussion-button" + ); + readonly closeDraftBtn = this.page.getByTestId("close-button"); + readonly cancelBtn = this.page.getByTestId("cancel-button"); + + readonly continueBtn = this.page.getByTestId("continue-button"); + readonly addLinkBtn = this.page.getByTestId("add-link-button"); + readonly verifyIdentityBtn = this.page.getByTestId("verify-identity-button"); + readonly saveDraftBtn = this.page.getByTestId("draft-button"); + readonly submitBtn = this.page.getByTestId("submit-button"); + readonly countryOfIncorporationBtn = this.page.getByTestId( + "country-of-incorporation" + ); + + readonly agreeCheckbox = this.page.getByLabel( + "I agree to the information in" + ); //BUG missing test Ids + readonly submitCheckbox = this.page.getByLabel("I consent to the public"); //BUG missing test Ids + + // input + readonly linkTextInput = this.page.getByTestId("link-0-text-input"); + readonly linkUrlInput = this.page.getByTestId("link-0-url-input"); + + // contact-information + readonly beneficiaryFullNameInput = this.page.getByLabel( + "Beneficiary Full Name *" + ); //BUG missing test Ids + readonly beneficiaryEmailInput = this.page.getByLabel("Beneficiary e-mail *"); //BUG missing test Ids + readonly submissionLeadFullNameInput = this.page.getByLabel( + "Submission Lead Full Name *" + ); //BUG missing test Ids + readonly submissionLeadEmailInput = this.page.getByLabel( + "Submission Lead Email *" + ); //BUG missing test Ids + + // proposal-ownership + readonly companyNameInput = this.page.getByLabel("Company Name *"); //BUG missing test Ids + readonly companyDomainNameInput = this.page.getByLabel( + "Company Domain Name *" + ); //BUG missing test Ids + readonly groupNameInput = this.page.getByLabel("Group Name *"); //BUG missing test Ids + readonly groupTypeInput = this.page.getByLabel("Type of Group *"); //BUG missing test Ids + readonly keyInformationOfGroupInput = this.page.getByLabel( + "Key Information to Identify" + ); //BUG missing test Ids + readonly contactDetailsInput = this.page.getByLabel( + "Please provide your preferred" + ); //BUG missing test Ids + + // problem-statements + readonly problemStatementInput = this.page.getByTestId( + "problem-statement-input" + ); + readonly proposalBenefitInput = this.page.getByTestId( + "proposal-benefit-input" + ); + readonly suplimentaryEndorsementInput = this.page.getByTestId( + "supplementary-endorsement-input" + ); + readonly productRoadmapDescriptionInput = this.page.getByLabel( + "Please explain how your" + ); // BUG missing test Ids + + // proposal-details + readonly proposalNameInput = this.page.getByLabel( + "What is your proposed name to" + ); //BUG missing testId + readonly proposalDescriptionInput = this.page.getByTestId( + "proposal-description-input" + ); + readonly proposalKeyDependenciesInput = this.page.getByTestId( + "key-dependencies-input" + ); + readonly proposalMaintainAndSupportInput = this.page.getByLabel( + "How will this proposal be" + ); //BUG missing testId + readonly milestonesInput = this.page.getByTestId( + "key-proposal-deliverables-input" + ); + readonly teamSizeAndDurationInput = this.page.getByTestId( + "resourcing-duration-estimates-input" + ); + readonly previousExperienceInput = this.page.getByLabel( + "Please provide previous" + ); //BUG missing testId + readonly otherDescriptionInput = this.page.getByLabel( + "Please describe what you have" + ); + + // costing + readonly adaAmountInput = this.page.getByLabel("ADA Amount *"); //BUG missing test Ids + readonly usaToAdaCnversionRateInput = this.page.getByLabel( + "USD to ADA Conversion Rate *" + ); //BUG missing test Ids + readonly preferredCurrencyAmountInput = this.page.getByLabel( + "Amount in preferred currency *" + ); + readonly costBreakdownInput = this.page.getByTestId("cost-breakdown-input"); + readonly venderDetailsInput = this.page.getByLabel("Please provide further"); //BUG missing test Ids + + // select + readonly beneficiaryCountrySelect = this.page.getByTestId( + "beneficiary-country-of-residence" + ); + readonly beneficiaryNationalitySelect = this.page.getByTestId( + "beneficiary-nationality" + ); + + readonly companyTypeSelect = this.page.getByTestId("beneficiary-type"); + readonly publicChampionSelect = this.page.getByTestId( + "proposal-public-champion" + ); + + readonly roadmapNameSelect = this.page.getByTestId("roadmap-name"); + readonly budgetDiscussionTypeSelect = this.page.getByTestId( + "budget-discussion-type-name" + ); + readonly committeeAlignmentTypeSelect = this.page.getByTestId( + "committee-alignment-type" + ); + + readonly contractingTypeSelect = this.page.getByTestId("contract-type-name"); + readonly preferredCurrencySelect = + this.page.getByTestId("preferred-currency"); + + readonly intersectNamedAdministratorSelect = this.page.getByTestId( + "itersect-named-administrator" + ); + + // content + readonly linkTextContent = this.page.getByTestId("link-0-text-content"); + readonly linkUrlContent = this.page.getByTestId("link-0-url-content"); + + constructor(private readonly page: Page) {} + + get currentPage(): Page { + return this.page; + } + + async goto() { + await this.page.goto(`${environments.frontendUrl}/budget_discussion`); + + await this.verifyIdentityBtn.click(); + await this.createBudgetProposalBtn.click(); + + await this.continueBtn.click(); + } + + async fillupContactInformationForm( + contactInformation: BudgetProposalContactInformationProps + ) { + await this.beneficiaryFullNameInput.fill( + contactInformation.beneficiaryFullName + ); + await this.beneficiaryEmailInput.fill(contactInformation.beneficiaryEmail); + await this.beneficiaryCountrySelect.click(); + await this.page + .getByTestId( + `${contactInformation.beneficiaryCountry.toLowerCase()}-button` + ) + .click(); + await this.beneficiaryNationalitySelect.click(); + await this.page + .getByTestId( + `${contactInformation.beneficiaryNationality.toLowerCase()}-button` + ) + .click(); + + await this.submissionLeadFullNameInput.fill( + contactInformation.submissionLeadFullName + ); + await this.submissionLeadEmailInput.fill( + contactInformation.submissionLeadEmail + ); + + await this.continueBtn.click(); + } + + async fillupProposalOwnershipForm( + proposalOwnership: BudgetProposalOwnershipProps + ) { + await this.companyTypeSelect.click(); + await this.page + .getByRole("option", { name: proposalOwnership.companyType }) + .click(); //BUG missing testId + + await this.publicChampionSelect.click(); + await this.page + .getByRole("option", { name: proposalOwnership.publicChampion }) + .click(); //BUG missing testId + + await this.contactDetailsInput.fill(proposalOwnership.contactDetails); + + if (proposalOwnership.companyType === "Group") { + await this.groupNameInput.fill(proposalOwnership.groupName); + await this.groupTypeInput.fill(proposalOwnership.groupType); + await this.keyInformationOfGroupInput.fill( + proposalOwnership.groupKeyIdentity + ); + } + if (proposalOwnership.companyType === "Company") { + await this.companyNameInput.fill(proposalOwnership.companyName); + await this.companyDomainNameInput.fill( + proposalOwnership.companyDomainName + ); + await this.countryOfIncorporationBtn.click(); + await this.page + .getByTestId( + `${proposalOwnership.countryOfIncorportation.toLowerCase()}-country-of-incorporation-button` + ) + .click(); + } + await this.agreeCheckbox.click(); + + await this.continueBtn.click(); + } + + async fillupProblemStatementAndBenefitsForm( + problemStatementAndBenefits: BudgetProposalProblemStatementAndBenefitProps + ) { + await this.problemStatementInput.fill( + problemStatementAndBenefits.problemStatement + ); + await this.proposalBenefitInput.fill( + problemStatementAndBenefits.proposalBenefits + ); + await this.suplimentaryEndorsementInput.fill( + problemStatementAndBenefits.suplimentaryEndorsement + ); + + await this.roadmapNameSelect.click(); + await this.page + .getByTestId( + `${problemStatementAndBenefits.roadmapName.toLowerCase()}-button` + ) + .click(); + + if ( + problemStatementAndBenefits.roadmapName === + "It supports the product roadmap" + ) { + await this.productRoadmapDescriptionInput.fill( + problemStatementAndBenefits.productRoadmapDescription + ); + } + + await this.budgetDiscussionTypeSelect.click(); + await this.page + .getByTestId( + `${problemStatementAndBenefits.budgetDiscussionType.toLowerCase()}-button` + ) + .click(); + + await this.committeeAlignmentTypeSelect.click(); + await this.page + .getByTestId( + `${problemStatementAndBenefits.committeeAlignmentType.toLowerCase()}-button` + ) + .click(); + + await this.continueBtn.click(); + } + + async fillupProposalDetailsForm(proposalDetails: BudgetProposalDetailsProps) { + await this.proposalNameInput.fill(proposalDetails.proposalName); + await this.proposalDescriptionInput.fill( + proposalDetails.proposalDescription + ); + await this.proposalKeyDependenciesInput.fill( + proposalDetails.proposalKeyDependencies + ); + await this.proposalMaintainAndSupportInput.fill( + proposalDetails.proposalMaintainAndSupport + ); + await this.milestonesInput.fill(proposalDetails.milestones); + await this.teamSizeAndDurationInput.fill( + proposalDetails.teamSizeAndDuration + ); + await this.previousExperienceInput.fill(proposalDetails.previousExperience); + + await this.contractingTypeSelect.click(); + await this.page + .getByTestId(`${proposalDetails.contracting.toLowerCase()}-button`) + .click(); + if (proposalDetails.contracting === "Other") { + await this.otherDescriptionInput.fill(proposalDetails.otherDescription); + } + await this.continueBtn.click(); + } + + async fillupCostingForm(costing: BudgetCostingProps) { + await this.adaAmountInput.fill(costing.adaAmount.toString()); + await this.usaToAdaCnversionRateInput.fill( + costing.usaToAdaCnversionRate.toString() + ); + await this.preferredCurrencySelect.click(); + await this.page + .getByTestId(`${costing.preferredCurrency.toLowerCase()}-button`) + .click(); + await this.preferredCurrencyAmountInput.fill( + costing.AmountInPreferredCurrency.toString() + ); + await this.costBreakdownInput.fill(costing.costBreakdown); + + await this.continueBtn.click(); + } + + async fillupFurtherInformation(proposal_links: Array) { + for (let i = 0; i < proposal_links.length; i++) { + if (i > 0) { + await this.addLinkBtn.click(); + } + await this.page + .getByTestId(`link-${i}-url-input`) + .fill(proposal_links[i].prop_link); + await this.page + .getByTestId(`link-${i}-text-input`) + .fill(proposal_links[i].prop_link_text); + } + await this.continueBtn.click(); + } + + async fillupForm(budgetProposal: BudgetProposalProps, stage = 8) { + await this.fillupContactInformationForm(budgetProposal.contactInformation); + + if (stage > 2) { + await this.fillupProposalOwnershipForm(budgetProposal.proposalOwnership); + } + + if (stage > 3) { + await this.fillupProblemStatementAndBenefitsForm( + budgetProposal.problemStatementAndBenefits + ); + } + + if (stage > 4) { + await this.fillupProposalDetailsForm(budgetProposal.proposalDetails); + } + if (stage > 5) { + await this.fillupCostingForm(budgetProposal.costing); + } + if (stage > 6) { + await this.fillupFurtherInformation(budgetProposal.furtherInformation); + } + if (stage > 7) { + await this.intersectNamedAdministratorSelect.click(); + + await this.page + .getByTestId( + `${budgetProposal.administrationAndAuditing.intersectAdministration}-button` + ) + .click(); + if (!budgetProposal.administrationAndAuditing.intersectAdministration) { + await this.venderDetailsInput.fill( + budgetProposal.administrationAndAuditing.venderDetails + ); + } + await this.continueBtn.click(); + + await this.submitCheckbox.click(); + await this.continueBtn.click(); + } + } + + async getAllDrafts() { + await expect( + this.page + .locator('[data-testid^="draft-"][data-testid$="-proposal"]') + .first() + ).toBeVisible({ timeout: 60_000 }); // slow rendering + + return await this.page + .locator('[data-testid^="draft-"][data-testid$="-proposal"]') + .all(); + } + + async getFirstDraft() { + await expect( + this.page + .locator('[data-testid^="draft-"][data-testid$="-proposal"]') + .first() + ).toBeVisible({ timeout: 60_000 }); // slow rendering + + return this.page + .locator('[data-testid^="draft-"][data-testid$="-proposal"]') + .first(); + } + + async viewLastDraft() { + await expect( + this.page + .locator('[data-testid^="draft-"][data-testid$="-proposal"]') + .last() + ).toBeVisible({ timeout: 60_000 }); // slow rendering + + return await this.page.getByTestId("draft-start-editing").last().click(); + } + + generateValidBudgetProposalContactInformation(): BudgetProposalContactInformationProps { + return { + beneficiaryFullName: faker.person.fullName(), + beneficiaryEmail: faker.internet.email(), + beneficiaryCountry: faker.helpers + .arrayElement(Object.values(LocationEnum)) + .replace(/ /g, "-"), + beneficiaryNationality: faker.helpers + .arrayElement(Object.values(LocationEnum)) + .replace(/ /g, "-"), + submissionLeadFullName: faker.person.fullName(), + submissionLeadEmail: faker.internet.email(), + }; + } + + generateValidBudgetProposalProblemStatementAndBenefits(): BudgetProposalProblemStatementAndBenefitProps { + return { + problemStatement: faker.lorem.paragraph(2), + proposalBenefits: faker.lorem.paragraph(2), + roadmapName: faker.helpers.arrayElement(Object.values(RoadmapNameEnum)), + budgetDiscussionType: faker.helpers.arrayElement( + Object.values(BudgetDiscussionEnum).filter( + (type) => type !== "No Category" + ) + ), + productRoadmapDescription: faker.lorem.paragraph(2), + committeeAlignmentType: faker.helpers.arrayElement( + Object.values(CommitteeAlignmentEnum) + ), + suplimentaryEndorsement: faker.lorem.paragraph(2), + }; + } + + generateValidProposalOwnerShip(): BudgetProposalOwnershipProps { + return { + companyType: faker.helpers.arrayElement(Object.values(CompanyEnum)), + publicChampion: faker.helpers.arrayElement( + Object.values(ProposalChampionEnum) + ), + contactDetails: faker.internet.email(), + groupName: faker.company.name(), + groupType: faker.company.buzzVerb(), + groupKeyIdentity: faker.lorem.paragraph(2), + companyName: faker.company.name(), + companyDomainName: faker.internet.domainName(), + countryOfIncorportation: faker.helpers + .arrayElement(Object.values(LocationEnum)) + .replace(/ /g, "-"), + }; + } + + generateValidBudgetProposalDetails(): BudgetProposalDetailsProps { + return { + proposalName: faker.lorem.words(3), + proposalDescription: faker.lorem.paragraph(2), + proposalKeyDependencies: faker.lorem.paragraph(1), + proposalMaintainAndSupport: faker.lorem.paragraph(2), + milestones: faker.lorem.lines(2), + teamSizeAndDuration: faker.lorem.paragraph(2), + previousExperience: faker.lorem.paragraph(2), + contracting: faker.helpers.arrayElement( + Object.values(ProposalContractingEnum) + ), + otherDescription: faker.lorem.paragraph(2), + }; + } + + generateValidCosting(): BudgetCostingProps { + return { + adaAmount: faker.number.int({ min: 100, max: 10000 }), + usaToAdaCnversionRate: faker.number.int({ min: 1, max: 100 }), + preferredCurrency: faker.helpers.arrayElement( + Object.values(PreferredCurrencyEnum) + ), + AmountInPreferredCurrency: faker.number.int({ min: 1, max: 100 }), + costBreakdown: faker.lorem.paragraph(2), + }; + } + + generateValidFurtherInformation(): Array { + return [ + { + prop_link: faker.internet.url(), + prop_link_text: faker.lorem.words(2), + }, + { + prop_link: faker.internet.url(), + prop_link_text: faker.lorem.words(2), + }, + ]; + } + + generateAdministrationAndAuditing(): AdministrationAndAuditingProps { + return { + intersectAdministration: faker.datatype.boolean(), + venderDetails: faker.lorem.paragraph(2), + }; + } + + generateValidBudgetProposalInformation(): BudgetProposalProps { + return { + contactInformation: this.generateValidBudgetProposalContactInformation(), + proposalOwnership: this.generateValidProposalOwnerShip(), + problemStatementAndBenefits: + this.generateValidBudgetProposalProblemStatementAndBenefits(), + proposalDetails: this.generateValidBudgetProposalDetails(), + costing: this.generateValidCosting(), + furtherInformation: this.generateValidFurtherInformation(), + administrationAndAuditing: this.generateAdministrationAndAuditing(), + }; + } + + async createDraftBudgetProposal(fillAllDetails = false) { + const budgetProposal = this.generateValidBudgetProposalInformation(); + + if (fillAllDetails) { + await this.fillupForm(budgetProposal); + } else { + await this.fillupContactInformationForm( + budgetProposal.contactInformation + ); + } + + await this.saveDraftBtn.click(); + await this.closeDraftBtn.click(); + await this.cancelBtn.click(); + await this.createBudgetProposalBtn.click(); + + return fillAllDetails ? budgetProposal : budgetProposal.contactInformation; + } + + async createBudgetProposal(): Promise<{ + proposalId: number; + proposalDetails: BudgetProposalProps; + }> { + const budgetProposalRequest: BudgetProposalProps = + this.generateValidBudgetProposalInformation(); + + await this.fillupForm(budgetProposalRequest); + await this.submitBtn.click(); + + // assert to check if the proposal is created and navigated to details page + await expect(this.page.getByTestId("review-version")).toBeVisible({ + timeout: 60_000, + }); + + const currentPageUrl = this.page.url(); + return { + proposalId: extractProposalIdFromUrl(currentPageUrl), + proposalDetails: budgetProposalRequest, + }; + } +} diff --git a/tests/govtool-frontend/playwright/lib/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index 84cc17c38..7173a5529 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -3,6 +3,7 @@ import { CardanoTestWalletJson } from "@cardanoapi/cardano-test-wallet/types"; export type StaticWallet = CardanoTestWalletJson & { dRepId: string; address: string; + givenName?: string; }; export type KuberValue = { @@ -166,6 +167,9 @@ export type CommentResponse = { comment_text: string; createdAt: string; updatedAt: string; + bd_proposal_id: string | null; + drep_id: string | null; + comments_reports: any; user_govtool_username: string; subcommens_number: number; }; @@ -323,3 +327,194 @@ export interface InvalidMetadataType { url: string; hash: string; } + +export enum BudgetDiscussionEnum { + Core = "Core", + Research = "Research", + GovernanceSupport = "Governance Support", + MarketingAndInnovation = "Marketing & Innovation", + NoCategory = "No Category", +} + +export interface BudgetProposalContactInformationProps { + beneficiaryFullName: string; + beneficiaryEmail: string; + beneficiaryCountry: string; + beneficiaryNationality: string; + submissionLeadFullName: string; + submissionLeadEmail: string; +} + +export type CompanyType = "Individual" | "Company" | "Group"; +export enum CompanyEnum { + Individual = "Individual", + Company = "Company", + Group = "Group", +} + +export type ProposalChampionType = + | "Beneficiary listed above" + | "Submission lead listed above"; +export enum ProposalChampionEnum { + BeneficiaryListedAbove = "Beneficiary listed above", + SubmissionLeadListedAbove = "Submission lead listed above", +} + +export interface BudgetProposalOwnershipProps { + companyType: CompanyType; + publicChampion: ProposalChampionType; + contactDetails: string; + groupName?: string; + groupType?: string; + groupKeyIdentity?: string; + companyName?: string; + companyDomainName?: string; + countryOfIncorportation?: string; +} + +export type RoadmapNameType = + | "Scaling the L1 Engine" + | "Architectural Excellence" + | "Leios" + | "Incoming Liquidity" + | "L2 Expansion" + | "Programmable Assets" + | "Multiple Node Implementations" + | "SPO Incentive Improvements" + | "It doesn't align" + | "It supports the product roadmap" + | "Developer / User Experience"; + +export enum RoadmapNameEnum { + ScalingTheL1Engine = "Scaling the L1 Engine", + ArchitecturalExcellence = "Architectural Excellence", + Leios = "Leios", + IncomingLiquidity = "Incoming Liquidity", + L2Expansion = "L2 Expansion", + ProgrammableAssets = "Programmable Assets", + MultipleNodeImplementations = "Multiple Node Implementations", + SPOIncentiveImprovements = "SPO Incentive Improvements", + NoAlignment = "It doesn't align", + SupportsProductRoadmap = "It supports the product roadmap", + DeveloperUserExperience = "Developer / User Experience", +} + +export type BudgetDiscussionType = + | "Core" + | "Research" + | "Governance Support" + | "Marketing & Innovation" + | "None of these"; + +export enum CommitteeAlignmentEnum { + TechnicalSteeringCommittee = "Technical Steering Committee", + ProductCommittee = "Product Committee", + OpenSourceCommittee = "Open Source Committee", + CivicsCommittee = "Civics Committee", + MembershipAndCommunityCommittee = "Membership & Community Committee", + BudgetCommittee = "Budget Committee", + MarketingCommittee = "Marketing Committee", + Unsure = "Unsure", + None = "None", +} + +export type CommitteeAlignmentType = + | "Technical Steering Committee" + | "Product Committee" + | "Open Source Committee" + | "Civics Committee" + | "Membership & Community Committee" + | "Budget Committee" + | "Marketing Committee" + | "Unsure" + | "None"; + +export enum LocationEnum { + Nepal = "Nepal", + Netherlands = "Netherlands", + UnitedStates = "United States", + UnitedKingdom = "United Kingdom", + Canada = "Canada", + Australia = "Australia", + Germany = "Germany", + France = "France", + Japan = "Japan", + SouthKorea = "South Korea", +} + +export interface BudgetProposalProblemStatementAndBenefitProps { + problemStatement: string; + proposalBenefits: string; + roadmapName: RoadmapNameType; + productRoadmapDescription?: string; + budgetDiscussionType: BudgetDiscussionType; + committeeAlignmentType: CommitteeAlignmentType; + suplimentaryEndorsement: string; +} + +export type ProposalContractingType = + | "Milestone Based Fixed Price" + | "Time and Materials" + | "Service Level Agreement" + | "Other" + | "Reimbursement" + | "Intersect Procurement Process"; + +export enum ProposalContractingEnum { + MilestoneBasedFixedPrice = "Milestone Based Fixed Price", + TimeAndMaterials = "Time and Materials", + ServiceLevelAgreement = "Service Level Agreement", + Other = "Other", + Reimbursement = "Reimbursement", + IntersectProcurementProcess = "Intersect Procurement Process", +} + +export interface BudgetProposalDetailsProps { + proposalName: string; + proposalDescription: string; + proposalKeyDependencies: string; + proposalMaintainAndSupport: string; + milestones: string; + teamSizeAndDuration: string; + previousExperience: string; + contracting: ProposalContractingType; + otherDescription?: string; +} + +export type preferredCurrencyType = + | "United States Dollar" + | "Euro" + | "Japanese Yen" + | "Australian Dollar" + | "Nepalese Rupee"; + +export enum PreferredCurrencyEnum { + UnitedStatesDollar = "United States Dollar", + Euro = "Euro", + JapaneseYen = "Japanese Yen", + AustralianDollar = "Australian Dollar", + NepaleseRupee = "Nepalese Rupee", +} + +export interface BudgetCostingProps { + adaAmount: number; + usaToAdaCnversionRate: number; + preferredCurrency: preferredCurrencyType; + AmountInPreferredCurrency: number; + costBreakdown: string; +} + +export interface AdministrationAndAuditingProps { + intersectAdministration: boolean; + venderDetails: string; +} + +export interface BudgetProposalProps { + contactInformation: BudgetProposalContactInformationProps; + proposalOwnership: BudgetProposalOwnershipProps; + problemStatementAndBenefits: BudgetProposalProblemStatementAndBenefitProps; + proposalDetails: BudgetProposalDetailsProps; + costing: BudgetCostingProps; + furtherInformation: Array; + administrationAndAuditing: AdministrationAndAuditingProps; +} diff --git a/tests/govtool-frontend/playwright/package.json b/tests/govtool-frontend/playwright/package.json index dc24100b7..a57cd1b79 100644 --- a/tests/govtool-frontend/playwright/package.json +++ b/tests/govtool-frontend/playwright/package.json @@ -26,7 +26,7 @@ "test": "npx playwright test", "format": "prettier . --write", "test:outcomes": "npx playwright test outcomes.spec.ts --ui", - "generate-wallets": "ts-node ./generate_wallets.ts 19" + "generate-wallets": "ts-node ./generate_wallets.ts 24" }, "dependencies": { "@cardanoapi/cardano-test-wallet": "^3.0.0", diff --git a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.dRep.spec.ts new file mode 100644 index 000000000..8af3b53bb --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.dRep.spec.ts @@ -0,0 +1,109 @@ +import { faker } from "@faker-js/faker"; +import { test } from "@fixtures/budgetProposal"; +import { setAllureEpic } from "@helpers/allure"; +import BudgetDiscussionDetailsPage from "@pages/budgetDiscussionDetailsPage"; +import { expect } from "@playwright/test"; +import { dRep03Wallet } from "@constants/staticWallets"; +import BudgetDiscussionPage from "@pages/budgetDiscussionPage"; + +test.beforeEach(async () => { + await setAllureEpic("11. Proposal Budget"); +}); + +test.describe("Budget proposal dRep behaviour", () => { + test.use({ + storageState: ".auth/dRep03.json", + wallet: dRep03Wallet, + }); + + test.describe("Budget proposal voting", () => { + let budgetDiscussionDetailsPage: BudgetDiscussionDetailsPage; + test.beforeEach(async ({ page, proposalId }) => { + budgetDiscussionDetailsPage = new BudgetDiscussionDetailsPage(page); + await budgetDiscussionDetailsPage.goto(proposalId); + + await budgetDiscussionDetailsPage.verifyIdentityBtn.click(); + }); + + test("11K. Should allow registered DRep to vote on a proposal", async () => { + const pollVotes = ["Yes", "No"]; + const choice = faker.helpers.arrayElement(pollVotes); + + await budgetDiscussionDetailsPage.voteOnPoll(choice); + + await expect(budgetDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + await expect(budgetDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); + await expect( + budgetDiscussionDetailsPage.currentPage.getByTestId( + `poll-${choice.toLowerCase()}-count` + ) + ).toHaveText(`${choice}: (100%)`); + // opposite of random choice vote + const oppositeVote = pollVotes.filter((vote) => vote !== choice)[0]; + await expect( + budgetDiscussionDetailsPage.currentPage.getByTestId( + `poll-${oppositeVote.toLowerCase()}-count` + ) + ).toHaveText(`${oppositeVote}: (0%)`); + }); + + test("11L. Should allow registered DRep to change vote on a proposal", async () => { + test.slow(); + const pollVotes = ["Yes", "No"]; + const choice = faker.helpers.arrayElement(pollVotes); + + await budgetDiscussionDetailsPage.voteOnPoll(choice); + await budgetDiscussionDetailsPage.changePollVote(); + + await expect(budgetDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + await expect(budgetDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); + + // vote must be changed + await expect( + budgetDiscussionDetailsPage.currentPage.getByTestId( + `poll-${choice.toLowerCase()}-count` + ) + ).toHaveText(`${choice}: (0%)`, { timeout: 60_000 }); + // opposite of random choice vote + const oppositeVote = pollVotes.filter((vote) => vote !== choice)[0]; + await expect( + budgetDiscussionDetailsPage.currentPage.getByTestId( + `poll-${oppositeVote.toLowerCase()}-count` + ) + ).toHaveText(`${oppositeVote}: (100%)`); + }); + }); + + test("11M. Should display DRep tag, name and ID when a registered DRep comments on a proposal", async ({ + page, + }) => { + const comment = faker.lorem.paragraph(2); + const budgetDiscussionPage = new BudgetDiscussionPage(page); + await budgetDiscussionPage.goto(); + const budgetDiscussionDetailsPage = + await budgetDiscussionPage.viewFirstProposal(); + await budgetDiscussionDetailsPage.addComment(comment); + + await expect( + budgetDiscussionDetailsPage.currentPage + .locator('[data-testid^="comment-"][data-testid$="-content"]') + .first() + ).toHaveText(comment); + + const dRepCommentedCard = budgetDiscussionDetailsPage.currentPage + .locator('[data-testid^="comment-"][data-testid$="-content-card"]') + .first(); + + await expect( + dRepCommentedCard.getByText("DRep", { exact: true }) + ).toBeVisible(); + + await expect(dRepCommentedCard.getByTestId("given-name")).toHaveText( + dRep03Wallet.givenName + ); + + await expect(dRepCommentedCard.getByTestId("drep-id")).toHaveText( + dRep03Wallet.dRepId + ); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.loggedin.spec.ts new file mode 100644 index 000000000..e73882eac --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.loggedin.spec.ts @@ -0,0 +1,73 @@ +import { budgetProposal01Wallet } from "@constants/staticWallets"; +import { faker } from "@faker-js/faker"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import BudgetDiscussionDetailsPage from "@pages/budgetDiscussionDetailsPage"; +import BudgetDiscussionPage from "@pages/budgetDiscussionPage"; +import { expect } from "@playwright/test"; + +test.beforeEach(async () => { + await setAllureEpic("11. Proposal Budget"); +}); + +test.describe("Budget proposal logged in state", () => { + test.use({ + storageState: ".auth/budgetProposal01.json", + wallet: budgetProposal01Wallet, + }); + + let budgetDiscussionDetailsPage: BudgetDiscussionDetailsPage; + + test.beforeEach(async ({ page }) => { + const budgetDiscussionPage = new BudgetDiscussionPage(page); + await budgetDiscussionPage.goto(); + await budgetDiscussionPage.verifyIdentityBtn.click(); + budgetDiscussionDetailsPage = + await budgetDiscussionPage.viewFirstProposal(); + }); + + test("11G. Should sort the budget proposal comments", async ({ page }) => { + for (let i = 0; i < 4; i++) { + const comment = faker.lorem.paragraph(2); + await budgetDiscussionDetailsPage.addComment(comment); + await page.waitForTimeout(2_000); + } + await budgetDiscussionDetailsPage.sortAndValidate( + "asc", + (date1, date2) => new Date(date1) <= new Date(date2) + ); + }); + + test("11H. Should restrict non registered DRep users from voting", async () => { + // wait for the page to load + await budgetDiscussionDetailsPage.currentPage.waitForTimeout(5_000); + + await expect(budgetDiscussionDetailsPage.pollVoteCard).not.toBeVisible(); + await expect(budgetDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + + await expect(budgetDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); + }); + + test("11I. Should comments on any proposal", async ({}) => { + const comment = faker.lorem.paragraph(2); + await budgetDiscussionDetailsPage.addComment(comment); + await expect( + budgetDiscussionDetailsPage.currentPage + .locator('[data-testid^="comment-"][data-testid$="-content"]') + .first() + ).toHaveText(comment); + }); + + test("11J. Should reply to any comments", async ({}) => { + const randComment = faker.lorem.paragraph(2); + const randReply = faker.lorem.words(5); + + await budgetDiscussionDetailsPage.addComment(randComment); + + await budgetDiscussionDetailsPage.replyComment(randReply); + const replyRendered = await budgetDiscussionDetailsPage.currentPage + .locator(`[data-testid^="reply-"][data-testid$="-content"]`) + .textContent(); + expect(replyRendered).toContain(randReply); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.spec.ts b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.spec.ts new file mode 100644 index 000000000..9e7fc2ce2 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.spec.ts @@ -0,0 +1,249 @@ +import environments from "@constants/environments"; +import { faker } from "@faker-js/faker"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import { injectLogger } from "@helpers/page"; +import { extractProposalIdFromUrl } from "@helpers/string"; +import { functionWaitedAssert } from "@helpers/waitedLoop"; +import BudgetDiscussionDetailsPage from "@pages/budgetDiscussionDetailsPage"; +import BudgetDiscussionPage from "@pages/budgetDiscussionPage"; +import { expect } from "@playwright/test"; +import { BudgetDiscussionEnum, CommentResponse } from "@types"; + +const mockBudgetProposal = require("../../lib/_mock/budgetProposal.json"); +const mockPoll = require("../../lib/_mock/budgetProposalPoll.json"); +const mockComments = require("../../lib/_mock/budgetProposalComments.json"); + +test.beforeEach(async ({}) => { + await setAllureEpic("11. Proposal Budget"); +}); + +test("11A. Should access budget proposal page in disconnect state", async ({ + page, +}) => { + const budgetDiscussionPage = new BudgetDiscussionPage(page); + await budgetDiscussionPage.goto(); + + await expect( + budgetDiscussionPage.currentPage.getByText(/Budget Proposals/i) + ).toHaveCount(2); +}); + +test.describe("Budget proposal list manipulation", () => { + test("11B_1. Should search for budget proposals by title", async ({ + page, + }) => { + let proposalName = "EchoFeed"; + let proposalNameSet = false; + + await page.route("**/api/bds?**", async (route) => { + const response = await route.fetch(); + const json = await response.json(); + if (!proposalNameSet && "data" in json && json["data"].length > 0) { + const randomIndex = Math.floor(Math.random() * json["data"].length); + proposalName = + json["data"][randomIndex]["attributes"]["bd_proposal_detail"]["data"][ + "attributes" + ]["proposal_name"]; + proposalNameSet = true; + } + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(json), + }); + }); + + const responsePromise = page.waitForResponse("**/api/bds?**"); + const budgetDiscussionPage = new BudgetDiscussionPage(page); + await budgetDiscussionPage.goto(); + + await responsePromise; + + await budgetDiscussionPage.searchInput.fill(proposalName); + + await page.waitForTimeout(2000); + + await functionWaitedAssert( + async () => { + const proposalCards = await budgetDiscussionPage.getAllProposals(); + for (const proposalCard of proposalCards) { + await expect(proposalCard).toBeVisible(); + const proposalTitle = await proposalCard + .getByTestId("budget-discussion-title") + .textContent(); + expect(proposalTitle.toLowerCase()).toContain( + proposalName.toLowerCase() + ); + } + }, + { + message: `A proposal card does not contain the search term ${proposalName}`, + } + ); + }); + + test.describe("Filter and sort budget proposals", () => { + let budgetDiscussionPage: BudgetDiscussionPage; + + test.beforeEach(async ({ page }) => { + budgetDiscussionPage = new BudgetDiscussionPage(page); + await budgetDiscussionPage.goto(); + }); + + test("11B_2. Should filter budget proposals by categories", async () => { + test.slow(); + await budgetDiscussionPage.filterBtn.click(); + + // proposal type filter + await budgetDiscussionPage.applyAndValidateFilters( + Object.values(BudgetDiscussionEnum), + budgetDiscussionPage._validateTypeFiltersInProposalCard + ); + }); + + test("11B_3. Should sort budget proposals", async () => { + await budgetDiscussionPage.sortAndValidate( + "asc", + (p1, p2) => p1.attributes.createdAt <= p2.attributes.createdAt + ); + + await budgetDiscussionPage.sortAndValidate( + "desc", + (p1, p2) => p1.attributes.createdAt >= p2.attributes.createdAt + ); + }); + }); +}); + +test("11C. Should show view-all categorized budget proposal", async ({ + browser, +}) => { + await Promise.all( + Object.values(BudgetDiscussionEnum).map(async (proposalType: string) => { + const context = await browser.newContext(); + const page = await context.newPage(); + injectLogger(page); + + const budgetDiscussionPage = new BudgetDiscussionPage(page); + await budgetDiscussionPage.goto(); + const isShowAllButtonVisible = await page + .waitForSelector( + `[data-testid="${proposalType.toLowerCase().replace(/ /g, "-")}-show-all-button"]`, + { timeout: 60_000 } + ) + .then(() => true) + .catch(() => false); + + if (isShowAllButtonVisible) { + await page + .getByTestId( + proposalType.toLowerCase().replace(/ /g, "-") + "-show-all-button" + ) + .click(); + + const proposalCards = await budgetDiscussionPage.getAllProposals(); + + for (const proposalCard of proposalCards) { + const ExpectedProposalType = + proposalType === BudgetDiscussionEnum.NoCategory + ? "None of these" + : proposalType; + await expect( + proposalCard.getByTestId("budget-discussion-type") + ).toHaveText(ExpectedProposalType, { timeout: 60_000 }); + } + } else { + expect(true, `No ${proposalType} found`).toBeTruthy(); + } + }) + ); +}); + +test("11D. Should share budget proposal", async ({ page, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + const budgetDiscussionPage = new BudgetDiscussionPage(page); + await budgetDiscussionPage.goto(); + + const budgetDiscussionDetailsPage = + await budgetDiscussionPage.viewFirstProposal(); + + const currentPageUrl = page.url(); + const proposalId = extractProposalIdFromUrl(currentPageUrl); + + await budgetDiscussionDetailsPage.shareBtn.click(); + await budgetDiscussionDetailsPage.copyLinkBtn.click(); + await expect(budgetDiscussionDetailsPage.copyLinkText).toBeVisible(); + + const copiedTextDRepDirectory = await page.evaluate(() => + navigator.clipboard.readText() + ); + const expectedCopyUrl = `${environments.frontendUrl}/budget_discussion/${proposalId}`; + + expect(copiedTextDRepDirectory).toEqual(expectedCopyUrl); +}); + +test("11E. Should view comments with count indications on a budget proposal", async ({ + page, +}) => { + let responsePromise = page.waitForResponse((response) => + response.url().includes(`/api/comments`) + ); + + const budgetDiscussionPage = new BudgetDiscussionPage(page); + await budgetDiscussionPage.goto(); + + const budgetDiscussionDetailsPage = + await budgetDiscussionPage.viewFirstProposal(); + const response = await responsePromise; + + const comments: CommentResponse[] = (await response.json()).data; + + await responsePromise; + + await expect(budgetDiscussionDetailsPage.totalComments).toHaveText( + comments.length.toString() + ); +}); + +test.describe("Restricted access to interact budget proposal", () => { + let budgetDiscussionDetailsPage: BudgetDiscussionDetailsPage; + + test.beforeEach(async ({ page }) => { + await page.route("**/api/bds/**", async (route) => + route.fulfill({ + body: JSON.stringify(mockBudgetProposal), + }) + ); + + await page.route("**/api/bd-polls**", async (route) => + route.fulfill({ + body: JSON.stringify(mockPoll), + }) + ); + + await page.route("**/api/comments**", async (route) => + route.fulfill({ + body: JSON.stringify(mockComments), + }) + ); + + budgetDiscussionDetailsPage = new BudgetDiscussionDetailsPage(page); + await budgetDiscussionDetailsPage.goto(mockBudgetProposal.data.id); + }); + test("11F_1. Should restrict users without wallets from commenting", async () => { + await budgetDiscussionDetailsPage.commentInput.fill( + faker.lorem.paragraph() + ); + + await expect(budgetDiscussionDetailsPage.commentBtn).toBeDisabled(); + }); + test("11F_2. Should restrict users without wallets from voting", async () => { + // wait for the page to load + await budgetDiscussionDetailsPage.currentPage.waitForTimeout(5_000); + await expect(budgetDiscussionDetailsPage.pollVoteCard).not.toBeVisible(); + await expect(budgetDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + + await expect(budgetDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.loggedin.spec.ts new file mode 100644 index 000000000..b42d941cc --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.loggedin.spec.ts @@ -0,0 +1,307 @@ +import { + budgetProposal01Wallet, + budgetProposal02Wallet, + budgetProposal03Wallet, + budgetProposal04Wallet, +} from "@constants/staticWallets"; +import { test } from "@fixtures/budgetProposal"; +import { setAllureEpic } from "@helpers/allure"; +import { createNewPageWithWallet } from "@helpers/page"; +import BudgetDiscussionDetailsPage from "@pages/budgetDiscussionDetailsPage"; +import BudgetDiscussionSubmissionPage from "@pages/budgetDiscussionSubmissionPage"; +import { expect } from "@playwright/test"; +import { + BudgetProposalContactInformationProps, + BudgetProposalProps, + CompanyEnum, +} from "@types"; + +test.beforeEach(async () => { + await setAllureEpic("12. Proposal Budget Submission"); +}); + +test.describe("Budget proposal 01 wallet", () => { + test.use({ + storageState: ".auth/budgetProposal01.json", + wallet: budgetProposal01Wallet, + }); + + test("12B. Should access proposal creation page in connected state", async ({ + page, + }) => { + await page.goto("/"); + await page.getByTestId("budget-discussion-link").click(); + await page.getByTestId("verify-identity-button").click(); + + await expect( + page.getByTestId("propose-a-budget-discussion-button") + ).toBeVisible({ timeout: 60_000 }); + }); + + test.describe("Budget proposal with proposalSubmissionPageNavigation", () => { + let budgetProposalSubmissionPage: BudgetDiscussionSubmissionPage; + test.beforeEach(async ({ page }) => { + budgetProposalSubmissionPage = new BudgetDiscussionSubmissionPage(page); + await budgetProposalSubmissionPage.goto(); + }); + + test.describe("Budget proposal field verification", () => { + test("12D_1. Should verify all field of “contact information” section", async () => { + await expect( + budgetProposalSubmissionPage.beneficiaryFullNameInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.beneficiaryEmailInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.beneficiaryCountrySelect + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.beneficiaryNationalitySelect + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.submissionLeadFullNameInput + ).toBeVisible(); + }); + + test("12D_2. Should verify all field of “proposal ownership” section", async () => { + const proposalContactInformationContent = + budgetProposalSubmissionPage.generateValidBudgetProposalContactInformation(); + await budgetProposalSubmissionPage.fillupContactInformationForm( + proposalContactInformationContent + ); + + // default field + await expect( + budgetProposalSubmissionPage.companyTypeSelect + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.publicChampionSelect + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.contactDetailsInput + ).toBeVisible(); + + // company type field + await budgetProposalSubmissionPage.companyTypeSelect.click(); + await budgetProposalSubmissionPage.currentPage + .getByRole("option", { name: CompanyEnum.Company }) + .click(); //BUG missing testId + + await expect( + budgetProposalSubmissionPage.companyNameInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.companyDomainNameInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.countryOfIncorporationBtn + ).toBeVisible(); + + // group type field + await budgetProposalSubmissionPage.companyTypeSelect.click(); + await budgetProposalSubmissionPage.currentPage + .getByRole("option", { name: CompanyEnum.Group }) + .click(); //BUG missing testId + await expect(budgetProposalSubmissionPage.groupNameInput).toBeVisible(); + await expect(budgetProposalSubmissionPage.groupTypeInput).toBeVisible(); + await expect( + budgetProposalSubmissionPage.keyInformationOfGroupInput + ).toBeVisible(); + }); + + test("12D_3. Should verify all field of “problem statements and proposal benefits” section", async () => { + const proposalInformation = + budgetProposalSubmissionPage.generateValidBudgetProposalInformation(); + await budgetProposalSubmissionPage.fillupForm(proposalInformation, 3); + + await expect( + budgetProposalSubmissionPage.problemStatementInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.proposalBenefitInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.roadmapNameSelect + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.budgetDiscussionTypeSelect + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.committeeAlignmentTypeSelect + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.suplimentaryEndorsementInput + ).toBeVisible(); + }); + + test("12D_4. Should verify all field of “proposal details” section", async () => { + const proposalInformation = + budgetProposalSubmissionPage.generateValidBudgetProposalInformation(); + await budgetProposalSubmissionPage.fillupForm(proposalInformation, 4); + + await expect( + budgetProposalSubmissionPage.proposalNameInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.proposalDescriptionInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.proposalKeyDependenciesInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.milestonesInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.teamSizeAndDurationInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.previousExperienceInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.contractingTypeSelect + ).toBeVisible(); + }); + + test("12D_5. Should verify all field of “costing” section", async () => { + const proposalInformation = + budgetProposalSubmissionPage.generateValidBudgetProposalInformation(); + await budgetProposalSubmissionPage.fillupForm(proposalInformation, 5); + + await expect(budgetProposalSubmissionPage.adaAmountInput).toBeVisible(); + await expect( + budgetProposalSubmissionPage.usaToAdaCnversionRateInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.preferredCurrencySelect + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.preferredCurrencyAmountInput + ).toBeVisible(); + await expect( + budgetProposalSubmissionPage.costBreakdownInput + ).toBeVisible(); + }); + + test("12D_6. Should verify all field of “further information” section", async () => { + const proposalInformation = + budgetProposalSubmissionPage.generateValidBudgetProposalInformation(); + await budgetProposalSubmissionPage.fillupForm(proposalInformation, 6); + + await expect(budgetProposalSubmissionPage.linkTextInput).toBeVisible(); + await expect(budgetProposalSubmissionPage.linkUrlInput).toBeVisible(); + await expect(budgetProposalSubmissionPage.addLinkBtn).toBeVisible(); + }); + + test("12D_7. Should verify all field of “administration and auditing” section", async () => { + const proposalInformation = + budgetProposalSubmissionPage.generateValidBudgetProposalInformation(); + await budgetProposalSubmissionPage.fillupForm(proposalInformation, 7); + + await expect( + budgetProposalSubmissionPage.intersectNamedAdministratorSelect + ).toBeVisible(); + }); + }); + + test.describe("Budget proposal field validation", () => { + test("12E_1. Should accept valid data in “contact information” section", async ({}) => {}); + test("12E_2. Should accept valid data in “proposal ownership” section", async ({}) => {}); + test("12E_3. Should accept valid data in “problem statements and proposal benefits” section", async ({}) => {}); + test("12E_4. Should accept valid data in “proposal details” section", async ({}) => {}); + test("12E_5. Should accept valid data in “costing” section", async ({}) => {}); + test("12E_6. Should accept valid data in “further information” section", async ({}) => {}); + + test("12F_1. Should reject invalid data in “contact information” section", async ({}) => {}); + test("12F_2. Should reject invalid data in “proposal ownership” section", async ({}) => {}); + test("12F_3. Should reject invalid data in “problem statements and proposal benefits” section", async ({}) => {}); + test("12E_4. Should accept invalid data in “proposal details” section", async ({}) => {}); + test("12F_5. Should reject invalid data in “costing” section", async ({}) => {}); + test("12F_6. Should reject invalid data in “further information” section", async ({}) => {}); + }); + + test("12G. Should validate and review submitted budget proposal", async ({}) => {}); + }); +}); + +test("12C. Should save and view draft proposal", async ({ browser }) => { + const page = await createNewPageWithWallet(browser, { + storageState: ".auth/budgetProposal02.json", + wallet: budgetProposal02Wallet, + }); + + const budgetSubmissionPage = new BudgetDiscussionSubmissionPage(page); + await budgetSubmissionPage.goto(); + const draftContactInformationContent = + (await budgetSubmissionPage.createDraftBudgetProposal()) as BudgetProposalContactInformationProps; + const getAddDrafts = await budgetSubmissionPage.getAllDrafts(); + + expect(getAddDrafts.length).toBeGreaterThan(0); + + await budgetSubmissionPage.viewLastDraft(); + + await expect(budgetSubmissionPage.beneficiaryFullNameInput).toHaveValue( + draftContactInformationContent.beneficiaryFullName + ); + await expect(budgetSubmissionPage.beneficiaryEmailInput).toHaveValue( + draftContactInformationContent.beneficiaryEmail + ); + await expect(budgetSubmissionPage.beneficiaryCountrySelect).toHaveText( + draftContactInformationContent.beneficiaryCountry + ); + await expect(budgetSubmissionPage.beneficiaryNationalitySelect).toHaveText( + draftContactInformationContent.beneficiaryNationality + ); + await expect(budgetSubmissionPage.submissionLeadFullNameInput).toHaveValue( + draftContactInformationContent.submissionLeadFullName + ); + await expect(budgetSubmissionPage.submissionLeadEmailInput).toHaveValue( + draftContactInformationContent.submissionLeadEmail + ); +}); + +test("12H. Should submit a valid budget proposal", async ({ browser }) => { + const page = await createNewPageWithWallet(browser, { + storageState: ".auth/budgetProposal03.json", + wallet: budgetProposal03Wallet, + }); + const budgetSubmissionPage = new BudgetDiscussionSubmissionPage(page); + await budgetSubmissionPage.goto(); + const { proposalId, proposalDetails } = + await budgetSubmissionPage.createBudgetProposal(); + + const budgetDiscussionDetailsPage = new BudgetDiscussionDetailsPage(page); + await budgetDiscussionDetailsPage.goto(proposalId); + + await budgetDiscussionDetailsPage.validateProposalDetails(proposalDetails); + + await budgetDiscussionDetailsPage.deleteProposal(); +}); + +test("12I. Should submit a valid draft budget proposal", async ({ + browser, +}) => { + test.slow(); + const page = await createNewPageWithWallet(browser, { + storageState: ".auth/budgetProposal04.json", + wallet: budgetProposal04Wallet, + }); + + const budgetSubmissionPage = new BudgetDiscussionSubmissionPage(page); + await budgetSubmissionPage.goto(); + const draftContact = (await budgetSubmissionPage.createDraftBudgetProposal( + true + )) as BudgetProposalProps; + + await budgetSubmissionPage.viewLastDraft(); + + for (let i = 0; i < 8; i++) { + await budgetSubmissionPage.continueBtn.click(); + } + await budgetSubmissionPage.submitBtn.click(); + + const budgetDiscussionDetailsPage = new BudgetDiscussionDetailsPage(page); + await budgetDiscussionDetailsPage.validateProposalDetails(draftContact); + + await budgetDiscussionDetailsPage.deleteProposal(); +}); diff --git a/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.spec.ts b/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.spec.ts new file mode 100644 index 000000000..3ea61045e --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.spec.ts @@ -0,0 +1,22 @@ +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import { isMobile } from "@helpers/mobile"; +import { expect } from "@playwright/test"; + +test.beforeEach(async ({}) => { + await setAllureEpic("12. Proposal Budget Submission"); +}); + +test("12A. Should restrict from creating a budget proposal in disconnect state", async ({ + page, +}) => { + await page.goto("/"); + if (isMobile(page)) { + await page.getByTestId("open-drawer-button").click(); + } + await page.getByTestId("budget-discussion-link").click(); + await expect(page.getByTestId("verify-identity-button")).not.toBeVisible(); + await expect( + page.getByTestId("propose-a-budget-discussion-button") + ).not.toBeVisible(); +}); diff --git a/tests/govtool-frontend/playwright/tests/auth.setup.ts b/tests/govtool-frontend/playwright/tests/auth.setup.ts index 136a74098..2732ff065 100644 --- a/tests/govtool-frontend/playwright/tests/auth.setup.ts +++ b/tests/govtool-frontend/playwright/tests/auth.setup.ts @@ -7,8 +7,13 @@ import { adaHolder04Wallet, adaHolder05Wallet, adaHolder06Wallet, + budgetProposal01Wallet, + budgetProposal02Wallet, + budgetProposal03Wallet, + budgetProposal04Wallet, dRep01Wallet, dRep02Wallet, + dRep03Wallet, proposal01Wallet, proposal02Wallet, proposal03Wallet, @@ -31,6 +36,7 @@ import { skipIfNotHardFork } from "@helpers/cardano"; const dRep01AuthFile = ".auth/dRep01.json"; const dRep02AuthFile = ".auth/dRep02.json"; +const dRep03AuthFile = ".auth/dRep03.json"; const adaHolder01AuthFile = ".auth/adaHolder01.json"; const adaHolder02AuthFile = ".auth/adaHolder02.json"; @@ -51,6 +57,11 @@ const proposal07AuthFile = ".auth/proposal07.json"; const proposal08AuthFile = ".auth/proposal08.json"; const proposal09AuthFile = ".auth/proposal09.json"; +const budgetProposal01AuthFile = ".auth/budgetProposal01.json"; +const budgetProposal02AuthFile = ".auth/budgetProposal02.json"; +const budgetProposal03AuthFile = ".auth/budgetProposal03.json"; +const budgetProposal04AuthFile = ".auth/budgetProposal04.json"; + setup.beforeEach(async () => { await setAllureEpic("Setup"); await setAllureStory("Authentication"); @@ -75,6 +86,15 @@ setup("Create DRep 02 auth", async ({ page, context }) => { }); }); +setup("Create DRep 03 auth with username", async ({ page, context }) => { + await createAuthWithUserName({ + page, + context, + wallet: dRep03Wallet, + auth: dRep03AuthFile, + }); +}); + setup("Create User 01 auth", async ({ page, context }) => { await createAuth({ page, @@ -218,3 +238,39 @@ setup("Create Proposal 09 auth", async ({ page, context }) => { auth: proposal09AuthFile, }); }); + +setup("Create Budget Proposal 01 auth", async ({ page, context }) => { + await createAuthWithUserName({ + page, + context, + wallet: budgetProposal01Wallet, + auth: budgetProposal01AuthFile, + }); +}); + +setup("Create Budget Proposal 02 auth", async ({ page, context }) => { + await createAuthWithUserName({ + page, + context, + wallet: budgetProposal02Wallet, + auth: budgetProposal02AuthFile, + }); +}); + +setup("Create Budget Proposal 03 auth", async ({ page, context }) => { + await createAuthWithUserName({ + page, + context, + wallet: budgetProposal03Wallet, + auth: budgetProposal03AuthFile, + }); +}); + +setup("Create Budget Proposal 04 auth", async ({ page, context }) => { + await createAuthWithUserName({ + page, + context, + wallet: budgetProposal04Wallet, + auth: budgetProposal04AuthFile, + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/dRep.setup.ts b/tests/govtool-frontend/playwright/tests/dRep.setup.ts index 239e10e6d..7677ec7f5 100644 --- a/tests/govtool-frontend/playwright/tests/dRep.setup.ts +++ b/tests/govtool-frontend/playwright/tests/dRep.setup.ts @@ -11,9 +11,10 @@ import { test as setup } from "@fixtures/walletExtension"; import kuberService from "@services/kuberService"; import walletManager from "lib/walletManager"; import { functionWaitedAssert } from "@helpers/waitedLoop"; +import { StaticWallet } from "@types"; const REGISTER_DREP_WALLETS_COUNT = 6; -const DREP_WALLETS_COUNT = 10; +const DREP_WALLETS_COUNT = 9; let dRepDeposit: number; @@ -40,7 +41,17 @@ setup("Register DRep of static wallets", async () => { try { // Submit metadata to obtain a URL and generate hash value. const metadataPromises = dRepWallets.map(async (dRepWallet) => { - return { ...(await uploadMetadataAndGetJsonHash()), wallet: dRepWallet }; + const metadataResponse = await uploadMetadataAndGetJsonHash(); + const givenName = metadataResponse.givenName; + const index = dRepWallets.indexOf(dRepWallet); + dRepWallets[index] = { + ...dRepWallet, + givenName, + }; + return { + ...metadataResponse, + wallet: dRepWallet, + }; }); const metadataAndDRepWallets = await Promise.all(metadataPromises); @@ -64,7 +75,7 @@ setup("Register DRep of static wallets", async () => { setup("Setup temporary DRep wallets", async () => { setup.setTimeout(3 * environments.txTimeOut); - const dRepWallets = await generateWallets(DREP_WALLETS_COUNT); + const dRepWallets: StaticWallet[] = await generateWallets(DREP_WALLETS_COUNT); const registerDRepWallets = await generateWallets( REGISTER_DREP_WALLETS_COUNT ); @@ -78,7 +89,17 @@ setup("Setup temporary DRep wallets", async () => { // Submit metadata to obtain a URL and generate hash value. const metadataPromises = dRepWallets.map(async (dRepWallet) => { - return { ...(await uploadMetadataAndGetJsonHash()), wallet: dRepWallet }; + const metadataResponse = await uploadMetadataAndGetJsonHash(); + const givenName = metadataResponse.givenName; + const index = dRepWallets.indexOf(dRepWallet); + dRepWallets[index] = { + ...dRepWallet, + givenName, + }; + return { + ...metadataResponse, + wallet: dRepWallet, + }; }); const metadatasAndDRepWallets = await Promise.all(metadataPromises); diff --git a/tests/govtool-frontend/playwright/tests/faucet.setup.ts b/tests/govtool-frontend/playwright/tests/faucet.setup.ts index c0f61e63e..129fc35b7 100644 --- a/tests/govtool-frontend/playwright/tests/faucet.setup.ts +++ b/tests/govtool-frontend/playwright/tests/faucet.setup.ts @@ -6,6 +6,7 @@ import { pollTransaction } from "@helpers/transaction"; import { test as setup } from "@fixtures/walletExtension"; import { loadAmountFromFaucet } from "@services/faucetService"; import kuberService from "@services/kuberService"; +import { functionWaitedAssert } from "@helpers/waitedLoop"; setup.describe.configure({ timeout: environments.txTimeOut }); @@ -16,9 +17,15 @@ setup.beforeEach(async () => { }); setup("Faucet setup", async () => { - const balance = await kuberService.getBalance(faucetWallet.address); - if (balance > 100_000) return; + let balance: number; + functionWaitedAssert( + async () => { + balance = await kuberService.getBalance(faucetWallet.address); + }, + { message: "get balance" } + ); + if (balance > 100_000) return; const res = await loadAmountFromFaucet(faucetWallet.address); await pollTransaction(res.txid); });