From 2d535a8db8650063292b1ba0f3326d5b029b73af Mon Sep 17 00:00:00 2001 From: Olha Pidlisnyuk Date: Mon, 20 Jan 2025 17:03:53 +0100 Subject: [PATCH 1/4] test: e2e tests for mint token and fund transfer features --- playwright/e2e/manage-colony-funds.spec.ts | 357 +++++++++++++++++++++ playwright/e2e/staged-payment.spec.ts | 6 +- playwright/models/manage-colony-funds.ts | 301 +++++++++++++++++ 3 files changed, 661 insertions(+), 3 deletions(-) create mode 100644 playwright/e2e/manage-colony-funds.spec.ts create mode 100644 playwright/models/manage-colony-funds.ts diff --git a/playwright/e2e/manage-colony-funds.spec.ts b/playwright/e2e/manage-colony-funds.spec.ts new file mode 100644 index 0000000000..f73670eb11 --- /dev/null +++ b/playwright/e2e/manage-colony-funds.spec.ts @@ -0,0 +1,357 @@ +import { type BrowserContext, expect, type Page, test } from '@playwright/test'; + +import { Extensions } from '../models/extensions.ts'; +import { ManageColonyFunds } from '../models/manage-colony-funds.ts'; +import { + setCookieConsent, + signInAndNavigateToColony, +} from '../utils/common.ts'; + +test.describe.configure({ mode: 'serial' }); +test.describe('Manage Colony Funds', () => { + let page: Page; + let context: BrowserContext; + let manageColonyFunds: ManageColonyFunds; + let extensions: Extensions; + test.beforeAll(async ({ browser, baseURL }) => { + context = await browser.newContext(); + page = await context.newPage(); + manageColonyFunds = new ManageColonyFunds(page); + extensions = new Extensions(page); + await setCookieConsent(context, baseURL); + await signInAndNavigateToColony(page, { + colonyUrl: '/planex', + wallet: /dev wallet 1$/i, + }); + await extensions.enableReputationWeightedExtension({ + colonyPath: '/planex', + }); + }); + + test.afterAll(async () => { + await context?.close(); + }); + test.describe('Mint tokens', () => { + test('Permissions decision method', async () => { + const [currentBalance] = await manageColonyFunds.getBalance('planex', [ + { + token: 'CREDS', + }, + ]); + await manageColonyFunds.open('Mint tokens'); + + await manageColonyFunds.fillMintTokensForm({ + amount: '1_000_000', + decisionMethod: 'Permissions', + title: 'Mint tokens', + }); + await manageColonyFunds.mintTokensButton.click(); + await manageColonyFunds.waitForTransaction(); + + await expect( + manageColonyFunds.completedAction.getByRole('heading', { + name: 'Mint tokens', + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText(/Mint \w+ CREDS by \w+/), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText('Mint Tokens', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText('1MCREDS', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText('Permissions', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText( + 'Member used permissions to', + ), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByRole('heading', { + name: 'Overview', + }), + ).toBeVisible(); + + const [newBalance] = await manageColonyFunds.getBalance('planex', [ + { + token: 'CREDS', + }, + ]); + + const initial = parseFloat(currentBalance); + const after = parseFloat(newBalance); + + await expect(after).toBeGreaterThan(initial); + }); + + // eslint-disable-next-line playwright/no-skipped-test + test.skip('Reputation decision method', async () => { + await manageColonyFunds.open('Mint tokens'); + + await manageColonyFunds.fillMintTokensForm({ + amount: '1', + decisionMethod: 'Reputation', + title: 'Mint tokens', + }); + + await manageColonyFunds.mintTokensButton.click(); + await manageColonyFunds.waitForTransaction(); + + await expect(manageColonyFunds.stepper).toBeVisible(); + await expect(manageColonyFunds.completedAction).toBeVisible(); + await expect( + manageColonyFunds.stepper.getByText(/Total stake required: \d+/), + ).toBeVisible(); + + await manageColonyFunds.support(); + + await expect(manageColonyFunds.stepper).toHaveText(/100% Supported/); + + await manageColonyFunds.oppose(); + + await expect(manageColonyFunds.stepper).toHaveText(/100% Opposed/); + + await manageColonyFunds.stepper + .getByText(/Vote to support or oppose?/) + .waitFor(); + await manageColonyFunds.supportButton.click(); + await manageColonyFunds.submitVoteButton.click(); + await manageColonyFunds.revealVoteButton.click(); + await manageColonyFunds.stepper.getByText('vote revealed').waitFor(); + await expect( + manageColonyFunds.stepper.getByText( + 'Finalize to execute the agreed transaction', + ), + ).toBeVisible(); + await manageColonyFunds.finalizeButton.click(); + + await manageColonyFunds.waitForPending(); + await manageColonyFunds.claimButton.click(); + await manageColonyFunds.waitForPending(); + await manageColonyFunds.completedAction + .getByRole('heading', { name: 'Your overview Claimed' }) + .waitFor(); + + await manageColonyFunds.claimButton.waitFor({ + state: 'hidden', + }); + }); + + test('Form validation', async () => { + await manageColonyFunds.open('Mint tokens'); + await manageColonyFunds.mintTokensButton.click(); + + for (const message of ManageColonyFunds.validationMessages.mintTokens + .allRequiredFields) { + await expect(manageColonyFunds.drawer.getByText(message)).toBeVisible(); + } + + await manageColonyFunds.fillMintTokensForm({ + title: + 'This is a test title that exceeds the maximum character limit of sixty.', + }); + + await manageColonyFunds.mintTokensButton.click(); + + await expect( + manageColonyFunds.drawer.getByText( + ManageColonyFunds.validationMessages.common.title.maxLengthExceeded, + ), + ).toBeVisible(); + }); + }); + + test.describe('Funds transfer', () => { + test('Permissions decision method', async () => { + const transferAmount = '1'; + const [serenityBalance, andromedaBalance] = + await manageColonyFunds.getBalance('planex', [ + { + token: 'CREDS', + team: 'Serenity', + }, + { + token: 'CREDS', + team: 'Andromeda', + }, + ]); + await manageColonyFunds.open('Transfer funds'); + + await manageColonyFunds.fillTransferFundsForm({ + title: 'Transfer funds', + amount: transferAmount, + decisionMethod: 'Permissions', + from: 'Serenity', + to: 'Andromeda', + }); + + await manageColonyFunds.transferFundsButton.click(); + await manageColonyFunds.waitForTransaction(); + + await expect( + manageColonyFunds.completedAction.getByText( + `Move ${transferAmount} CREDS from Serenity to Andromeda`, + ), + ).toBeVisible(); + + const [newSerenityBalance, newAndromedaBalance] = + await manageColonyFunds.getBalance('planex', [ + { + token: 'CREDS', + team: 'Serenity', + }, + { + token: 'CREDS', + team: 'Andromeda', + }, + ]); + + expect(parseFloat(newSerenityBalance)).toBe( + parseFloat(serenityBalance) - parseFloat(transferAmount), + ); + expect(parseFloat(newAndromedaBalance)).toBe( + parseFloat(andromedaBalance) + parseFloat(transferAmount), + ); + }); + + // eslint-disable-next-line playwright/no-skipped-test + test.skip('Reputation decision method', async () => { + await manageColonyFunds.open('Transfer funds'); + + await manageColonyFunds.fillTransferFundsForm({ + title: 'Transfer funds', + amount: '1', + decisionMethod: 'Reputation', + from: 'General', + to: 'Andromeda', + }); + + await manageColonyFunds.transferFundsButton.click(); + await manageColonyFunds.waitForTransaction(); + + await expect(manageColonyFunds.stepper).toBeVisible(); + await expect(manageColonyFunds.completedAction).toBeVisible(); + await expect( + manageColonyFunds.completedAction.getByText( + /Move 1 CREDS from General to Andromeda/, + ), + ).toBeVisible(); + await expect( + manageColonyFunds.stepper.getByText(/Total stake required: \d+/), + ).toBeVisible(); + + await manageColonyFunds.support(); + + await expect(manageColonyFunds.stepper).toHaveText(/100% Supported/); + + await manageColonyFunds.oppose(); + + await expect(manageColonyFunds.stepper).toHaveText(/100% Opposed/); + + await manageColonyFunds.stepper + .getByText(/Vote to support or oppose?/) + .waitFor(); + await manageColonyFunds.supportButton.click(); + await manageColonyFunds.submitVoteButton.click(); + await expect( + manageColonyFunds.stepper.getByText(/You voted:Support/), + ).toBeVisible(); + + await expect( + manageColonyFunds.stepper.getByText(/^0 votes revealed$/), + ).toBeVisible(); + + await manageColonyFunds.revealVoteButton.click(); + + await manageColonyFunds.stepper + .getByRole('button', { + name: 'Support wins', + }) + .waitFor(); + + await expect( + manageColonyFunds.stepper.getByText( + 'Finalize to execute the agreed transaction', + ), + ).toBeVisible(); + await manageColonyFunds.finalizeButton.click(); + + await manageColonyFunds.waitForPending(); + await manageColonyFunds.claimButton.click(); + await manageColonyFunds.waitForPending(); + + await manageColonyFunds.stepper + .getByRole('heading', { + name: 'Your overview Claimed', + }) + .waitFor(); + }); + + test('Form validation', async () => { + await manageColonyFunds.open('Transfer funds'); + await manageColonyFunds.transferFundsButton.click(); + + for (const message of ManageColonyFunds.validationMessages.fundsTransfer + .allRequiredFields) { + await expect(manageColonyFunds.drawer.getByText(message)).toBeVisible(); + } + + await expect( + manageColonyFunds.drawer.getByText( + ManageColonyFunds.validationMessages.fundsTransfer.team.sameTeam, + ), + ).toBeHidden(); + + await manageColonyFunds.fillTransferFundsForm({ + from: 'General', + to: 'General', + amount: '1', + title: 'Transfer funds', + decisionMethod: 'Permissions', + }); + + await expect( + manageColonyFunds.drawer.getByText( + ManageColonyFunds.validationMessages.fundsTransfer.team.sameTeam, + ), + ).toBeVisible(); + + await manageColonyFunds.fillTransferFundsForm({ + amount: '999_999_999_999_999_999', + }); + + await expect( + manageColonyFunds.drawer.getByText( + ManageColonyFunds.validationMessages.fundsTransfer.amount + .notEnoughFunds, + ), + ).toBeVisible(); + + await manageColonyFunds.fillTransferFundsForm({ + tokenSymbol: 'ƓƓƓ', + }); + + await expect( + manageColonyFunds.drawer.getByText( + ManageColonyFunds.validationMessages.fundsTransfer.token.locked, + ), + ).toBeVisible(); + }); + }); +}); diff --git a/playwright/e2e/staged-payment.spec.ts b/playwright/e2e/staged-payment.spec.ts index b41be524a8..464edcf864 100644 --- a/playwright/e2e/staged-payment.spec.ts +++ b/playwright/e2e/staged-payment.spec.ts @@ -52,7 +52,7 @@ test.describe('Staged Payment', () => { test('Permission decision method', async () => { await stagedPayment.fillForm({ - title: 'Test Staged Payment with Permission Decision Method', + title: 'Test Staged Payment', team: 'General', recipient: recipientAddress, decisionMethod: 'Permissions', @@ -173,9 +173,9 @@ test.describe('Staged Payment', () => { await verifyCompletedStagedPayment(stagedPayment); }); - test('Reputation decision method', async () => { + test('Reputation decision method | Voting', async () => { await stagedPayment.fillForm({ - title: 'Test Staged Payment with Reputation Decision Method', + title: 'Test Staged Payment', team: 'General', recipient: recipientAddress, decisionMethod: 'Permissions', diff --git a/playwright/models/manage-colony-funds.ts b/playwright/models/manage-colony-funds.ts new file mode 100644 index 0000000000..6ba1f90a28 --- /dev/null +++ b/playwright/models/manage-colony-funds.ts @@ -0,0 +1,301 @@ +import type { Page, Locator } from '@playwright/test'; + +export class ManageColonyFunds { + static readonly validationMessages = { + common: { + amount: { + isRequired: 'required field', + mustBeGreaterThanZero: 'Amount must be greater than zero', + }, + title: { + maxLengthExceeded: 'Title must not exceed 60 characters', + isRequired: 'Title is required', + }, + }, + mintTokens: { + allRequiredFields: [ + 'required field', + 'decisionMethod must be defined', + 'Title is required', + ], + }, + fundsTransfer: { + allRequiredFields: [ + 'from must be a `number` type, but the final value was: `NaN` (cast from the value `""`).', + 'decisionMethod must be defined', + 'to is a required field', + 'Amount must be greater than zero', + 'Title is required', + ], + team: { + sameTeam: 'Cannot move to same team pot.', + }, + amount: { + notEnoughFunds: + 'Not enough funds to cover the payment and network fees', + }, + token: { + locked: + 'This token is locked and is not able to be used for payments. Check with the token creator for details.', + }, + }, + }; + + drawer: Locator; + + form: Locator; + + sidebar: Locator; + + selectDropdown: Locator; + + decisionMethodDropdown: Locator; + + stepper: Locator; + + closeButton: Locator; + + mintTokensButton: Locator; + + completedAction: Locator; + + confirmationDialog: Locator; + + supportButton: Locator; + + opposeButton: Locator; + + submitVoteButton: Locator; + + revealVoteButton: Locator; + + finalizeButton: Locator; + + claimButton: Locator; + + transferFundsButton: Locator; + + constructor(private page: Page) { + this.drawer = page.getByTestId('action-drawer'); + this.form = page.getByTestId('action-form'); + this.selectDropdown = page.getByTestId('search-select-menu'); + this.decisionMethodDropdown = page + .getByRole('list') + .filter({ hasText: /Available decision methods/i }); + this.sidebar = page.getByTestId('colony-page-sidebar'); + this.stepper = page.getByTestId('stepper'); + this.closeButton = page.getByRole('button', { name: /close the modal/i }); + this.mintTokensButton = page + .getByRole('button', { + name: 'Mint tokens', + }) + .nth(1); + this.completedAction = page.getByTestId('completed-action'); + this.confirmationDialog = this.page + .getByRole('dialog') + .filter({ hasText: 'Do you wish to cancel the action creation?' }); + this.supportButton = this.stepper.getByText('Support', { exact: true }); + this.opposeButton = this.stepper.getByText('Oppose', { exact: true }); + this.submitVoteButton = this.stepper.getByRole('button', { + name: 'Submit vote', + }); + this.revealVoteButton = this.stepper.getByRole('button', { + name: 'Reveal vote', + }); + this.finalizeButton = this.completedAction + .getByRole('button', { + name: 'Finalize', + }) + .last(); + this.claimButton = this.stepper + .getByRole('button', { + name: 'Claim', + }) + .last(); + this.transferFundsButton = this.form + .getByRole('button', { + name: 'Transfer funds', + }) + .last(); + } + + async open(motionTitle: 'Mint tokens' | 'Transfer funds') { + await this.sidebar.getByLabel('Start the manage colony action').click(); + await this.drawer.waitFor({ state: 'visible' }); + await this.drawer + .getByRole('button', { + name: motionTitle, + }) + .click(); + await this.form.waitFor({ state: 'visible' }); + await this.drawer + .getByTestId('action-sidebar-description') + .waitFor({ state: 'visible' }); + } + + async close() { + await this.closeButton.click(); + + const dialog = this.confirmationDialog; + if (await dialog.isVisible()) { + await dialog + .getByRole('button', { name: 'Yes, cancel the action' }) + .click(); + } + } + + async getBalance( + colonyName: string, + balances: { + token: string; + team?: string; + }[], + ) { + await this.page.goto(`/${colonyName}/balances`); + + const result: string[] = []; + + for (const { team, token } of balances) { + if (team) { + await this.page.getByRole('button', { name: team }).click(); + await this.page.waitForURL(/\?team=/); + } + + const value = await this.page + .getByRole('table') + .locator('tr td') + .filter({ hasText: token }) + .last() + .textContent(); + result.push(value?.split(' ')[0] ?? ''); + } + await this.page.getByRole('button', { name: 'All teams' }).click(); + + return result; + } + + async waitForTransaction() { + await this.waitForPending(); + await this.page.waitForURL(/\?tx=/); + await this.page + .getByTestId('loading-skeleton') + .last() + .waitFor({ state: 'hidden' }); + } + + async waitForPending() { + await this.page + .getByRole('button', { name: 'Pending' }) + .last() + .waitFor({ state: 'visible' }); + + await this.page + .getByRole('button', { name: 'Pending' }) + .last() + .waitFor({ state: 'hidden' }); + } + + async setDecisionMethod(decisionMethod: 'Permissions' | 'Reputation') { + await this.form.getByRole('button', { name: 'Select method' }).click(); + + await this.decisionMethodDropdown + .getByRole('button', { name: decisionMethod }) + .click(); + } + + async fillMintTokensForm({ + amount, + decisionMethod, + title, + }: { + amount?: string; + decisionMethod?: 'Permissions' | 'Reputation'; + title?: string; + }) { + if (title) { + await this.form.getByPlaceholder('Enter title').fill(title); + } + if (amount) { + await this.form.getByPlaceholder('Enter amount').fill(amount); + } + + if (decisionMethod) { + await this.setDecisionMethod(decisionMethod); + } + } + + async fillTransferFundsForm({ + amount, + title, + from, + to, + decisionMethod, + tokenSymbol, + }: { + amount?: string; + title?: string; + from?: string; + to?: string; + decisionMethod?: 'Permissions' | 'Reputation'; + tokenSymbol?: string; + }) { + if (title) { + await this.form.getByPlaceholder('Enter title').fill(title); + } + if (amount) { + await this.form.getByPlaceholder('Enter amount').fill(amount); + } + if (from) { + await this.form + .getByRole('button', { name: 'Select team' }) + .first() + .click(); + await this.selectDropdown.getByRole('button', { name: from }).click(); + } + if (to) { + await this.form + .getByRole('button', { name: 'Select team' }) + .last() + .click(); + await this.selectDropdown.getByRole('button', { name: to }).click(); + } + + if (decisionMethod) { + await this.setDecisionMethod(decisionMethod); + } + if (tokenSymbol) { + await this.form.getByRole('button', { name: 'Select token' }).click(); + await this.page + .getByTestId('token-list-item') + .getByText(tokenSymbol) + .first() + .click(); + } + } + + async support(stakeAmount?: string) { + await this.supportButton.click(); + await this.stepper.getByLabel('Stake amount').waitFor({ state: 'visible' }); + await this.stepper + .getByRole('button', { name: 'Max' }) + .waitFor({ state: 'visible' }); + if (stakeAmount) { + await this.stepper.getByLabel('Stake amount').fill(stakeAmount); + } else { + await this.stepper.getByRole('button', { name: 'Max' }).click(); + } + await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); + await this.stepper.getByRole('button', { name: 'Stake' }).click(); + } + + async oppose(stakeAmount?: string) { + await this.opposeButton.click(); + if (stakeAmount) { + await this.stepper.getByRole('textbox').fill(stakeAmount); + } else { + await this.stepper.getByRole('button', { name: 'Max' }).click(); + } + await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); + await this.stepper.getByRole('button', { name: 'Stake' }).click(); + } +} From c31cce7b254df0d57e39ae5519cd29862ad72b1b Mon Sep 17 00:00:00 2001 From: Olha Pidlisnyuk Date: Wed, 22 Jan 2025 20:01:48 +0100 Subject: [PATCH 2/4] test: manage colony tokens feature --- playwright/e2e/manage-colony-funds.spec.ts | 218 +++++++++++++++++- playwright/models/manage-colony-funds.ts | 87 ++++++- .../TeamColorField/TeamColorField.tsx | 1 + .../partials/TokenSelect/TokenSelect.tsx | 2 +- .../TokenSearchSelect/TokenSearchSelect.tsx | 1 + 5 files changed, 297 insertions(+), 12 deletions(-) diff --git a/playwright/e2e/manage-colony-funds.spec.ts b/playwright/e2e/manage-colony-funds.spec.ts index f73670eb11..9474c6ebbe 100644 --- a/playwright/e2e/manage-colony-funds.spec.ts +++ b/playwright/e2e/manage-colony-funds.spec.ts @@ -13,6 +13,7 @@ test.describe('Manage Colony Funds', () => { let context: BrowserContext; let manageColonyFunds: ManageColonyFunds; let extensions: Extensions; + test.beforeAll(async ({ browser, baseURL }) => { context = await browser.newContext(); page = await context.newPage(); @@ -97,11 +98,10 @@ test.describe('Manage Colony Funds', () => { const initial = parseFloat(currentBalance); const after = parseFloat(newBalance); - await expect(after).toBeGreaterThan(initial); + expect(after).toBeGreaterThan(initial); }); - // eslint-disable-next-line playwright/no-skipped-test - test.skip('Reputation decision method', async () => { + test('Reputation decision method', async () => { await manageColonyFunds.open('Mint tokens'); await manageColonyFunds.fillMintTokensForm({ @@ -174,6 +174,8 @@ test.describe('Manage Colony Funds', () => { ManageColonyFunds.validationMessages.common.title.maxLengthExceeded, ), ).toBeVisible(); + + await manageColonyFunds.close(); }); }); @@ -230,8 +232,7 @@ test.describe('Manage Colony Funds', () => { ); }); - // eslint-disable-next-line playwright/no-skipped-test - test.skip('Reputation decision method', async () => { + test('Reputation decision method', async () => { await manageColonyFunds.open('Transfer funds'); await manageColonyFunds.fillTransferFundsForm({ @@ -352,6 +353,213 @@ test.describe('Manage Colony Funds', () => { ManageColonyFunds.validationMessages.fundsTransfer.token.locked, ), ).toBeVisible(); + + await manageColonyFunds.close(); + }); + }); + + test.describe('Manage tokens', () => { + test.afterAll(async () => { + await manageColonyFunds.removeTokens(); + }); + test('Add/Remove tokens | Permissions decision method', async () => { + await manageColonyFunds.open('Manage tokens'); + await manageColonyFunds.setTitle('Manage tokens test'); + await manageColonyFunds.setDecisionMethod('Permissions'); + + const approvedTokens = await manageColonyFunds.tokenSelector.count(); + await expect( + manageColonyFunds.form.getByRole('heading', { + name: 'Approved tokens', + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.manageTokensTable.getByText('Ether', { exact: true }), + ).toBeVisible(); + + await expect( + manageColonyFunds.manageTokensTable.getByText('ETH', { exact: true }), + ).toBeVisible(); + + await expect( + manageColonyFunds.manageTokensTable.getByText('Space Credits', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.manageTokensTable.getByText('CREDS', { + exact: true, + }), + ).toBeVisible(); + + await manageColonyFunds.form + .getByRole('button', { + name: 'Add token', + }) + .click(); + + await manageColonyFunds.manageTokensTable + .getByLabel('Select token or enter token') + .click(); + + await manageColonyFunds.tokenSearchMenu + .getByRole('button', { + name: 'Avatar of token', + }) + .first() + .click(); + + await expect(manageColonyFunds.tokenSelector).toHaveCount( + approvedTokens + 1, + ); + + await expect( + manageColonyFunds.manageTokensTable.getByText('New'), + ).toBeVisible(); + + await manageColonyFunds.updateTokensButton.click(); + + await manageColonyFunds.waitForTransaction(); + + await expect( + manageColonyFunds.completedAction.getByRole('heading', { + name: 'Manage tokens test', + }), + ).toBeVisible(); + + await expect(manageColonyFunds.manageTokensTable).toBeVisible(); + await expect( + manageColonyFunds.completedAction.getByText('Manage tokens', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText('Permissions', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText('New', { + exact: true, + }), + ).toBeVisible(); + + const newTokenSymbol = ( + await manageColonyFunds.manageTokensTable + .getByRole('cell', { name: 'New' }) + .textContent() + ) + ?.replace('New', '') + ?.slice(0, 5); + + await manageColonyFunds.verifyTokenAdded({ + colonyName: 'planex', + token: newTokenSymbol ?? '', + }); + }); + + test('Reputation decision method', async () => { + await manageColonyFunds.open('Manage tokens'); + await manageColonyFunds.setTitle('Manage tokens test'); + await manageColonyFunds.setDecisionMethod('Reputation'); + + await manageColonyFunds.form + .getByRole('button', { + name: 'Add token', + }) + .click(); + + await manageColonyFunds.manageTokensTable + .getByLabel('Select token or enter token') + .click(); + + await manageColonyFunds.tokenSearchMenu + .getByRole('button', { + name: 'Avatar of token', + }) + .first() + .click(); + + await manageColonyFunds.updateTokensButton.click(); + + await manageColonyFunds.waitForTransaction(); + + await expect( + manageColonyFunds.completedAction.getByText('Manage tokens', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.stepper.getByText(/Total stake required: \d+/), + ).toBeVisible(); + + await manageColonyFunds.support(); + + await expect(manageColonyFunds.stepper).toHaveText(/100% Supported/); + + await manageColonyFunds.oppose(); + + await expect(manageColonyFunds.stepper).toHaveText(/100% Opposed/); + + await manageColonyFunds.stepper + .getByText(/Vote to support or oppose?/) + .waitFor(); + await manageColonyFunds.supportButton.click(); + await manageColonyFunds.submitVoteButton.click(); + await manageColonyFunds.revealVoteButton.click(); + await manageColonyFunds.stepper.getByText('vote revealed').waitFor(); + await expect( + manageColonyFunds.stepper.getByText( + 'Finalize to execute the agreed transaction', + ), + ).toBeVisible(); + await manageColonyFunds.finalizeButton.click(); + + await manageColonyFunds.waitForPending(); + await manageColonyFunds.claimButton.click(); + + await manageColonyFunds.completedAction + .getByRole('heading', { name: 'Your overview Claimed' }) + .waitFor(); + + await expect(manageColonyFunds.claimButton).toBeHidden(); + }); + + test('Form validation', async () => { + await manageColonyFunds.open('Manage tokens'); + await manageColonyFunds.updateTokensButton.click(); + + for (const message of ManageColonyFunds.validationMessages.manageTokens + .allRequiredFields) { + await expect(manageColonyFunds.drawer.getByText(message)).toBeVisible(); + } + + await manageColonyFunds.form + .getByRole('button', { + name: 'Add token', + }) + .click(); + await manageColonyFunds.manageTokensTable + .getByLabel('Select token or enter token') + .click(); + + await manageColonyFunds.tokenSearchMenu + .getByPlaceholder('Select token or enter token address') + .fill('0x0000000000000000000000000000000000000000'); + + await expect( + manageColonyFunds.tokenSearchMenu.getByText( + ManageColonyFunds.validationMessages.manageTokens.token + .isAlreadyAdded, + ), + ).toBeVisible(); + + await manageColonyFunds.close(); }); }); }); diff --git a/playwright/models/manage-colony-funds.ts b/playwright/models/manage-colony-funds.ts index 6ba1f90a28..3a6dd03e3a 100644 --- a/playwright/models/manage-colony-funds.ts +++ b/playwright/models/manage-colony-funds.ts @@ -1,4 +1,4 @@ -import type { Page, Locator } from '@playwright/test'; +import { type Page, type Locator, expect } from '@playwright/test'; export class ManageColonyFunds { static readonly validationMessages = { @@ -39,6 +39,16 @@ export class ManageColonyFunds { 'This token is locked and is not able to be used for payments. Check with the token creator for details.', }, }, + manageTokens: { + allRequiredFields: [ + 'No changes in the table', + 'Title is required', + "Decision Method can't be empty", + ], + token: { + isAlreadyAdded: 'This token is already on colony tokens list', + }, + }, }; drawer: Locator; @@ -75,6 +85,14 @@ export class ManageColonyFunds { transferFundsButton: Locator; + updateTokensButton: Locator; + + manageTokensTable: Locator; + + tokenSelector: Locator; + + tokenSearchMenu: Locator; + constructor(private page: Page) { this.drawer = page.getByTestId('action-drawer'); this.form = page.getByTestId('action-form'); @@ -117,9 +135,21 @@ export class ManageColonyFunds { name: 'Transfer funds', }) .last(); + this.updateTokensButton = this.form + .getByRole('button', { + name: 'Update tokens', + }) + .last(); + this.manageTokensTable = this.drawer.getByRole('table'); + this.tokenSelector = this.page.getByTestId('token-select'); + this.tokenSearchMenu = this.page.getByTestId('token-search-select'); } - async open(motionTitle: 'Mint tokens' | 'Transfer funds') { + async setTitle(title: string) { + await this.form.getByPlaceholder('Enter title').fill(title); + } + + async open(motionTitle: 'Mint tokens' | 'Transfer funds' | 'Manage tokens') { await this.sidebar.getByLabel('Start the manage colony action').click(); await this.drawer.waitFor({ state: 'visible' }); await this.drawer @@ -213,7 +243,7 @@ export class ManageColonyFunds { title?: string; }) { if (title) { - await this.form.getByPlaceholder('Enter title').fill(title); + await this.setTitle(title); } if (amount) { await this.form.getByPlaceholder('Enter amount').fill(amount); @@ -240,7 +270,7 @@ export class ManageColonyFunds { tokenSymbol?: string; }) { if (title) { - await this.form.getByPlaceholder('Enter title').fill(title); + await this.setTitle(title); } if (amount) { await this.form.getByPlaceholder('Enter amount').fill(amount); @@ -284,7 +314,7 @@ export class ManageColonyFunds { } else { await this.stepper.getByRole('button', { name: 'Max' }).click(); } - await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); + await this.stepper.getByRole('button', { name: 'Stake' }).click(); } @@ -295,7 +325,52 @@ export class ManageColonyFunds { } else { await this.stepper.getByRole('button', { name: 'Max' }).click(); } - await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); + await this.stepper.getByRole('button', { name: 'Stake' }).click(); } + + async verifyTokenAdded({ + colonyName, + token, + }: { + colonyName: string; + token: string; + }) { + await this.page.goto(`/${colonyName}/balances`); + + await this.page + .getByRole('heading', { name: 'Colony token balance' }) + .waitFor({ state: 'visible' }); + await this.page.getByRole('table').waitFor({ state: 'visible' }); + await this.page + .getByRole('table') + .getByText(token) + .first() + .waitFor({ state: 'visible' }); + } + + async removeTokens() { + await this.open('Manage tokens'); + await this.setTitle('Remove tokens'); + await this.setDecisionMethod('Permissions'); + const allAddedTokens = await this.manageTokensTable + .getByLabel('Open menu') + .all(); + + if (allAddedTokens.length === 0) { + return; + } + + for (const token of allAddedTokens) { + await token.click(); + await this.page.getByRole('button', { name: 'Remove row' }).click(); + } + + await this.updateTokensButton.click(); + await this.waitForTransaction(); + + await expect(this.manageTokensTable.getByText('Removed')).toHaveCount( + allAddedTokens.length, + ); + } } diff --git a/src/components/v5/common/ActionSidebar/partials/TeamColorField/TeamColorField.tsx b/src/components/v5/common/ActionSidebar/partials/TeamColorField/TeamColorField.tsx index d65cf71f89..7947600cc7 100644 --- a/src/components/v5/common/ActionSidebar/partials/TeamColorField/TeamColorField.tsx +++ b/src/components/v5/common/ActionSidebar/partials/TeamColorField/TeamColorField.tsx @@ -85,6 +85,7 @@ const TeamColorField: FC = ({ name, disabled }) => { className="absolute z-sidebar w-full max-w-[calc(100%-2.25rem)] px-0 py-6 sm:w-auto sm:max-w-none sm:px-6" hasShadow rounded="s" + testId="team-color-select-menu" > = ({ }, [isTokenSelectVisible]); return ( -
+
{readonly || readOnlyProp ? (
Date: Thu, 23 Jan 2025 11:35:30 +0100 Subject: [PATCH 3/4] test: e2e tests for manage colony teams motions --- playwright/e2e/manage-colony-teams.spec.ts | 262 ++++++++++++++++ playwright/models/manage-colony-teams.ts | 290 ++++++++++++++++++ .../TeamColorField/TeamColorField.tsx | 1 + src/components/v5/common/TeamColorBadge.tsx | 2 +- 4 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 playwright/e2e/manage-colony-teams.spec.ts create mode 100644 playwright/models/manage-colony-teams.ts diff --git a/playwright/e2e/manage-colony-teams.spec.ts b/playwright/e2e/manage-colony-teams.spec.ts new file mode 100644 index 0000000000..7da7dcbeeb --- /dev/null +++ b/playwright/e2e/manage-colony-teams.spec.ts @@ -0,0 +1,262 @@ +import { type BrowserContext, expect, type Page, test } from '@playwright/test'; + +import { Extensions } from '../models/extensions.ts'; +import { ManageColonyTeams } from '../models/manage-colony-teams.ts'; +import { + generateRandomString, + setCookieConsent, + signInAndNavigateToColony, +} from '../utils/common.ts'; + +test.describe.configure({ mode: 'serial' }); +test.describe('Manage Colony Teams', () => { + let page: Page; + let context: BrowserContext; + let manageColonyTeams: ManageColonyTeams; + let extensions: Extensions; + + test.beforeAll(async ({ browser, baseURL }) => { + context = await browser.newContext(); + page = await context.newPage(); + manageColonyTeams = new ManageColonyTeams(page); + extensions = new Extensions(page); + await setCookieConsent(context, baseURL); + await signInAndNavigateToColony(page, { + colonyUrl: '/planex', + wallet: /dev wallet 1$/i, + }); + await extensions.enableReputationWeightedExtension({ + colonyPath: '/planex', + }); + }); + + test.afterAll(async () => { + await context?.close(); + }); + test('Create new team | Permissions decision method', async () => { + const teamName = `Test team ${generateRandomString()}`; + await manageColonyTeams.open('Create new team'); + await manageColonyTeams.fillForm({ + title: 'Test team', + purpose: 'This is a test purpose', + name: teamName, + decisionMethod: 'Permissions', + }); + const colorIndex = Math.floor(Math.random() * 16); + const selectedColor = await manageColonyTeams.setColor(colorIndex); + const teamNameColor = await manageColonyTeams.getTeamNameColor(teamName); + + expect(selectedColor).toBe(teamNameColor); + await manageColonyTeams.createTeamButton.click(); + + await manageColonyTeams.waitForTransaction(); + + await expect( + manageColonyTeams.completedAction.getByText( + 'Member used permissions to create this action', + ), + ).toBeVisible(); + + await expect( + manageColonyTeams.completedAction.getByText(teamName, { exact: true }), + ).toHaveCount(2); + + await expect( + manageColonyTeams.completedAction.getByText('Permissions', { + exact: true, + }), + ).toBeVisible(); + + const { teamBadgeColor, teamNameTextColor } = + await manageColonyTeams.getTeamColorBadgeColor(teamName); + + expect(teamBadgeColor).toBe(selectedColor); + expect(teamNameTextColor).toBe(teamNameColor); + + await manageColonyTeams.verifyTeamCreated({ + teamName, + colonyName: 'planex', + }); + }); + + test('Create new team | Reputation decision method', async () => { + const teamName = `Test team ${generateRandomString()}`; + await manageColonyTeams.open('Create new team'); + const colorIndex = Math.floor(Math.random() * 16); + await manageColonyTeams.fillForm({ + title: 'Test team', + purpose: 'This is a test purpose', + name: teamName, + colorIndex, + decisionMethod: 'Reputation', + }); + + await manageColonyTeams.createTeamButton.click(); + + await manageColonyTeams.waitForTransaction(); + + await expect(manageColonyTeams.stepper).toBeVisible(); + await expect(manageColonyTeams.completedAction).toBeVisible(); + await expect( + manageColonyTeams.completedAction.getByText( + `Create new team ${teamName} by leela`, + ), + ).toBeVisible(); + await expect( + manageColonyTeams.stepper.getByText(/Total stake required: \d+/), + ).toBeVisible(); + + await manageColonyTeams.support(); + + await expect(manageColonyTeams.stepper).toHaveText(/100% Supported/); + + await manageColonyTeams.oppose(); + + await expect(manageColonyTeams.stepper).toHaveText(/100% Opposed/); + + await manageColonyTeams.stepper + .getByText(/Vote to support or oppose?/) + .waitFor(); + await manageColonyTeams.supportButton.click(); + await manageColonyTeams.submitVoteButton.click(); + await expect( + manageColonyTeams.stepper.getByText(/You voted:Support/), + ).toBeVisible(); + + await expect( + manageColonyTeams.stepper.getByText(/^0 votes revealed$/), + ).toBeVisible(); + + await manageColonyTeams.revealVoteButton.click(); + + await manageColonyTeams.stepper + .getByRole('button', { + name: 'Support wins', + }) + .waitFor(); + + await expect( + manageColonyTeams.stepper.getByText( + 'Finalize to execute the agreed transaction', + ), + ).toBeVisible(); + await manageColonyTeams.finalizeButton.click(); + + await manageColonyTeams.waitForPending(); + await manageColonyTeams.claimButton.click(); + await manageColonyTeams.waitForPending(); + + await manageColonyTeams.stepper + .getByRole('heading', { + name: 'Your overview Claimed', + }) + .waitFor(); + }); + + test('Form validation', async () => { + await manageColonyTeams.open('Create new team'); + await manageColonyTeams.createTeamButton.click(); + + // Verify all required field validation messages + for (const message of ManageColonyTeams.validationMessages.createTeam + .allRequiredFields) { + await expect(manageColonyTeams.drawer.getByText(message)).toBeVisible(); + } + + // Test title max length validation + await manageColonyTeams.fillForm({ + title: + 'This is a test title that exceeds the maximum character limit of sixty characters long.', + }); + + await manageColonyTeams.createTeamButton.click(); + + await expect( + manageColonyTeams.drawer.getByText( + ManageColonyTeams.validationMessages.common.title.maxLengthExceeded, + ), + ).toBeVisible(); + + // Verify long teamName value is truncated + await manageColonyTeams.fillForm({ + name: 'This is a test title that exceeds the maximum character limit of 20 characters long.', + }); + await expect( + manageColonyTeams.form.getByPlaceholder('Enter team name'), + ).toHaveValue('This is a test title'); + + await manageColonyTeams.fillForm({ + purpose: 's'.repeat(121), + }); + await expect( + manageColonyTeams.drawer.getByText( + ManageColonyTeams.validationMessages.common.purpose.maxLengthExceeded, + ), + ).toBeVisible(); + + await manageColonyTeams.close(); + }); + + test('Edit team | Permissions decision method', async () => { + // Create a team to edit + const initialTeamName = `Test team ${generateRandomString()}`; + await manageColonyTeams.open('Create new team'); + await manageColonyTeams.fillForm({ + title: 'Create test team', + purpose: 'Initial purpose', + name: initialTeamName, + decisionMethod: 'Permissions', + }); + const initialColorIndex = 2; + const initialColor = await manageColonyTeams.setColor(initialColorIndex); + await manageColonyTeams.createTeamButton.click(); + await manageColonyTeams.waitForTransaction(); + // Edit team + await manageColonyTeams.open('Edit a team'); + + await manageColonyTeams.selectTeam(initialTeamName); + + // Update team details + const updatedTeamName = `Updated team ${generateRandomString()}`; + const updatedPurpose = 'Updated team purpose'; + await manageColonyTeams.fillForm({ + title: 'Edit team details', + name: updatedTeamName, + purpose: updatedPurpose, + decisionMethod: 'Permissions', + }); + + const newColorIndex = 3; + const updatedColor = await manageColonyTeams.setColor(newColorIndex); + + await manageColonyTeams.editTeamButton.click(); + await manageColonyTeams.waitForTransaction(); + + // Verify the completed action + await expect( + manageColonyTeams.completedAction.getByText( + 'Member used permissions to create this action', + ), + ).toBeVisible(); + + await expect( + manageColonyTeams.completedAction.getByText(updatedTeamName, { + exact: true, + }), + ).toHaveCount(2); + + // Verify color changes + const { teamBadgeColor } = + await manageColonyTeams.getTeamColorBadgeColor(updatedTeamName); + + expect(teamBadgeColor).toBe(updatedColor); + expect(teamBadgeColor).not.toBe(initialColor); + + // Verify the purpose was updated + await expect( + manageColonyTeams.completedAction.getByText(updatedPurpose, { + exact: true, + }), + ).toBeVisible(); + }); +}); diff --git a/playwright/models/manage-colony-teams.ts b/playwright/models/manage-colony-teams.ts new file mode 100644 index 0000000000..da949fab7e --- /dev/null +++ b/playwright/models/manage-colony-teams.ts @@ -0,0 +1,290 @@ +import { type Page, type Locator, expect } from '@playwright/test'; + +export class ManageColonyTeams { + static readonly validationMessages = { + createTeam: { + allRequiredFields: [ + 'decisionMethod must be defined', + 'domainColor must be defined', + 'Team name required.', + 'Title is required', + ], + }, + common: { + title: { + maxLengthExceeded: 'Title must not exceed 60 characters', + }, + purpose: { + maxLengthExceeded: 'domainPurpose must be at most 120 characters', + }, + }, + }; + + drawer: Locator; + + form: Locator; + + sidebar: Locator; + + selectDropdown: Locator; + + decisionMethodDropdown: Locator; + + createTeamButton: Locator; + + editTeamButton: Locator; + + stepper: Locator; + + closeButton: Locator; + + completedAction: Locator; + + confirmationDialog: Locator; + + supportButton: Locator; + + opposeButton: Locator; + + submitVoteButton: Locator; + + revealVoteButton: Locator; + + finalizeButton: Locator; + + claimButton: Locator; + + teamColorSelectButton: Locator; + + teamColorSelectMenu: Locator; + + teamColorBadge: Locator; + + constructor(private page: Page) { + this.drawer = page.getByTestId('action-drawer'); + this.form = page.getByTestId('action-form'); + this.selectDropdown = page.getByTestId('search-select-menu'); + this.decisionMethodDropdown = page + .getByRole('list') + .filter({ hasText: /Available decision methods/i }); + this.sidebar = page.getByTestId('colony-page-sidebar'); + this.createTeamButton = this.form.getByRole('button', { + name: 'Create team', + exact: true, + }); + this.editTeamButton = this.form.getByRole('button', { + name: 'Edit team', + exact: true, + }); + this.stepper = page.getByTestId('stepper'); + this.closeButton = page.getByRole('button', { name: /close the modal/i }); + this.completedAction = page.getByTestId('completed-action'); + this.confirmationDialog = this.page + .getByRole('dialog') + .filter({ hasText: 'Do you wish to cancel the action creation?' }); + this.supportButton = this.stepper.getByText('Support', { exact: true }); + this.opposeButton = this.stepper.getByText('Oppose', { exact: true }); + this.submitVoteButton = this.stepper.getByRole('button', { + name: 'Submit vote', + }); + this.revealVoteButton = this.stepper.getByRole('button', { + name: 'Reveal vote', + }); + this.finalizeButton = this.completedAction + .getByRole('button', { + name: 'Finalize', + }) + .last(); + this.claimButton = this.stepper + .getByRole('button', { + name: 'Claim', + }) + .last(); + this.teamColorSelectButton = this.page.getByTestId( + 'team-color-select-button', + ); + this.teamColorSelectMenu = this.page.getByTestId('team-color-select-menu'); + this.teamColorBadge = this.page.getByTestId('team-color-badge'); + } + + async setTitle(title: string) { + await this.form.getByPlaceholder('Enter title').fill(title); + } + + async open(motionTitle: 'Create new team' | 'Edit a team') { + await this.sidebar.getByLabel('Start the manage colony action').click(); + await this.drawer.waitFor({ state: 'visible' }); + await this.drawer + .getByRole('button', { + name: motionTitle, + }) + .click(); + await this.form.waitFor({ state: 'visible' }); + await this.drawer + .getByTestId('action-sidebar-description') + .waitFor({ state: 'visible' }); + } + + async setColor(colorIndex: number): Promise { + await this.teamColorSelectButton.click(); + await this.teamColorSelectMenu.waitFor({ state: 'visible' }); + const selectedColor = await this.teamColorSelectMenu + .getByRole('listitem') + .nth(colorIndex) + .getByRole('button') + .locator('div div') + .evaluate((el) => { + return getComputedStyle(el).backgroundColor; + }); + await this.teamColorSelectMenu + .getByRole('listitem') + .nth(colorIndex) + .getByRole('button') + .click(); + + await this.teamColorSelectMenu.waitFor({ state: 'hidden' }); + return selectedColor; + } + + async getTeamNameColor(teamName: string): Promise { + const teamNameElement = this.form + .getByRole('button') + .getByText(teamName, { exact: true }); + return teamNameElement.evaluate((el) => { + return window.getComputedStyle(el).color; + }); + } + + async getTeamColorBadgeColor(teamName: string) { + const teamBadgeColor = await this.teamColorBadge.evaluate((el) => { + return window.getComputedStyle(el.firstChild as Element).backgroundColor; + }); + const teamNameTextColor = await this.teamColorBadge + .getByText(teamName) + .evaluate((el) => { + return window.getComputedStyle(el).color; + }); + return { teamBadgeColor, teamNameTextColor }; + } + + async close() { + await this.closeButton.click(); + + const dialog = this.confirmationDialog; + if (await dialog.isVisible()) { + await dialog + .getByRole('button', { name: 'Yes, cancel the action' }) + .click(); + } + } + + async waitForTransaction() { + await this.waitForPending(); + await this.page.waitForURL(/\?tx=/); + await this.page + .getByTestId('loading-skeleton') + .last() + .waitFor({ state: 'hidden' }); + } + + async waitForPending() { + await this.page + .getByRole('button', { name: 'Pending' }) + .last() + .waitFor({ state: 'visible' }); + + await this.page + .getByRole('button', { name: 'Pending' }) + .last() + .waitFor({ state: 'hidden' }); + } + + async setDecisionMethod(decisionMethod: 'Permissions' | 'Reputation') { + await this.form.getByRole('button', { name: 'Select method' }).click(); + + await this.decisionMethodDropdown + .getByRole('button', { name: decisionMethod }) + .click(); + } + + async fillForm({ + title, + name, + colorIndex, + purpose, + decisionMethod, + }: { + title?: string; + name?: string; + colorIndex?: number; + purpose?: string; + decisionMethod?: 'Permissions' | 'Reputation'; + }) { + if (title) { + await this.setTitle(title); + } + + if (name) { + await this.form.getByPlaceholder('Enter team name').fill(name); + } + + if (purpose) { + await this.form.getByPlaceholder('Enter short description').fill(purpose); + } + + if (colorIndex !== undefined) { + await this.setColor(colorIndex); + } + + if (decisionMethod) { + await this.setDecisionMethod(decisionMethod); + } + } + + async selectTeam(teamName: string) { + await this.form + .getByRole('button', { name: 'Select team', exact: true }) + .click(); + await this.selectDropdown.getByText(teamName).click(); + } + + async verifyTeamCreated({ + teamName, + colonyName, + }: { + teamName: string; + colonyName: string; + }) { + await this.page.goto(`/${colonyName}/teams`); + await this.page.getByText('Loading').waitFor({ state: 'hidden' }); + await expect( + this.page.getByRole('heading', { name: teamName, exact: true }), + ).toBeVisible(); + } + + async support(stakeAmount?: string) { + await this.supportButton.click(); + await this.stepper.getByLabel('Stake amount').waitFor({ state: 'visible' }); + await this.stepper + .getByRole('button', { name: 'Max' }) + .waitFor({ state: 'visible' }); + if (stakeAmount) { + await this.stepper.getByLabel('Stake amount').fill(stakeAmount); + } else { + await this.stepper.getByRole('button', { name: 'Max' }).click(); + } + await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); + await this.stepper.getByRole('button', { name: 'Stake' }).click(); + } + + async oppose(stakeAmount?: string) { + await this.opposeButton.click(); + if (stakeAmount) { + await this.stepper.getByRole('textbox').fill(stakeAmount); + } else { + await this.stepper.getByRole('button', { name: 'Max' }).click(); + } + await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); + await this.stepper.getByRole('button', { name: 'Stake' }).click(); + } +} diff --git a/src/components/v5/common/ActionSidebar/partials/TeamColorField/TeamColorField.tsx b/src/components/v5/common/ActionSidebar/partials/TeamColorField/TeamColorField.tsx index 7947600cc7..57cae51681 100644 --- a/src/components/v5/common/ActionSidebar/partials/TeamColorField/TeamColorField.tsx +++ b/src/components/v5/common/ActionSidebar/partials/TeamColorField/TeamColorField.tsx @@ -63,6 +63,7 @@ const TeamColorField: FC = ({ name, disabled }) => { })} onClick={toggleDecisionSelect} disabled={disabled} + data-testid="team-color-select-button" > {field.value ? ( { ); return ( -
+
Date: Mon, 27 Jan 2025 12:14:08 +0100 Subject: [PATCH 4/4] test: e2e tests for manage reputation feature --- playwright.config.ts | 2 +- playwright/e2e/manage-colony-funds.spec.ts | 338 ++++++------------ .../e2e/manage-colony-reputation.spec.ts | 232 ++++++++++++ playwright/e2e/manage-colony-teams.spec.ts | 107 ++---- playwright/models/manage-colony-funds.ts | 46 +-- playwright/models/manage-colony-reputation.ts | 336 +++++++++++++++++ playwright/models/manage-colony-teams.ts | 14 +- playwright/models/reputation-flow.ts | 119 ++++++ .../common/Extensions/UserHub/UserHub.tsx | 3 +- .../BalanceTab/partials/TotalReputation.tsx | 1 + .../UserHubButton/UserHubButton.tsx | 1 + src/components/shared/Numeral/NumeralBase.tsx | 3 +- src/components/shared/Numeral/types.ts | 1 + src/components/v5/shared/Button/Button.tsx | 2 + src/components/v5/shared/Button/types.ts | 1 + 15 files changed, 867 insertions(+), 339 deletions(-) create mode 100644 playwright/e2e/manage-colony-reputation.spec.ts create mode 100644 playwright/models/manage-colony-reputation.ts create mode 100644 playwright/models/reputation-flow.ts diff --git a/playwright.config.ts b/playwright.config.ts index 08a97b762c..c76a82ebb2 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -8,7 +8,7 @@ dotenv.config({ path: path.resolve(__dirname, '.env') }); * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - fullyParallel: true, + fullyParallel: false, testDir: './playwright/e2e', /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, diff --git a/playwright/e2e/manage-colony-funds.spec.ts b/playwright/e2e/manage-colony-funds.spec.ts index 9474c6ebbe..597da42201 100644 --- a/playwright/e2e/manage-colony-funds.spec.ts +++ b/playwright/e2e/manage-colony-funds.spec.ts @@ -1,4 +1,5 @@ -import { type BrowserContext, expect, type Page, test } from '@playwright/test'; +/* eslint-disable playwright/expect-expect */ +import { expect, type Page, test } from '@playwright/test'; import { Extensions } from '../models/extensions.ts'; import { ManageColonyFunds } from '../models/manage-colony-funds.ts'; @@ -7,19 +8,12 @@ import { signInAndNavigateToColony, } from '../utils/common.ts'; -test.describe.configure({ mode: 'serial' }); test.describe('Manage Colony Funds', () => { - let page: Page; - let context: BrowserContext; - let manageColonyFunds: ManageColonyFunds; - let extensions: Extensions; - - test.beforeAll(async ({ browser, baseURL }) => { - context = await browser.newContext(); - page = await context.newPage(); - manageColonyFunds = new ManageColonyFunds(page); - extensions = new Extensions(page); - await setCookieConsent(context, baseURL); + async function setupTest(page: Page) { + const manageColonyFunds = new ManageColonyFunds(page); + const extensions = new Extensions(page); + + await setCookieConsent(page.context(), test.info().project.use.baseURL); await signInAndNavigateToColony(page, { colonyUrl: '/planex', wallet: /dev wallet 1$/i, @@ -27,81 +21,91 @@ test.describe('Manage Colony Funds', () => { await extensions.enableReputationWeightedExtension({ colonyPath: '/planex', }); - }); - - test.afterAll(async () => { - await context?.close(); - }); - test.describe('Mint tokens', () => { - test('Permissions decision method', async () => { - const [currentBalance] = await manageColonyFunds.getBalance('planex', [ - { - token: 'CREDS', - }, - ]); - await manageColonyFunds.open('Mint tokens'); - - await manageColonyFunds.fillMintTokensForm({ - amount: '1_000_000', - decisionMethod: 'Permissions', - title: 'Mint tokens', - }); - await manageColonyFunds.mintTokensButton.click(); - await manageColonyFunds.waitForTransaction(); - - await expect( - manageColonyFunds.completedAction.getByRole('heading', { - name: 'Mint tokens', - }), - ).toBeVisible(); - - await expect( - manageColonyFunds.completedAction.getByText(/Mint \w+ CREDS by \w+/), - ).toBeVisible(); - - await expect( - manageColonyFunds.completedAction.getByText('Mint Tokens', { - exact: true, - }), - ).toBeVisible(); - await expect( - manageColonyFunds.completedAction.getByText('1MCREDS', { - exact: true, - }), - ).toBeVisible(); + return { + manageColonyFunds, + extensions, + }; + } - await expect( - manageColonyFunds.completedAction.getByText('Permissions', { - exact: true, - }), - ).toBeVisible(); - - await expect( - manageColonyFunds.completedAction.getByText( - 'Member used permissions to', - ), - ).toBeVisible(); - - await expect( - manageColonyFunds.completedAction.getByRole('heading', { - name: 'Overview', - }), - ).toBeVisible(); + test.describe('Mint tokens', () => { + test('Permissions decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); - const [newBalance] = await manageColonyFunds.getBalance('planex', [ - { - token: 'CREDS', - }, - ]); + try { + const [currentBalance] = await manageColonyFunds.getBalance('planex', [ + { + token: 'CREDS', + }, + ]); + await manageColonyFunds.open('Mint tokens'); + + await manageColonyFunds.fillMintTokensForm({ + amount: '1_000_000', + decisionMethod: 'Permissions', + title: 'Mint tokens', + }); + await manageColonyFunds.mintTokensButton.click(); + await manageColonyFunds.waitForTransaction(); + + await expect( + manageColonyFunds.completedAction.getByRole('heading', { + name: 'Mint tokens', + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText(/Mint \w+ CREDS by \w+/), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText('Mint Tokens', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText('1MCREDS', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText('Permissions', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByText( + 'Member used permissions to', + ), + ).toBeVisible(); + + await expect( + manageColonyFunds.completedAction.getByRole('heading', { + name: 'Overview', + }), + ).toBeVisible(); + + const [newBalance] = await manageColonyFunds.getBalance('planex', [ + { + token: 'CREDS', + }, + ]); - const initial = parseFloat(currentBalance); - const after = parseFloat(newBalance); + const initial = parseFloat(currentBalance); + const after = parseFloat(newBalance); - expect(after).toBeGreaterThan(initial); + expect(after).toBeGreaterThan(initial); + } finally { + await page.context().close(); + } }); - test('Reputation decision method', async () => { + test('Reputation decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + await manageColonyFunds.open('Mint tokens'); await manageColonyFunds.fillMintTokensForm({ @@ -113,47 +117,14 @@ test.describe('Manage Colony Funds', () => { await manageColonyFunds.mintTokensButton.click(); await manageColonyFunds.waitForTransaction(); - await expect(manageColonyFunds.stepper).toBeVisible(); - await expect(manageColonyFunds.completedAction).toBeVisible(); - await expect( - manageColonyFunds.stepper.getByText(/Total stake required: \d+/), - ).toBeVisible(); - - await manageColonyFunds.support(); - - await expect(manageColonyFunds.stepper).toHaveText(/100% Supported/); - - await manageColonyFunds.oppose(); - - await expect(manageColonyFunds.stepper).toHaveText(/100% Opposed/); + await manageColonyFunds.completeReputationFlow(); - await manageColonyFunds.stepper - .getByText(/Vote to support or oppose?/) - .waitFor(); - await manageColonyFunds.supportButton.click(); - await manageColonyFunds.submitVoteButton.click(); - await manageColonyFunds.revealVoteButton.click(); - await manageColonyFunds.stepper.getByText('vote revealed').waitFor(); - await expect( - manageColonyFunds.stepper.getByText( - 'Finalize to execute the agreed transaction', - ), - ).toBeVisible(); - await manageColonyFunds.finalizeButton.click(); - - await manageColonyFunds.waitForPending(); - await manageColonyFunds.claimButton.click(); - await manageColonyFunds.waitForPending(); - await manageColonyFunds.completedAction - .getByRole('heading', { name: 'Your overview Claimed' }) - .waitFor(); - - await manageColonyFunds.claimButton.waitFor({ - state: 'hidden', - }); + await manageColonyFunds.removeTokens(); }); - test('Form validation', async () => { + test('Form validation', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + await manageColonyFunds.open('Mint tokens'); await manageColonyFunds.mintTokensButton.click(); @@ -180,7 +151,9 @@ test.describe('Manage Colony Funds', () => { }); test.describe('Funds transfer', () => { - test('Permissions decision method', async () => { + test('Permissions decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + const transferAmount = '1'; const [serenityBalance, andromedaBalance] = await manageColonyFunds.getBalance('planex', [ @@ -232,7 +205,9 @@ test.describe('Manage Colony Funds', () => { ); }); - test('Reputation decision method', async () => { + test('Reputation decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + await manageColonyFunds.open('Transfer funds'); await manageColonyFunds.fillTransferFundsForm({ @@ -246,65 +221,19 @@ test.describe('Manage Colony Funds', () => { await manageColonyFunds.transferFundsButton.click(); await manageColonyFunds.waitForTransaction(); - await expect(manageColonyFunds.stepper).toBeVisible(); await expect(manageColonyFunds.completedAction).toBeVisible(); await expect( manageColonyFunds.completedAction.getByText( /Move 1 CREDS from General to Andromeda/, ), ).toBeVisible(); - await expect( - manageColonyFunds.stepper.getByText(/Total stake required: \d+/), - ).toBeVisible(); - - await manageColonyFunds.support(); - - await expect(manageColonyFunds.stepper).toHaveText(/100% Supported/); - - await manageColonyFunds.oppose(); - - await expect(manageColonyFunds.stepper).toHaveText(/100% Opposed/); - - await manageColonyFunds.stepper - .getByText(/Vote to support or oppose?/) - .waitFor(); - await manageColonyFunds.supportButton.click(); - await manageColonyFunds.submitVoteButton.click(); - await expect( - manageColonyFunds.stepper.getByText(/You voted:Support/), - ).toBeVisible(); - - await expect( - manageColonyFunds.stepper.getByText(/^0 votes revealed$/), - ).toBeVisible(); - - await manageColonyFunds.revealVoteButton.click(); - - await manageColonyFunds.stepper - .getByRole('button', { - name: 'Support wins', - }) - .waitFor(); - - await expect( - manageColonyFunds.stepper.getByText( - 'Finalize to execute the agreed transaction', - ), - ).toBeVisible(); - await manageColonyFunds.finalizeButton.click(); - - await manageColonyFunds.waitForPending(); - await manageColonyFunds.claimButton.click(); - await manageColonyFunds.waitForPending(); - await manageColonyFunds.stepper - .getByRole('heading', { - name: 'Your overview Claimed', - }) - .waitFor(); + await manageColonyFunds.completeReputationFlow(); }); - test('Form validation', async () => { + test('Form validation', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + await manageColonyFunds.open('Transfer funds'); await manageColonyFunds.transferFundsButton.click(); @@ -344,25 +273,16 @@ test.describe('Manage Colony Funds', () => { ), ).toBeVisible(); - await manageColonyFunds.fillTransferFundsForm({ - tokenSymbol: 'ƓƓƓ', - }); - - await expect( - manageColonyFunds.drawer.getByText( - ManageColonyFunds.validationMessages.fundsTransfer.token.locked, - ), - ).toBeVisible(); - await manageColonyFunds.close(); }); }); test.describe('Manage tokens', () => { - test.afterAll(async () => { - await manageColonyFunds.removeTokens(); - }); - test('Add/Remove tokens | Permissions decision method', async () => { + test('Add/Remove tokens | Permissions decision method', async ({ + page, + }) => { + const { manageColonyFunds } = await setupTest(page); + await manageColonyFunds.open('Manage tokens'); await manageColonyFunds.setTitle('Manage tokens test'); await manageColonyFunds.setDecisionMethod('Permissions'); @@ -375,7 +295,9 @@ test.describe('Manage Colony Funds', () => { ).toBeVisible(); await expect( - manageColonyFunds.manageTokensTable.getByText('Ether', { exact: true }), + manageColonyFunds.manageTokensTable.getByText('Ether', { + exact: true, + }), ).toBeVisible(); await expect( @@ -462,7 +384,9 @@ test.describe('Manage Colony Funds', () => { }); }); - test('Reputation decision method', async () => { + test('Reputation decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + await manageColonyFunds.open('Manage tokens'); await manageColonyFunds.setTitle('Manage tokens test'); await manageColonyFunds.setDecisionMethod('Reputation'); @@ -485,7 +409,6 @@ test.describe('Manage Colony Funds', () => { .click(); await manageColonyFunds.updateTokensButton.click(); - await manageColonyFunds.waitForTransaction(); await expect( @@ -494,43 +417,12 @@ test.describe('Manage Colony Funds', () => { }), ).toBeVisible(); - await expect( - manageColonyFunds.stepper.getByText(/Total stake required: \d+/), - ).toBeVisible(); - - await manageColonyFunds.support(); - - await expect(manageColonyFunds.stepper).toHaveText(/100% Supported/); - - await manageColonyFunds.oppose(); - - await expect(manageColonyFunds.stepper).toHaveText(/100% Opposed/); - - await manageColonyFunds.stepper - .getByText(/Vote to support or oppose?/) - .waitFor(); - await manageColonyFunds.supportButton.click(); - await manageColonyFunds.submitVoteButton.click(); - await manageColonyFunds.revealVoteButton.click(); - await manageColonyFunds.stepper.getByText('vote revealed').waitFor(); - await expect( - manageColonyFunds.stepper.getByText( - 'Finalize to execute the agreed transaction', - ), - ).toBeVisible(); - await manageColonyFunds.finalizeButton.click(); - - await manageColonyFunds.waitForPending(); - await manageColonyFunds.claimButton.click(); - - await manageColonyFunds.completedAction - .getByRole('heading', { name: 'Your overview Claimed' }) - .waitFor(); - - await expect(manageColonyFunds.claimButton).toBeHidden(); + await manageColonyFunds.completeReputationFlow(); }); - test('Form validation', async () => { + test('Form validation', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + await manageColonyFunds.open('Manage tokens'); await manageColonyFunds.updateTokensButton.click(); diff --git a/playwright/e2e/manage-colony-reputation.spec.ts b/playwright/e2e/manage-colony-reputation.spec.ts new file mode 100644 index 0000000000..4672e04ee1 --- /dev/null +++ b/playwright/e2e/manage-colony-reputation.spec.ts @@ -0,0 +1,232 @@ +/* eslint-disable no-warning-comments */ +import { expect, type Page, test } from '@playwright/test'; + +import { Extensions } from '../models/extensions.ts'; +import { ManageReputation } from '../models/manage-colony-reputation.ts'; +import { + setCookieConsent, + signInAndNavigateToColony, +} from '../utils/common.ts'; + +test.describe('Manage Colony Reputation', () => { + async function setupTest(page: Page) { + const manageReputation = new ManageReputation(page); + const extensions = new Extensions(page); + + await setCookieConsent(page.context(), test.info().project.use.baseURL); + await signInAndNavigateToColony(page, { + colonyUrl: '/planex', + wallet: /dev wallet 1$/i, + }); + await extensions.enableReputationWeightedExtension({ + colonyPath: '/planex', + }); + + return { + manageReputation, + extensions, + }; + } + + test('Permissions decision method', async ({ page }) => { + const { manageReputation } = await setupTest(page); + + const points = '20'; + const member = '0xb77D57F4959eAfA0339424b83FcFaf9c15407461'; + const memberName = 'leela'; + const title = 'Award reputation to leela'; + await manageReputation.open('Manage reputation'); + + await manageReputation.fillManageReputationForm({ + title, + modification: 'Award reputation', + member, + amount: points, + decisionMethod: 'Permissions', + }); + + const percetage = + await manageReputation.verifyReputationValuesInTable(points); + + await manageReputation.updateReputationButton.click(); + await manageReputation.waitForTransaction(); + + await expect( + manageReputation.completedAction.getByText( + `Add ${points} reputation points to ${memberName} by leela`, + ), + ).toBeVisible(); + await expect( + manageReputation.completedAction.getByText( + 'Member used permissions to create this action', + ), + ).toBeVisible(); + await expect( + manageReputation.completedAction.getByText('Manage reputation'), + ).toBeVisible(); + + await expect( + manageReputation.completedAction.getByText(title, { exact: true }), + ).toBeVisible(); + + await expect( + manageReputation.completedAction.getByText('Award reputation', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageReputation.completedAction.getByRole('button', { + name: `Avatar of user ${memberName}`, + }), + ).toHaveCount(2); + + await expect( + manageReputation.completedAction.getByRole('table').getByText('Awarded'), + ).toBeVisible(); + + await expect( + manageReputation.completedAction + .getByRole('table') + .getByText(`${points} ${percetage}`), + ).toBeVisible(); + }); + + test('Reputation decision method', async ({ page }) => { + const { manageReputation } = await setupTest(page); + + const points = '100'; + const member = '0x27fF0C145E191C22C75cD123C679C3e1F58a4469'; + const memberName = 'fry'; + const title = 'Award reputation to fry'; + + await manageReputation.open('Manage reputation'); + + await manageReputation.fillManageReputationForm({ + title, + modification: 'Award reputation', + member, + amount: points, + decisionMethod: 'Reputation', + }); + + const percetage = + await manageReputation.verifyReputationValuesInTable(points); + + await manageReputation.updateReputationButton.click(); + await manageReputation.waitForTransaction(); + + await expect( + manageReputation.completedAction.getByText( + `Add ${points} reputation points to ${memberName} by leela`, + ), + ).toBeVisible(); + + await manageReputation.completeReputationFlow(); + + await expect( + manageReputation.completedAction.getByText('Manage reputation'), + ).toBeVisible(); + + await expect( + manageReputation.completedAction.getByText(title, { exact: true }), + ).toBeVisible(); + + await expect( + manageReputation.completedAction.getByText('Award reputation', { + exact: true, + }), + ).toBeVisible(); + + await expect( + manageReputation.completedAction.getByRole('button', { + name: 'Avatar of user fry', + }), + ).toBeVisible(); + + await expect( + manageReputation.completedAction.getByRole('table').getByText('Awarded'), + ).toBeVisible(); + + await expect( + manageReputation.completedAction + .getByRole('table') + .getByText(`${points} ${percetage}`), + ).toBeVisible(); + }); + + test('Form validation', async ({ page }) => { + const { manageReputation } = await setupTest(page); + + await manageReputation.open('Manage reputation'); + await manageReputation.updateReputationButton.click(); + + for (const message of ManageReputation.validationMessages + .allRequiredFields) { + await expect(manageReputation.drawer.getByText(message)).toBeVisible(); + } + + await manageReputation.fillManageReputationForm({ + title: + 'This is a test title that exceeds the maximum character limit of sixty characters long.', + }); + + await manageReputation.updateReputationButton.click(); + + await expect( + manageReputation.drawer.getByText( + ManageReputation.validationMessages.title.maxLengthExceeded, + ), + ).toBeVisible(); + + await manageReputation.fillManageReputationForm({ + title: 'Award reputation', + modification: 'Award reputation', + member: '0x27fF0C145E191C22C75cD123C679C3e1F58a4469', + decisionMethod: 'Reputation', + amount: '-100', + }); + + await expect( + manageReputation.drawer.getByText( + ManageReputation.validationMessages.amount.mustBeGreaterThanZero, + ), + ).toBeVisible(); + + await manageReputation.fillManageReputationForm({ + amount: '0', + }); + + await expect( + manageReputation.drawer.getByText( + ManageReputation.validationMessages.amount.mustBeGreaterThanZero, + ), + ).toBeVisible(); + + await manageReputation.fillManageReputationForm({ + amount: '1'.padEnd(80, '0'), // 1 followed by 79 zeros + }); + + await expect(manageReputation.drawer.getByText('×1078')).toHaveCount(2); + + // TODO: Uncomment this test when the issue is fixed + // Test very small number + // await manageReputation.fillManageReputationForm({ + // amount: '0.0000000000000000001', // 18 decimal places + // }); + // await expect(manageReputation.drawer.getByText('×10-18')).toHaveCount(2); + + // Test invalid member address validation + // TODO: Uncomment this test when the validation is fixed + // await manageReputation.fillManageReputationForm({ + // member: '0x1234567890', + // }); + // await expect( + // manageReputation.drawer.getByText( + // ManageReputation.validationMessages.member.invalid, + // ), + // ).toBeVisible(); + + await manageReputation.close(); + }); +}); diff --git a/playwright/e2e/manage-colony-teams.spec.ts b/playwright/e2e/manage-colony-teams.spec.ts index 7da7dcbeeb..64db93b865 100644 --- a/playwright/e2e/manage-colony-teams.spec.ts +++ b/playwright/e2e/manage-colony-teams.spec.ts @@ -1,4 +1,5 @@ -import { type BrowserContext, expect, type Page, test } from '@playwright/test'; +/* eslint-disable playwright/expect-expect */ +import { expect, type Page, test } from '@playwright/test'; import { Extensions } from '../models/extensions.ts'; import { ManageColonyTeams } from '../models/manage-colony-teams.ts'; @@ -8,19 +9,12 @@ import { signInAndNavigateToColony, } from '../utils/common.ts'; -test.describe.configure({ mode: 'serial' }); test.describe('Manage Colony Teams', () => { - let page: Page; - let context: BrowserContext; - let manageColonyTeams: ManageColonyTeams; - let extensions: Extensions; - - test.beforeAll(async ({ browser, baseURL }) => { - context = await browser.newContext(); - page = await context.newPage(); - manageColonyTeams = new ManageColonyTeams(page); - extensions = new Extensions(page); - await setCookieConsent(context, baseURL); + async function setupTest(page: Page) { + const manageColonyTeams = new ManageColonyTeams(page); + const extensions = new Extensions(page); + + await setCookieConsent(page.context(), test.info().project.use.baseURL); await signInAndNavigateToColony(page, { colonyUrl: '/planex', wallet: /dev wallet 1$/i, @@ -28,12 +22,16 @@ test.describe('Manage Colony Teams', () => { await extensions.enableReputationWeightedExtension({ colonyPath: '/planex', }); - }); - test.afterAll(async () => { - await context?.close(); - }); - test('Create new team | Permissions decision method', async () => { + return { + manageColonyTeams, + extensions, + }; + } + + test('Create new team | Permissions decision method', async ({ page }) => { + const { manageColonyTeams } = await setupTest(page); + const teamName = `Test team ${generateRandomString()}`; await manageColonyTeams.open('Create new team'); await manageColonyTeams.fillForm({ @@ -79,7 +77,9 @@ test.describe('Manage Colony Teams', () => { }); }); - test('Create new team | Reputation decision method', async () => { + test('Create new team | Reputation decision method', async ({ page }) => { + const { manageColonyTeams } = await setupTest(page); + const teamName = `Test team ${generateRandomString()}`; await manageColonyTeams.open('Create new team'); const colorIndex = Math.floor(Math.random() * 16); @@ -92,78 +92,27 @@ test.describe('Manage Colony Teams', () => { }); await manageColonyTeams.createTeamButton.click(); - await manageColonyTeams.waitForTransaction(); - await expect(manageColonyTeams.stepper).toBeVisible(); - await expect(manageColonyTeams.completedAction).toBeVisible(); await expect( manageColonyTeams.completedAction.getByText( `Create new team ${teamName} by leela`, ), ).toBeVisible(); - await expect( - manageColonyTeams.stepper.getByText(/Total stake required: \d+/), - ).toBeVisible(); - - await manageColonyTeams.support(); - - await expect(manageColonyTeams.stepper).toHaveText(/100% Supported/); - - await manageColonyTeams.oppose(); - - await expect(manageColonyTeams.stepper).toHaveText(/100% Opposed/); - - await manageColonyTeams.stepper - .getByText(/Vote to support or oppose?/) - .waitFor(); - await manageColonyTeams.supportButton.click(); - await manageColonyTeams.submitVoteButton.click(); - await expect( - manageColonyTeams.stepper.getByText(/You voted:Support/), - ).toBeVisible(); - - await expect( - manageColonyTeams.stepper.getByText(/^0 votes revealed$/), - ).toBeVisible(); - - await manageColonyTeams.revealVoteButton.click(); - - await manageColonyTeams.stepper - .getByRole('button', { - name: 'Support wins', - }) - .waitFor(); - - await expect( - manageColonyTeams.stepper.getByText( - 'Finalize to execute the agreed transaction', - ), - ).toBeVisible(); - await manageColonyTeams.finalizeButton.click(); - - await manageColonyTeams.waitForPending(); - await manageColonyTeams.claimButton.click(); - await manageColonyTeams.waitForPending(); - - await manageColonyTeams.stepper - .getByRole('heading', { - name: 'Your overview Claimed', - }) - .waitFor(); + await manageColonyTeams.completeReputationFlow(); }); - test('Form validation', async () => { + test('Form validation', async ({ page }) => { + const { manageColonyTeams } = await setupTest(page); + await manageColonyTeams.open('Create new team'); await manageColonyTeams.createTeamButton.click(); - // Verify all required field validation messages for (const message of ManageColonyTeams.validationMessages.createTeam .allRequiredFields) { await expect(manageColonyTeams.drawer.getByText(message)).toBeVisible(); } - // Test title max length validation await manageColonyTeams.fillForm({ title: 'This is a test title that exceeds the maximum character limit of sixty characters long.', @@ -177,7 +126,6 @@ test.describe('Manage Colony Teams', () => { ), ).toBeVisible(); - // Verify long teamName value is truncated await manageColonyTeams.fillForm({ name: 'This is a test title that exceeds the maximum character limit of 20 characters long.', }); @@ -197,7 +145,9 @@ test.describe('Manage Colony Teams', () => { await manageColonyTeams.close(); }); - test('Edit team | Permissions decision method', async () => { + test('Edit team | Permissions decision method', async ({ page }) => { + const { manageColonyTeams } = await setupTest(page); + // Create a team to edit const initialTeamName = `Test team ${generateRandomString()}`; await manageColonyTeams.open('Create new team'); @@ -211,9 +161,9 @@ test.describe('Manage Colony Teams', () => { const initialColor = await manageColonyTeams.setColor(initialColorIndex); await manageColonyTeams.createTeamButton.click(); await manageColonyTeams.waitForTransaction(); + // Edit team await manageColonyTeams.open('Edit a team'); - await manageColonyTeams.selectTeam(initialTeamName); // Update team details @@ -232,7 +182,6 @@ test.describe('Manage Colony Teams', () => { await manageColonyTeams.editTeamButton.click(); await manageColonyTeams.waitForTransaction(); - // Verify the completed action await expect( manageColonyTeams.completedAction.getByText( 'Member used permissions to create this action', @@ -245,14 +194,12 @@ test.describe('Manage Colony Teams', () => { }), ).toHaveCount(2); - // Verify color changes const { teamBadgeColor } = await manageColonyTeams.getTeamColorBadgeColor(updatedTeamName); expect(teamBadgeColor).toBe(updatedColor); expect(teamBadgeColor).not.toBe(initialColor); - // Verify the purpose was updated await expect( manageColonyTeams.completedAction.getByText(updatedPurpose, { exact: true, diff --git a/playwright/models/manage-colony-funds.ts b/playwright/models/manage-colony-funds.ts index 3a6dd03e3a..82a4cbfeb2 100644 --- a/playwright/models/manage-colony-funds.ts +++ b/playwright/models/manage-colony-funds.ts @@ -1,6 +1,8 @@ import { type Page, type Locator, expect } from '@playwright/test'; -export class ManageColonyFunds { +import { ReputationFlow, type ReputationFlowModel } from './reputation-flow.ts'; + +export class ManageColonyFunds implements ReputationFlowModel { static readonly validationMessages = { common: { amount: { @@ -34,10 +36,6 @@ export class ManageColonyFunds { notEnoughFunds: 'Not enough funds to cover the payment and network fees', }, - token: { - locked: - 'This token is locked and is not able to be used for payments. Check with the token creator for details.', - }, }, manageTokens: { allRequiredFields: [ @@ -51,6 +49,8 @@ export class ManageColonyFunds { }, }; + page: Page; + drawer: Locator; form: Locator; @@ -93,7 +93,10 @@ export class ManageColonyFunds { tokenSearchMenu: Locator; - constructor(private page: Page) { + private reputationFlow: ReputationFlow; + + constructor(page: Page) { + this.page = page; this.drawer = page.getByTestId('action-drawer'); this.form = page.getByTestId('action-form'); this.selectDropdown = page.getByTestId('search-select-menu'); @@ -143,6 +146,7 @@ export class ManageColonyFunds { this.manageTokensTable = this.drawer.getByRole('table'); this.tokenSelector = this.page.getByTestId('token-select'); this.tokenSearchMenu = this.page.getByTestId('token-search-select'); + this.reputationFlow = new ReputationFlow(this); } async setTitle(title: string) { @@ -303,32 +307,6 @@ export class ManageColonyFunds { } } - async support(stakeAmount?: string) { - await this.supportButton.click(); - await this.stepper.getByLabel('Stake amount').waitFor({ state: 'visible' }); - await this.stepper - .getByRole('button', { name: 'Max' }) - .waitFor({ state: 'visible' }); - if (stakeAmount) { - await this.stepper.getByLabel('Stake amount').fill(stakeAmount); - } else { - await this.stepper.getByRole('button', { name: 'Max' }).click(); - } - - await this.stepper.getByRole('button', { name: 'Stake' }).click(); - } - - async oppose(stakeAmount?: string) { - await this.opposeButton.click(); - if (stakeAmount) { - await this.stepper.getByRole('textbox').fill(stakeAmount); - } else { - await this.stepper.getByRole('button', { name: 'Max' }).click(); - } - - await this.stepper.getByRole('button', { name: 'Stake' }).click(); - } - async verifyTokenAdded({ colonyName, token, @@ -373,4 +351,8 @@ export class ManageColonyFunds { allAddedTokens.length, ); } + + async completeReputationFlow() { + await this.reputationFlow.completeVotingWithSupport(); + } } diff --git a/playwright/models/manage-colony-reputation.ts b/playwright/models/manage-colony-reputation.ts new file mode 100644 index 0000000000..205a8ef1a2 --- /dev/null +++ b/playwright/models/manage-colony-reputation.ts @@ -0,0 +1,336 @@ +import { type Page, type Locator, expect } from '@playwright/test'; +import { forwardTime } from 'playwright/utils/common'; + +import { ReputationFlow } from './reputation-flow.ts'; + +export class ManageReputation { + static readonly validationMessages = { + allRequiredFields: [ + 'team must be a `number` type, but the final value was: `NaN` (cast from the value `""`).', + 'decisionMethod must be defined', + 'modification is a required field', + 'amount is a required field', + 'member is a required field', + 'Title is required', + ], + title: { + maxLengthExceeded: 'Title must not exceed 60 characters', + }, + amount: { + mustBeGreaterThanZero: 'Amount must be greater than zero', + }, + member: { + invalid: 'This is not a valid address', + }, + }; + + page: Page; + + drawer: Locator; + + form: Locator; + + sidebar: Locator; + + selectDropdown: Locator; + + decisionMethodDropdown: Locator; + + stepper: Locator; + + closeButton: Locator; + + completedAction: Locator; + + confirmationDialog: Locator; + + supportButton: Locator; + + opposeButton: Locator; + + submitVoteButton: Locator; + + revealVoteButton: Locator; + + finalizeButton: Locator; + + claimButton: Locator; + + updateReputationButton: Locator; + + table: Locator; + + currentReputation: Locator; + + newReputation: Locator; + + userHubButton: Locator; + + userHubPopover: Locator; + + totalReputationPoints: Locator; + + reputationFlow: ReputationFlow; + + constructor(page: Page) { + this.page = page; + this.drawer = page.getByTestId('action-drawer'); + this.form = page.getByTestId('action-form'); + this.selectDropdown = page.getByTestId('search-select-menu'); + this.decisionMethodDropdown = page + .getByRole('list') + .filter({ hasText: /Available decision methods/i }); + this.sidebar = page.getByTestId('colony-page-sidebar'); + this.stepper = page.getByTestId('stepper'); + this.closeButton = page.getByRole('button', { name: /close the modal/i }); + this.completedAction = page.getByTestId('completed-action'); + this.confirmationDialog = this.page + .getByRole('dialog') + .filter({ hasText: 'Do you wish to cancel the action creation?' }); + this.supportButton = this.stepper.getByText('Support', { exact: true }); + this.opposeButton = this.stepper.getByText('Oppose', { exact: true }); + this.submitVoteButton = this.stepper.getByRole('button', { + name: 'Submit vote', + }); + this.revealVoteButton = this.stepper.getByRole('button', { + name: 'Reveal vote', + }); + this.finalizeButton = this.completedAction + .getByRole('button', { + name: 'Finalize', + }) + .last(); + this.claimButton = this.stepper + .getByRole('button', { + name: 'Claim', + }) + .last(); + this.updateReputationButton = this.drawer + .getByRole('button', { + name: 'Update reputation', + }) + .last(); + this.table = this.page.getByRole('table'); + this.currentReputation = this.table + .locator('tbody') + .nth(0) + .locator('td') + .nth(0); + this.newReputation = this.table + .locator('tbody') + .nth(0) + .locator('td') + .last(); + this.userHubButton = this.page.getByTestId('user-hub-button'); + this.userHubPopover = this.page.getByTestId('user-hub'); + this.totalReputationPoints = + this.userHubPopover.getByTestId('reputation-points'); + this.reputationFlow = new ReputationFlow(this); + } + + async setTitle(title: string) { + await this.form.getByPlaceholder('Enter title').fill(title); + } + + async open(motionTitle: 'Manage reputation') { + await this.sidebar.getByLabel('Start the manage colony action').click(); + await this.drawer.waitFor({ state: 'visible' }); + await this.drawer + .getByRole('button', { + name: motionTitle, + }) + .click(); + await this.form.waitFor({ state: 'visible' }); + await this.drawer + .getByTestId('action-sidebar-description') + .waitFor({ state: 'visible' }); + } + + async close() { + await this.closeButton.click(); + + const dialog = this.confirmationDialog; + if (await dialog.isVisible()) { + await dialog + .getByRole('button', { name: 'Yes, cancel the action' }) + .click(); + } + } + + async waitForTransaction() { + await this.waitForPending(); + await this.page.waitForURL(/\?tx=/); + await this.page + .getByTestId('loading-skeleton') + .last() + .waitFor({ state: 'hidden' }); + } + + async waitForPending() { + await this.page + .getByRole('button', { name: 'Pending' }) + .last() + .waitFor({ state: 'visible' }); + + await this.page + .getByRole('button', { name: 'Pending' }) + .last() + .waitFor({ state: 'hidden' }); + } + + async setDecisionMethod(decisionMethod: 'Permissions' | 'Reputation') { + await this.form.getByRole('button', { name: 'Select method' }).click(); + + await this.decisionMethodDropdown + .getByRole('button', { name: decisionMethod }) + .click(); + } + + async fillManageReputationForm({ + title, + modification, + member, + amount, + decisionMethod, + }: { + title?: string; + modification?: string; + member?: string; + amount?: string; + decisionMethod?: 'Permissions' | 'Reputation'; + }) { + if (title) { + await this.setTitle(title); + } + + if (modification) { + await this.form + .getByRole('button', { name: 'Select modification' }) + .click(); + await this.page.getByRole('button', { name: modification }).click(); + } + + if (member) { + await this.form.getByRole('button', { name: 'Select user' }).click(); + await this.page + .getByPlaceholder('Search or add wallet address') + .fill(member); + await this.page.getByRole('button', { name: member }).click(); + } + + if (decisionMethod) { + await this.setDecisionMethod(decisionMethod); + } + + if (amount) { + await this.table.getByPlaceholder('Enter value').fill(amount); + } + } + + async selectTeam(teamName: string) { + await this.form + .getByRole('button', { name: 'Select team', exact: true }) + .click(); + await this.selectDropdown.getByText(teamName).click(); + } + + async support(stakeAmount?: string) { + await this.supportButton.click(); + await this.stepper.getByLabel('Stake amount').waitFor({ state: 'visible' }); + await this.stepper + .getByRole('button', { name: 'Max' }) + .waitFor({ state: 'visible' }); + if (stakeAmount) { + await this.stepper.getByLabel('Stake amount').fill(stakeAmount); + } else { + await this.stepper.getByRole('button', { name: 'Max' }).click(); + } + await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); + await this.stepper.getByRole('button', { name: 'Stake' }).click(); + } + + async oppose(stakeAmount?: string) { + await this.opposeButton.click(); + if (stakeAmount) { + await this.stepper.getByRole('textbox').fill(stakeAmount); + } else { + await this.stepper.getByRole('button', { name: 'Max' }).click(); + } + await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); + await this.stepper.getByRole('button', { name: 'Stake' }).click(); + } + + async verifyReputationValuesInTable(amount: string) { + const currentReputation = await this.currentReputation.textContent(); + const newReputation = await this.newReputation.textContent(); + + // Extract numbers by taking everything before "Points" and removing commas + const currentValue = parseFloat( + currentReputation?.split('Points')[0].replace(/,/g, '') || '0', + ); + const newValue = parseFloat( + newReputation?.split('Points')[0].replace(/,/g, '') || '0', + ); + const amountValue = parseFloat(amount); + + // Verify that new reputation equals current reputation plus amount + expect(newValue).toBeCloseTo(currentValue + amountValue, 0); + + const percentage = await this.table + .locator('tbody td') + .nth(1) + .textContent(); + + return percentage; + } + + async verifyReputationChange({ + pointsAdded = 0, + pointsRemoved = 0, + }: { + pointsAdded?: number; + pointsRemoved?: number; + }) { + await this.userHubButton.click(); + await this.userHubPopover.waitFor({ state: 'visible' }); + const initialTotalReputationPoints = + await this.totalReputationPoints.textContent(); + const initialTotalReputationPointsValue = parseFloat( + (initialTotalReputationPoints || '0').replace(/,/g, ''), + ); + + await forwardTime(2); + + await this.page.reload(); + // It doesn't work locally without this second forwardTime - reputation points are not updated + await forwardTime(2); + await this.page.reload(); + + await this.page.getByText('Loading').waitFor({ state: 'hidden' }); + + await this.page + .getByTestId('loading-skeleton') + .last() + .waitFor({ state: 'hidden' }); + await this.userHubButton.click(); + await this.userHubPopover.waitFor({ state: 'visible' }); + + const totalReputationPoints = + await this.totalReputationPoints.textContent(); + const reputationPointsValue = parseFloat( + (totalReputationPoints || '0').replace(/,/g, ''), + ); + + const expectedValue = Math.floor( + initialTotalReputationPointsValue + pointsAdded + pointsRemoved, + ); + const actualValue = Math.floor(reputationPointsValue); + + // Verify that actual value is within ±1 of expected value to account for rounding differences + expect(Math.abs(actualValue - expectedValue) <= 1).toBeTruthy(); + } + + async completeReputationFlow() { + await this.reputationFlow.completeVotingWithSupport(); + } +} diff --git a/playwright/models/manage-colony-teams.ts b/playwright/models/manage-colony-teams.ts index da949fab7e..ec35a56054 100644 --- a/playwright/models/manage-colony-teams.ts +++ b/playwright/models/manage-colony-teams.ts @@ -1,5 +1,7 @@ import { type Page, type Locator, expect } from '@playwright/test'; +import { ReputationFlow } from './reputation-flow.ts'; + export class ManageColonyTeams { static readonly validationMessages = { createTeam: { @@ -20,6 +22,8 @@ export class ManageColonyTeams { }, }; + page: Page; + drawer: Locator; form: Locator; @@ -60,7 +64,10 @@ export class ManageColonyTeams { teamColorBadge: Locator; - constructor(private page: Page) { + reputationFlow: ReputationFlow; + + constructor(page: Page) { + this.page = page; this.drawer = page.getByTestId('action-drawer'); this.form = page.getByTestId('action-form'); this.selectDropdown = page.getByTestId('search-select-menu'); @@ -105,6 +112,7 @@ export class ManageColonyTeams { ); this.teamColorSelectMenu = this.page.getByTestId('team-color-select-menu'); this.teamColorBadge = this.page.getByTestId('team-color-badge'); + this.reputationFlow = new ReputationFlow(this); } async setTitle(title: string) { @@ -287,4 +295,8 @@ export class ManageColonyTeams { await this.stepper.getByRole('button', { name: 'Stake' }).waitFor(); await this.stepper.getByRole('button', { name: 'Stake' }).click(); } + + async completeReputationFlow() { + await this.reputationFlow.completeVotingWithSupport(); + } } diff --git a/playwright/models/reputation-flow.ts b/playwright/models/reputation-flow.ts new file mode 100644 index 0000000000..fdb22bb400 --- /dev/null +++ b/playwright/models/reputation-flow.ts @@ -0,0 +1,119 @@ +import { type Locator, expect, type Page } from '@playwright/test'; +import { forwardTime } from 'playwright/utils/common'; + +export interface ReputationFlowModel { + page: Page; + stepper: Locator; + completedAction: Locator; + supportButton: Locator; + opposeButton: Locator; + submitVoteButton: Locator; + revealVoteButton: Locator; + finalizeButton: Locator; + claimButton: Locator; + waitForPending(): Promise; +} + +export class ReputationFlow { + // eslint-disable-next-line no-useless-constructor, no-empty-function + constructor(private model: ReputationFlowModel) {} + + async completeVotingWithSupport() { + await this.voteOnMotion('Support'); + await this.model.stepper.getByText('100% Supported').waitFor(); + + await forwardTime(1); + + await this.model.page.reload(); + await this.model.page.getByText('Loading').waitFor({ state: 'hidden' }); + await this.model.page + .getByTestId('loading-skeleton') + .last() + .waitFor({ state: 'hidden' }); + + await expect( + this.model.stepper.getByRole('button', { name: 'Staking', exact: true }), + ).toBeVisible(); + await this.model.stepper + .getByRole('button', { name: 'Supported' }) + .waitFor(); + + await expect( + this.model.stepper.getByRole('button', { name: 'Finalize' }), + ).toHaveCount(2); + await this.model.finalizeButton.last().click(); + + await this.model.claimButton.click(); + + await this.model.completedAction + .getByRole('heading', { name: 'Your overview Claimed' }) + .waitFor(); + + await this.model.claimButton.waitFor({ state: 'hidden' }); + } + + async completeFullVotingWithSupport() { + await expect(this.model.stepper).toBeVisible(); + + await expect( + this.model.stepper.getByText(/Total stake required: \d+/), + ).toBeVisible(); + + await this.voteOnMotion('Support'); + + await expect(this.model.stepper).toHaveText(/100% Supported/); + + await this.voteOnMotion('Oppose'); + await expect(this.model.stepper).toHaveText(/100% Opposed/); + + await this.model.stepper.getByText(/Vote to support or oppose?/).waitFor(); + await this.model.supportButton.click(); + await this.model.supportButton.waitFor({ state: 'hidden' }); + await this.model.submitVoteButton.click(); + await this.model.submitVoteButton.waitFor({ state: 'hidden' }); + await this.model.revealVoteButton.click(); + await this.model.stepper.getByText('1 vote revealed').waitFor(); + + await expect( + this.model.stepper.getByText( + 'Finalize to execute the agreed transaction', + ), + ).toBeVisible(); + await this.model.finalizeButton.click(); + + await this.model.claimButton.click(); + + await this.model.completedAction + .getByRole('heading', { name: 'Your overview Claimed' }) + .waitFor(); + + await this.model.claimButton.waitFor({ state: 'hidden' }); + } + + async voteOnMotion(voteType: 'Support' | 'Oppose') { + await this.model.completedAction + .getByRole('heading', { + name: 'Total stake required', + }) + .waitFor({ state: 'visible' }); + await this.model.completedAction.getByTestId('countDownTimer').waitFor({ + state: 'visible', + }); + await this.model.completedAction + .getByText(voteType, { exact: true }) + .click(); + await this.model.completedAction + .getByRole('button', { name: 'Max' }) + .click(); + + await this.model.completedAction + .getByRole('button', { name: 'Stake' }) + .click(); + + await this.model.completedAction + .getByRole('button', { name: 'Stake' }) + .waitFor({ + state: 'hidden', + }); + } +} diff --git a/src/components/common/Extensions/UserHub/UserHub.tsx b/src/components/common/Extensions/UserHub/UserHub.tsx index 3d2f549aee..dcbb8b2a9c 100644 --- a/src/components/common/Extensions/UserHub/UserHub.tsx +++ b/src/components/common/Extensions/UserHub/UserHub.tsx @@ -83,7 +83,7 @@ const UserHub: FC = ({ return (
= ({ 'sm:min-h-[27.75rem]': selectedTab === UserHubTab.Balance, }, )} + data-testid="user-hub" >
{isMobile ? ( diff --git a/src/components/common/Extensions/UserHub/partials/BalanceTab/partials/TotalReputation.tsx b/src/components/common/Extensions/UserHub/partials/BalanceTab/partials/TotalReputation.tsx index e42da76a11..e466b9fd73 100644 --- a/src/components/common/Extensions/UserHub/partials/BalanceTab/partials/TotalReputation.tsx +++ b/src/components/common/Extensions/UserHub/partials/BalanceTab/partials/TotalReputation.tsx @@ -83,6 +83,7 @@ const TotalReputation: FC = ({ className="text-sm" value={formattedReputationPoints} suffix=" pts" + testId="reputation-points" />
diff --git a/src/components/common/Extensions/UserHubButton/UserHubButton.tsx b/src/components/common/Extensions/UserHubButton/UserHubButton.tsx index 6d1fb2fb52..48fddbb4f3 100644 --- a/src/components/common/Extensions/UserHubButton/UserHubButton.tsx +++ b/src/components/common/Extensions/UserHubButton/UserHubButton.tsx @@ -173,6 +173,7 @@ const UserHubButton: FC = ({ openTab, onOpen }) => {
) : (