diff --git a/playwright.config.ts b/playwright.config.ts index 08a97b762c9..c76a82ebb2c 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 new file mode 100644 index 00000000000..597da42201c --- /dev/null +++ b/playwright/e2e/manage-colony-funds.spec.ts @@ -0,0 +1,457 @@ +/* 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'; +import { + setCookieConsent, + signInAndNavigateToColony, +} from '../utils/common.ts'; + +test.describe('Manage Colony Funds', () => { + 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, + }); + await extensions.enableReputationWeightedExtension({ + colonyPath: '/planex', + }); + + return { + manageColonyFunds, + extensions, + }; + } + + test.describe('Mint tokens', () => { + test('Permissions decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + + 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); + + expect(after).toBeGreaterThan(initial); + } finally { + await page.context().close(); + } + }); + + test('Reputation decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + + await manageColonyFunds.open('Mint tokens'); + + await manageColonyFunds.fillMintTokensForm({ + amount: '1', + decisionMethod: 'Reputation', + title: 'Mint tokens', + }); + + await manageColonyFunds.mintTokensButton.click(); + await manageColonyFunds.waitForTransaction(); + + await manageColonyFunds.completeReputationFlow(); + + await manageColonyFunds.removeTokens(); + }); + + test('Form validation', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + + 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(); + + await manageColonyFunds.close(); + }); + }); + + test.describe('Funds transfer', () => { + test('Permissions decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + + 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), + ); + }); + + test('Reputation decision method', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + + 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.completedAction).toBeVisible(); + await expect( + manageColonyFunds.completedAction.getByText( + /Move 1 CREDS from General to Andromeda/, + ), + ).toBeVisible(); + + await manageColonyFunds.completeReputationFlow(); + }); + + test('Form validation', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + + 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.close(); + }); + }); + + test.describe('Manage tokens', () => { + 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'); + + 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 ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + + 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 manageColonyFunds.completeReputationFlow(); + }); + + test('Form validation', async ({ page }) => { + const { manageColonyFunds } = await setupTest(page); + + 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/e2e/manage-colony-reputation.spec.ts b/playwright/e2e/manage-colony-reputation.spec.ts new file mode 100644 index 00000000000..4672e04ee1e --- /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 new file mode 100644 index 00000000000..64db93b865a --- /dev/null +++ b/playwright/e2e/manage-colony-teams.spec.ts @@ -0,0 +1,209 @@ +/* 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'; +import { + generateRandomString, + setCookieConsent, + signInAndNavigateToColony, +} from '../utils/common.ts'; + +test.describe('Manage Colony Teams', () => { + 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, + }); + await extensions.enableReputationWeightedExtension({ + colonyPath: '/planex', + }); + + 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({ + 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 ({ page }) => { + const { manageColonyTeams } = await setupTest(page); + + 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.completedAction.getByText( + `Create new team ${teamName} by leela`, + ), + ).toBeVisible(); + await manageColonyTeams.completeReputationFlow(); + }); + + test('Form validation', async ({ page }) => { + const { manageColonyTeams } = await setupTest(page); + + await manageColonyTeams.open('Create new team'); + await manageColonyTeams.createTeamButton.click(); + + for (const message of ManageColonyTeams.validationMessages.createTeam + .allRequiredFields) { + await expect(manageColonyTeams.drawer.getByText(message)).toBeVisible(); + } + + 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(); + + 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 ({ page }) => { + const { manageColonyTeams } = await setupTest(page); + + // 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(); + + await expect( + manageColonyTeams.completedAction.getByText( + 'Member used permissions to create this action', + ), + ).toBeVisible(); + + await expect( + manageColonyTeams.completedAction.getByText(updatedTeamName, { + exact: true, + }), + ).toHaveCount(2); + + const { teamBadgeColor } = + await manageColonyTeams.getTeamColorBadgeColor(updatedTeamName); + + expect(teamBadgeColor).toBe(updatedColor); + expect(teamBadgeColor).not.toBe(initialColor); + + await expect( + manageColonyTeams.completedAction.getByText(updatedPurpose, { + exact: true, + }), + ).toBeVisible(); + }); +}); diff --git a/playwright/e2e/staged-payment.spec.ts b/playwright/e2e/staged-payment.spec.ts index b41be524a89..464edcf864c 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 00000000000..82a4cbfeb27 --- /dev/null +++ b/playwright/models/manage-colony-funds.ts @@ -0,0 +1,358 @@ +import { type Page, type Locator, expect } from '@playwright/test'; + +import { ReputationFlow, type ReputationFlowModel } from './reputation-flow.ts'; + +export class ManageColonyFunds implements ReputationFlowModel { + 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', + }, + }, + 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', + }, + }, + }; + + page: Page; + + 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; + + updateTokensButton: Locator; + + manageTokensTable: Locator; + + tokenSelector: Locator; + + tokenSearchMenu: Locator; + + 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'); + 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(); + 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'); + this.reputationFlow = new ReputationFlow(this); + } + + 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 + .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.setTitle(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.setTitle(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 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, + ); + } + + 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 00000000000..205a8ef1a2a --- /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 new file mode 100644 index 00000000000..ec35a56054e --- /dev/null +++ b/playwright/models/manage-colony-teams.ts @@ -0,0 +1,302 @@ +import { type Page, type Locator, expect } from '@playwright/test'; + +import { ReputationFlow } from './reputation-flow.ts'; + +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', + }, + }, + }; + + page: Page; + + 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; + + 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.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'); + this.reputationFlow = new ReputationFlow(this); + } + + 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(); + } + + 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 00000000000..fdb22bb400e --- /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 3d2f549aeec..dcbb8b2a9cc 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 e42da76a115..e466b9fd73b 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 6d1fb2fb525..48fddbb4f30 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 }) => {