diff --git a/.gitignore b/.gitignore index 17f558947..ce7cb9890 100644 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,4 @@ automation/blob-report/ automation/playwright/.cache/ automation/.env automation/data/*.bin +automation/data/*.csv diff --git a/automation/README.md b/automation/README.md index b3ae21426..af6e6d7a3 100644 --- a/automation/README.md +++ b/automation/README.md @@ -139,3 +139,9 @@ npx playwright test tests/SystemFileTests ```bash npx playwright test tests/GroupTransactionTests ``` + +### 13. Group organization tests + +```bash +npx playwright test tests/OrganizationGroupTests +``` diff --git a/automation/pages/GroupPage.js b/automation/pages/GroupPage.js index 433200876..e09a00a57 100644 --- a/automation/pages/GroupPage.js +++ b/automation/pages/GroupPage.js @@ -1,12 +1,16 @@ +const path = require('path'); const BasePage = require('./BasePage'); const TransactionPage = require('./TransactionPage'); +const OrganizationPage = require('./OrganizationPage'); const { getTransactionGroupsForTransactionId } = require('../utils/databaseQueries'); +const { generateCSVFile } = require('../utils/csvGenerator'); class GroupPage extends BasePage { constructor(window) { super(window); this.window = window; this.transactionPage = new TransactionPage(window); + this.organizationPage = new OrganizationPage(window); } /* Selectors */ @@ -24,10 +28,13 @@ class GroupPage extends BasePage { deleteAllButtonSelector = 'button-delete-all'; confirmDeleteAllButtonSelector = 'button-confirm-delete-all'; confirmGroupTransactionButtonSelector = 'button-confirm-group-transaction'; + detailsGroupButtonSelector = 'button-group-details'; + importCsvButtonSelector = 'button-import-csv'; - // Messages + // Text toastMessageSelector = '.v-toast__text'; emptyTransactionTextSelector = 'p-empty-transaction-text'; + transactionGroupDetailsIdSelector = 'td-group-transaction-id'; // Inputs descriptionInputSelector = 'input-transaction-group-description'; @@ -38,6 +45,7 @@ class GroupPage extends BasePage { transactionDeleteButtonIndexSelector = 'button-transaction-delete-'; transactionDuplicateButtonIndexSelector = 'button-transaction-duplicate-'; transactionEditButtonIndexSelector = 'button-transaction-edit-'; + orgTransactionDetailsButtonIndexSelector = 'button-group-transaction-'; async closeModalIfVisible(selector) { const modalButton = this.window.getByTestId(selector); @@ -121,6 +129,31 @@ class GroupPage extends BasePage { return await this.getText(this.transactionTimestampIndexSelector + index); } + async getTransactionGroupDetailsId(index) { + return await this.getText(this.transactionGroupDetailsIdSelector, index); + } + + async getAllTransactionTimestamps(numberOfTransactions) { + const timestamps = []; + for (let i = 0; i < numberOfTransactions; i++) { + timestamps.push(await this.getTransactionGroupDetailsId(i)); + } + return timestamps; + } + + async verifyAllTransactionsAreSuccessful(timestampsForVerification) { + for (let i = 0; i < timestampsForVerification.length; i++) { + const transactionDetails = await this.transactionPage.mirrorGetTransactionResponse( + timestampsForVerification[i], + ); + const result = transactionDetails.transactions[0]?.result; + if (result !== 'SUCCESS') { + return false; + } + } + return true; + } + async clickTransactionDeleteButton(index) { await this.click(this.transactionDeleteButtonIndexSelector + index); } @@ -153,6 +186,39 @@ class GroupPage extends BasePage { } } + async generateAndImportCsvFile(fromAccountId, numberOfTransactions = 10) { + const fileName = 'groupTransactions.csv'; + const receiverAccount = '0.0.1031'; + await generateCSVFile({ + senderAccount: fromAccountId, + accountId: receiverAccount, + startingAmount: 1, + numberOfTransactions: numberOfTransactions, + fileName: fileName, + }); + await this.uploadFile( + this.importCsvButtonSelector, + path.resolve(__dirname, '..', 'data', fileName), + ); + } + + async addOrgAllowanceTransactionToGroup(numberOfTransactions = 1, allowanceOwner, amount) { + await this.fillDescription('test'); + for (let i = 0; i < numberOfTransactions; i++) { + await this.clickOnAddTransactionButton(); + await this.transactionPage.clickOnApproveAllowanceTransaction(); + + await this.transactionPage.fillInAllowanceOwner(allowanceOwner); + await this.transactionPage.fillInAllowanceAmount(amount); + await this.transactionPage.fillInSpenderAccountId( + await this.getTextFromInputField(this.transactionPage.payerDropdownSelector), + this.addToGroupButtonSelector, + ); + + await this.clickAddToGroupButton(); + } + } + async isEmptyTransactionTextVisible() { return this.isElementVisible(this.emptyTransactionTextSelector); } @@ -178,6 +244,81 @@ class GroupPage extends BasePage { async doTransactionGroupsExist(transactionId) { return !!(await getTransactionGroupsForTransactionId(transactionId)); } + + async clickOnDetailsGroupButton() { + await this.click(this.detailsGroupButtonSelector); + } + + async clickOnTransactionDetailsButton(index) { + await this.click(this.orgTransactionDetailsButtonIndexSelector + index); + } + + async logInAndSignGroupTransactionsByAllUsers(encryptionPassword) { + for (let i = 1; i < this.organizationPage.users.length; i++) { + console.log(`Signing transaction for user ${i}`); + const user = this.organizationPage.users[i]; + await this.organizationPage.signInOrganization(user.email, user.password, encryptionPassword); + await this.transactionPage.clickOnTransactionsMenuButton(); + await this.organizationPage.clickOnReadyToSignTab(); + await this.clickOnDetailsGroupButton(); + await this.clickOnTransactionDetailsButton(0); + + // Initially sign the first transaction + await this.organizationPage.clickOnSignTransactionButton(); + + // After signing, we check if there's a "Next" button to continue to the next transaction + let hasNext = await this.isElementVisible( + this.organizationPage.nextTransactionButtonSelector, + ); + + while (hasNext) { + // Click on the "Next" button to move to the next transaction + await this.click(this.organizationPage.nextTransactionButtonSelector); + + // Now the button transforms into "Sign" for the next transaction + // Sign this transaction as well + await this.organizationPage.clickOnSignTransactionButton(); + + // Check again if there's another transaction after this one + hasNext = await this.isElementVisible(this.organizationPage.nextTransactionButtonSelector); + } + + await this.organizationPage.logoutFromOrganization(); + } + } + + async clickOnSignAllButton(retries = 3, retryDelay = 1000) { + const selector = this.organizationPage.signAllTransactionsButtonSelector; + + for (let attempt = 1; attempt <= retries; attempt++) { + console.log(`Attempt ${attempt}/${retries} to click "Sign All" button.`); + + try { + // Attempt to click the button + await this.click(selector); + + // Wait for it to disappear (tweak timeouts as needed) + await this.waitForElementToDisappear(selector, 10000, 30000); + + // If no error was thrown, we succeeded — break out + console.log( + `Successfully clicked "Sign All" button and it disappeared on attempt #${attempt}.`, + ); + return; // or break; + } catch (error) { + console.error(`Attempt #${attempt} to click "Sign All" button failed: ${error.message}`); + + if (attempt < retries) { + console.log(`Retrying in ${retryDelay}ms...`); + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } else { + throw new Error( + `Failed to click "Sign All" button and wait for it to disappear after ${retries} attempts.`, + ); + } + } + } + } } module.exports = GroupPage; diff --git a/automation/pages/TransactionPage.js b/automation/pages/TransactionPage.js index 2b39069cc..4c72e4495 100644 --- a/automation/pages/TransactionPage.js +++ b/automation/pages/TransactionPage.js @@ -1,3 +1,4 @@ +const path = require('path'); const BasePage = require('./BasePage'); const { getAccountDetails, getTransactionDetails } = require('../utils/mirrorNodeAPI'); const { @@ -6,7 +7,6 @@ const { verifyFileExists, } = require('../utils/databaseQueries'); const { decodeAndFlattenKeys } = require('../utils/keyUtil'); -const path = require('path'); class TransactionPage extends BasePage { constructor(window) { @@ -774,12 +774,8 @@ class TransactionPage extends BasePage { ); } - async fillInSpenderAccountId(accountId) { - await this.fillInAccountId( - accountId, - this.allowanceSpenderAccountSelector, - this.signAndSubmitButtonSelector, - ); + async fillInSpenderAccountId(accountId, buttonSelector = this.signAndSubmitButtonSelector) { + await this.fillInAccountId(accountId, this.allowanceSpenderAccountSelector, buttonSelector); } async fillInSpenderAccountIdNormally(accountId) { diff --git a/automation/tests/groupTransactionTests.test.js b/automation/tests/groupTransactionTests.test.js index fbff625bf..71bdbc275 100644 --- a/automation/tests/groupTransactionTests.test.js +++ b/automation/tests/groupTransactionTests.test.js @@ -2,7 +2,6 @@ const { test } = require('@playwright/test'); const { expect } = require('playwright/test'); const RegistrationPage = require('../pages/RegistrationPage.js'); const LoginPage = require('../pages/LoginPage'); -const SettingsPage = require('../pages/SettingsPage'); const TransactionPage = require('../pages/TransactionPage'); const GroupPage = require('../pages/GroupPage'); const { resetDbState } = require('../utils/databaseUtil'); @@ -16,7 +15,7 @@ const { let app, window; let globalCredentials = { email: '', password: '' }; -let registrationPage, loginPage, settingsPage, transactionPage, groupPage; +let registrationPage, loginPage, transactionPage, groupPage; test.describe('Group transaction tests', () => { test.beforeAll(async () => { @@ -24,7 +23,6 @@ test.describe('Group transaction tests', () => { ({ app, window } = await setupApp()); loginPage = new LoginPage(window); registrationPage = new RegistrationPage(window); - settingsPage = new SettingsPage(window); transactionPage = new TransactionPage(window); groupPage = new GroupPage(window); diff --git a/automation/tests/organizationContactListTests.test.js b/automation/tests/organizationContactListTests.test.js index e486ed877..e4018d100 100644 --- a/automation/tests/organizationContactListTests.test.js +++ b/automation/tests/organizationContactListTests.test.js @@ -14,7 +14,6 @@ const OrganizationPage = require('../pages/OrganizationPage'); const SettingsPage = require('../pages/SettingsPage'); const ContactListPage = require('../pages/ContactListPage'); const { resetDbState, resetPostgresDbState } = require('../utils/databaseUtil'); -const { getAssociatedAccounts } = require('../utils/mirrorNodeAPI'); let app, window; let globalCredentials = { email: '', password: '' }; diff --git a/automation/tests/organizationGroupTests.test.js b/automation/tests/organizationGroupTests.test.js new file mode 100644 index 000000000..1cffc9f4a --- /dev/null +++ b/automation/tests/organizationGroupTests.test.js @@ -0,0 +1,163 @@ +const { test } = require('@playwright/test'); +const { expect } = require('playwright/test'); +const RegistrationPage = require('../pages/RegistrationPage.js'); +const LoginPage = require('../pages/LoginPage'); +const TransactionPage = require('../pages/TransactionPage'); +const OrganizationPage = require('../pages/OrganizationPage'); +const GroupPage = require('../pages/GroupPage'); +const { resetDbState, resetPostgresDbState } = require('../utils/databaseUtil'); +const { disableNotificationsForTestUsers } = require('../utils/databaseQueries'); +const { + setupApp, + closeApp, + generateRandomEmail, + generateRandomPassword, + setupEnvironmentForTransactions, +} = require('../utils/util'); + +let app, window; +let globalCredentials = { email: '', password: '' }; +let registrationPage, loginPage, transactionPage, organizationPage, groupPage; +let firstUser, secondUser, thirdUser; +let complexKeyAccountId; + +test.describe('Organization Group Tx tests', () => { + test.beforeAll(async () => { + test.slow(); + await resetDbState(); + await resetPostgresDbState(); + ({ app, window } = await setupApp()); + loginPage = new LoginPage(window); + transactionPage = new TransactionPage(window); + organizationPage = new OrganizationPage(window); + registrationPage = new RegistrationPage(window); + groupPage = new GroupPage(window); + + organizationPage.complexFileId = []; + + // Generate credentials and store them globally + globalCredentials.email = generateRandomEmail(); + globalCredentials.password = generateRandomPassword(); + + // Generate test users in PostgreSQL database for organizations + await organizationPage.createUsers(3); + + // Perform registration with the generated credentials + await registrationPage.completeRegistration( + globalCredentials.email, + globalCredentials.password, + ); + + await setupEnvironmentForTransactions(window); + + // Setup Organization + await organizationPage.setupOrganization(); + await organizationPage.setUpUsers(window, globalCredentials.password); + + // Disable notifications for test users + await disableNotificationsForTestUsers(); + + firstUser = organizationPage.getUser(0); + secondUser = organizationPage.getUser(1); + thirdUser = organizationPage.getUser(2); + await organizationPage.signInOrganization( + firstUser.email, + firstUser.password, + globalCredentials.password, + ); + + // Set complex account for transactions + await organizationPage.addComplexKeyAccountForTransactions(); + + complexKeyAccountId = organizationPage.getComplexAccountId(); + groupPage.organizationPage = organizationPage; + await transactionPage.clickOnTransactionsMenuButton(); + await organizationPage.logoutFromOrganization(); + }); + + test.beforeEach(async () => { + await organizationPage.signInOrganization( + firstUser.email, + firstUser.password, + globalCredentials.password, + ); + + await transactionPage.clickOnTransactionsMenuButton(); + + //this is needed because tests fail in CI environment + if (process.env.CI) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + await groupPage.closeDraftTransactionModal(); + await groupPage.closeGroupDraftModal(); + await groupPage.deleteGroupModal(); + + await groupPage.navigateToGroupTransaction(); + }); + + test.afterEach(async () => { + await organizationPage.logoutFromOrganization(); + }); + + test.afterAll(async () => { + await closeApp(app); + await resetDbState(); + await resetPostgresDbState(); + }); + + test('Verify user can execute group transaction in organization', async () => { + test.slow(); + await groupPage.addOrgAllowanceTransactionToGroup(2, complexKeyAccountId, '10'); + const txId = await groupPage.getTransactionTimestamp(0); + const secondTxId = await groupPage.getTransactionTimestamp(1); + + await groupPage.clickOnSignAndExecuteButton(); + await groupPage.clickOnConfirmGroupTransactionButton(); + await groupPage.clickOnSignAllButton(); + await loginPage.waitForToastToDisappear(); + await transactionPage.clickOnTransactionsMenuButton(); + await organizationPage.logoutFromOrganization(); + await groupPage.logInAndSignGroupTransactionsByAllUsers(globalCredentials.password); + await organizationPage.signInOrganization( + firstUser.email, + firstUser.password, + globalCredentials.password, + ); + + const transactionDetails = await transactionPage.mirrorGetTransactionResponse(txId); + const transactionType = transactionDetails.transactions[0]?.name; + const result = transactionDetails.transactions[0]?.result; + expect(transactionType).toBe('CRYPTOAPPROVEALLOWANCE'); + expect(result).toBe('SUCCESS'); + + const secondTransactionDetails = await transactionPage.mirrorGetTransactionResponse(secondTxId); + const secondTransactionType = secondTransactionDetails.transactions[0]?.name; + const secondResult = secondTransactionDetails.transactions[0]?.result; + expect(secondTransactionType).toBe('CRYPTOAPPROVEALLOWANCE'); + expect(secondResult).toBe('SUCCESS'); + }); + + test('Verify user can import csv transactions', async () => { + test.slow(); + const numberOfTransactions = 5; + await groupPage.fillDescription('test'); + await groupPage.generateAndImportCsvFile(complexKeyAccountId, numberOfTransactions); + await groupPage.clickOnSignAndExecuteButton(); + await groupPage.clickOnConfirmGroupTransactionButton(); + const timestamps = await groupPage.getAllTransactionTimestamps(numberOfTransactions); + await groupPage.clickOnSignAllButton(); + await loginPage.waitForToastToDisappear(); + await transactionPage.clickOnTransactionsMenuButton(); + await organizationPage.logoutFromOrganization(); + await groupPage.logInAndSignGroupTransactionsByAllUsers(globalCredentials.password); + await organizationPage.signInOrganization( + firstUser.email, + firstUser.password, + globalCredentials.password, + ); + const isAllTransactionsSuccessful = + await groupPage.verifyAllTransactionsAreSuccessful(timestamps); + expect(isAllTransactionsSuccessful).toBe(true); + }); +}); diff --git a/automation/utils/csvGenerator.js b/automation/utils/csvGenerator.js new file mode 100644 index 000000000..cc1ddb2ca --- /dev/null +++ b/automation/utils/csvGenerator.js @@ -0,0 +1,58 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Generates a CSV file with the specified configuration for transaction groups. + * + * @param {string} senderAccount - The sender account in the format "0.0.xxxx". + * @param {string} accountId - The account ID for the transaction rows, "0.0.xxxx". + * @param {number} startingAmount - The amount to start with for the first line. + * @param {number} numberOfTransactions - The number of transactions in the group. + * @param {string} [fileName='output.csv'] - The name of the CSV file to create. + * @param {string} [date='9/4/24'] - The date to use for each transaction line. + * @param {string} [senderTime='14:35'] - The sending time (static or configurable). + */ +async function generateCSVFile({ + senderAccount = '0.0.1031', + accountId = '0.0.1030', + startingAmount = 1, + numberOfTransactions = 5, + fileName = 'output.csv', + date = '9/4/24', + senderTime = '14:35', +} = {}) { + // Construct the CSV lines + // Header lines + const lines = [ + `Sender Account,${senderAccount},,`, + `Sending Time,${senderTime},,`, + `Node IDs,,,`, + `AccountID,Amount,Start Date,memo`, + ]; + + // Amounts increment by 1 each line + for (let i = 0; i < numberOfTransactions; i++) { + const amount = startingAmount + i; + const memo = `memo line ${i}`; + lines.push(`${accountId},${amount},${date},${memo}`); + } + + // Join all lines + const csvContent = lines.join('\n'); + + // Ensure the data directory exists + const dataDirectory = path.resolve(__dirname, '../data'); + if (!fs.existsSync(dataDirectory)) { + fs.mkdirSync(dataDirectory, { recursive: true }); + } + + // Write the file + const filePath = path.resolve(dataDirectory, fileName); + fs.writeFileSync(filePath, csvContent, 'utf8'); + + console.log(`CSV file generated at: ${filePath}`); +} + +module.exports = { + generateCSVFile, +}; diff --git a/front-end/src/renderer/pages/CreateTransactionGroup/CreateTransactionGroup.vue b/front-end/src/renderer/pages/CreateTransactionGroup/CreateTransactionGroup.vue index 73211bf3f..0de512925 100644 --- a/front-end/src/renderer/pages/CreateTransactionGroup/CreateTransactionGroup.vue +++ b/front-end/src/renderer/pages/CreateTransactionGroup/CreateTransactionGroup.vue @@ -410,7 +410,7 @@ onBeforeRouteLeave(async to => {
- Import CSV
@@ -484,10 +484,13 @@ onBeforeRouteLeave(async to => {
+ -
- -
+ - diff --git a/front-end/src/renderer/pages/Transactions/components/History.vue b/front-end/src/renderer/pages/Transactions/components/History.vue index 17030ad5e..09ebd3c92 100644 --- a/front-end/src/renderer/pages/Transactions/components/History.vue +++ b/front-end/src/renderer/pages/Transactions/components/History.vue @@ -514,7 +514,9 @@ watch( diff --git a/front-end/src/renderer/pages/Transactions/components/InProgress.vue b/front-end/src/renderer/pages/Transactions/components/InProgress.vue index cb1e79b51..260fc92ac 100644 --- a/front-end/src/renderer/pages/Transactions/components/InProgress.vue +++ b/front-end/src/renderer/pages/Transactions/components/InProgress.vue @@ -359,7 +359,9 @@ watch([currentPage, pageSize, () => user.selectedOrganization], async () => { diff --git a/front-end/src/renderer/pages/Transactions/components/ReadyForExecution.vue b/front-end/src/renderer/pages/Transactions/components/ReadyForExecution.vue index 8078deb90..2f6549f47 100644 --- a/front-end/src/renderer/pages/Transactions/components/ReadyForExecution.vue +++ b/front-end/src/renderer/pages/Transactions/components/ReadyForExecution.vue @@ -306,7 +306,9 @@ watch( diff --git a/front-end/src/renderer/pages/Transactions/components/ReadyForReview.vue b/front-end/src/renderer/pages/Transactions/components/ReadyForReview.vue index 3ae28645c..8f8a71a82 100644 --- a/front-end/src/renderer/pages/Transactions/components/ReadyForReview.vue +++ b/front-end/src/renderer/pages/Transactions/components/ReadyForReview.vue @@ -376,7 +376,9 @@ watch( diff --git a/front-end/src/renderer/pages/Transactions/components/ReadyToSign.vue b/front-end/src/renderer/pages/Transactions/components/ReadyToSign.vue index 58ba97224..922db6e31 100644 --- a/front-end/src/renderer/pages/Transactions/components/ReadyToSign.vue +++ b/front-end/src/renderer/pages/Transactions/components/ReadyToSign.vue @@ -323,7 +323,10 @@ watch( }} - Details @@ -412,7 +415,9 @@ watch(