Skip to content

Commit 9be90ef

Browse files
authored
test: implement modular automation setup (hashgraph#2471)
Signed-off-by: yasenltd <yasenltd@gmail.com>
1 parent fa97675 commit 9be90ef

42 files changed

Lines changed: 807 additions & 566 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const PRIVATE_KEY_ENV_VAR = 'PRIVATE_KEY';
2+
export const OPERATOR_KEY_ENV_VAR = 'OPERATOR_KEY';
3+
export const ENVIRONMENT_ENV_VAR = 'ENVIRONMENT';

automation/constants/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './environmentVariables.constants.js';
2+
export * from './transactionEnvironment.constants.js';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const LOCALNET_PAYER_ACCOUNT_ID = '0.0.1022';
2+
export const LOCALNET_OPERATOR_ACCOUNT = '0.0.2';
3+
export const DEFAULT_LOCALNET_OPERATOR_KEY =
4+
'0x91132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137';
5+
export const DEFAULT_NETWORK = 'LOCALNET';
6+
export const TESTNET = 'TESTNET';
7+
export const PREVIEWNET = 'PREVIEWNET';
8+
export const CUSTOM = 'CUSTOM';
9+
export const PAYER_ACCOUNT_NICKNAME = 'Payer Account';
10+
export const OPERATOR_ACCOUNT_NICKNAME = 'Operator Account';

automation/pages/BasePage.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ export class BasePage {
7171
contentType: 'image/png',
7272
});
7373
}
74-
75-
console.log(`[step-screenshot] ${screenshotPath}`);
7674
} catch (error) {
7775
console.error(`Failed to capture step screenshot for ${stepName}:`, error);
7876
}

automation/pages/ContactListPage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export class ContactListPage extends BasePage {
228228
const accountIds = await getAssociatedAccounts(
229229
publicKey,
230230
this.VERY_LONG_TIMEOUT,
231-
this.SHORT_TIMEOUT,
231+
this.DEFAULT_TIMEOUT,
232232
);
233233

234234
if (accountIds.length > 0) {

automation/pages/GroupPage.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,13 @@ export class GroupPage extends BasePage {
5454
transactionDuplicateButtonIndexSelector = 'button-transaction-duplicate-';
5555
transactionEditButtonIndexSelector = 'button-transaction-edit-';
5656
orgTransactionDetailsButtonIndexSelector = 'button-group-transaction-';
57+
closeModalScreenshotPrefixSelector = 'close-modal-';
5758

5859
async closeModalIfVisible(selector: string) {
5960
const modalButton = this.getElement(selector);
60-
61-
await this.waitForElementToBeVisible(selector).catch(() => {});
62-
63-
if (await modalButton.isVisible()) {
61+
if (await this.isElementVisible(selector, null, this.LONG_TIMEOUT)) {
6462
await modalButton.click();
65-
await this.captureStepScreenshot(`close-modal-${selector}`);
63+
await this.captureStepScreenshot(`${this.closeModalScreenshotPrefixSelector}${selector}`);
6664
}
6765
}
6866

@@ -322,6 +320,9 @@ export class GroupPage extends BasePage {
322320
}
323321

324322
async logInAndSignGroupTransactionsByAllUsers(encryptionPassword: string, signAll = true) {
323+
const readyToSignMaxRetries = 30;
324+
const readyToSignRetryDelayMs = this.DEFAULT_TIMEOUT;
325+
325326
for (let i = 1; i < this.organizationPage.users.length; i++) {
326327
console.log(`Signing transaction for user ${i}`);
327328
const user = this.organizationPage.users[i];
@@ -333,13 +334,13 @@ export class GroupPage extends BasePage {
333334
// Backend cache linking can take 10-30s+ depending on mirror node latency
334335
const found = await this.waitForTransactionInTab(
335336
this.organizationPage.readyToSignTabSelector,
336-
30, // Max 30 retries (increased from 15)
337-
2000, // 2 seconds between retries = max 60s wait
337+
readyToSignMaxRetries,
338+
readyToSignRetryDelayMs,
338339
);
339340

340341
if (!found) {
341342
throw new Error(
342-
`User ${i} (${user.email}) could not find transaction in Ready to Sign tab after 30 retries (60s timeout)`,
343+
`User ${i} (${user.email}) could not find a transaction in Ready to Sign after ${readyToSignMaxRetries} attempts with a ${readyToSignRetryDelayMs}ms retry delay`,
343344
);
344345
}
345346

@@ -378,8 +379,6 @@ export class GroupPage extends BasePage {
378379
}
379380
} while (true);
380381
}
381-
382-
// Wait for backend to process signatures before next user logs in
383382
await this.waitForElementToDisappear(this.toastMessageSelector);
384383
await this.organizationPage.logoutFromOrganization();
385384
}

automation/pages/OrganizationPage.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
setupEnvironmentForTransactions,
1212
waitForValidStart,
1313
getPrivateKeyEnv
14-
} from '../utils/util.js';
14+
} from '../utils/automationSupport.js';
1515
import { createTestUsersBatch } from '../utils/databaseUtil.js';
1616
import { Mnemonic } from '@hashgraph/sdk';
1717
import {
@@ -523,7 +523,7 @@ export class OrganizationPage extends BasePage {
523523
const currentNickname = await this.getOrganizationNicknameText();
524524
if (currentNickname !== newNickname) {
525525
await this.clickOnEditNicknameOrganizationButton();
526-
await new Promise(resolve => setTimeout(resolve, 500));
526+
await new Promise(resolve => setTimeout(resolve, this.SHORT_TIMEOUT));
527527
await this.fillInNewOrganizationNickname(newNickname);
528528
await this.settingsPage.clickOnOrganisationsTab();
529529
retries++;
@@ -861,7 +861,7 @@ export class OrganizationPage extends BasePage {
861861
// Retrieve and store the transaction ID
862862
const transactionId = (await this.getTransactionDetailsId()) ?? '';
863863
await this.clickOnSignTransactionButton();
864-
await new Promise(resolve => setTimeout(resolve, 6000));
864+
await new Promise(resolve => setTimeout(resolve, this.LONG_TIMEOUT));
865865

866866
// Store the complex account ID
867867
const transactionResponse =
@@ -1527,7 +1527,7 @@ export class OrganizationPage extends BasePage {
15271527
getter: (index: number) => Promise<any>;
15281528
}>,
15291529
maxRetries = 10,
1530-
retryDelay = 500,
1530+
retryDelay = this.SHORT_TIMEOUT,
15311531
): Promise<{
15321532
transactionId: string | null;
15331533
transactionType: string | null;
@@ -1619,8 +1619,8 @@ export class OrganizationPage extends BasePage {
16191619

16201620
async clickOnSubmitSignButtonByTransactionId(
16211621
transactionId: string,
1622-
maxRetries = 10,
1623-
retryDelay = 1000,
1622+
maxRetries = 20,
1623+
retryDelay = this.SHORT_TIMEOUT,
16241624
) {
16251625
for (let attempt = 0; attempt < maxRetries; attempt++) {
16261626
const count = await this.countElements(this.transactionNodeTransactionIdIndexSelector);
@@ -1638,8 +1638,8 @@ export class OrganizationPage extends BasePage {
16381638

16391639
async clickOnReadyToSignDetailsButtonByTransactionId(
16401640
transactionId: string,
1641-
maxRetries = 10,
1642-
retryDelay = 1000,
1641+
maxRetries = 20,
1642+
retryDelay = this.SHORT_TIMEOUT,
16431643
) {
16441644
for (let attempt = 0; attempt < maxRetries; attempt++) {
16451645
const count = await this.countElements(this.transactionNodeTransactionIdIndexSelector);
@@ -1657,8 +1657,8 @@ export class OrganizationPage extends BasePage {
16571657

16581658
async clickOnReadyForExecutionDetailsButtonByTransactionId(
16591659
transactionId: string,
1660-
maxRetries = 10,
1661-
retryDelay = 1000,
1660+
maxRetries = 20,
1661+
retryDelay = this.SHORT_TIMEOUT,
16621662
) {
16631663
for (let attempt = 0; attempt < maxRetries; attempt++) {
16641664
const count = await this.countElements(this.transactionNodeTransactionIdIndexSelector);
@@ -1675,8 +1675,8 @@ export class OrganizationPage extends BasePage {
16751675

16761676
async clickOnHistoryDetailsButtonByTransactionId(
16771677
transactionId: string,
1678-
maxRetries = 10,
1679-
retryDelay = 1000,
1678+
maxRetries = 20,
1679+
retryDelay = this.SHORT_TIMEOUT,
16801680
) {
16811681
for (let attempt = 0; attempt < maxRetries; attempt++) {
16821682
const count = await this.countElements(this.transactionNodeTransactionIdIndexSelector);
@@ -1699,7 +1699,7 @@ export class OrganizationPage extends BasePage {
16991699
return true;
17001700
}
17011701
}
1702-
await new Promise(resolve => setTimeout(resolve, 500));
1702+
await new Promise(resolve => setTimeout(resolve, this.SHORT_TIMEOUT));
17031703
}
17041704
return false;
17051705
}
@@ -1712,7 +1712,7 @@ export class OrganizationPage extends BasePage {
17121712
return true;
17131713
}
17141714
}
1715-
await new Promise(resolve => setTimeout(resolve, 500));
1715+
await new Promise(resolve => setTimeout(resolve, this.SHORT_TIMEOUT));
17161716
}
17171717
return false;
17181718
}
@@ -1799,7 +1799,7 @@ export class OrganizationPage extends BasePage {
17991799
const status = await getLatestInAppNotificationStatusByEmail(secondUser.email);
18001800
return status !== null && !status.isRead;
18011801
},
1802-
{ timeout: 10000, intervals: [500] },
1802+
{ timeout: this.LONG_TIMEOUT * 2, intervals: [this.SHORT_TIMEOUT] },
18031803
)
18041804
.toBe(true);
18051805
}

automation/pages/TransactionPage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
getCleanAccountId,
1212
isLocalnetEnvironment,
1313
LOCALNET_PAYER_ACCOUNT_ID,
14-
} from '../utils/util.js';
14+
} from '../utils/automationSupport.js';
1515
import { Transaction } from '../../front-end/src/shared/interfaces/index.js';
1616
import * as path from 'node:path';
1717

@@ -206,7 +206,7 @@ export class TransactionPage extends BasePage {
206206
const accountDetails = await getAccountDetails(
207207
accountId,
208208
this.VERY_LONG_TIMEOUT,
209-
this.SHORT_TIMEOUT,
209+
this.DEFAULT_TIMEOUT,
210210
);
211211
console.log('Account Details:', accountDetails);
212212
return accountDetails;
@@ -216,7 +216,7 @@ export class TransactionPage extends BasePage {
216216
const transactionDetails = await getTransactionDetails(
217217
transactionId,
218218
this.VERY_LONG_TIMEOUT * 2,
219-
this.SHORT_TIMEOUT,
219+
this.DEFAULT_TIMEOUT,
220220
);
221221
const firstTransaction = transactionDetails.transactions.find(
222222
(tx: Transaction) => typeof tx.nonce === 'undefined' || tx.nonce === 0,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { ElectronApplication, Page } from '@playwright/test';
2+
import { launchHederaTransactionTool as defaultLaunchHederaTransactionTool } from '../utils/electronAppLauncher.js';
3+
import { migrationDataExists as defaultMigrationDataExists } from '../utils/oldTools.js';
4+
import { LoginPage as DefaultLoginPage } from '../pages/LoginPage.js';
5+
6+
export const asciiArt =
7+
'\n' +
8+
' ________ __ __ ________ ______ ______ __ ______ __ __ ________ ______ __ __ ______ ________ ______ ______ __ __ \n' +
9+
'/ / | / | / / \\ / \\/ | / \\/ | / / / \\/ \\ / |/ \\/ / |/ \\/ \\ / |\n' +
10+
'$$$$$$$$/$$ | $$ | $$$$$$$$/$$$$$$ /$$$$$$ $$ | /$$$$$$ $$ | $$ $$$$$$$$/$$$$$$ $$ \\ /$$ /$$$$$$ $$$$$$$$/$$$$$$//$$$$$$ $$ \\ $$ |\n' +
11+
' $$ | $$ \\/$$/ $$ | $$ | $$ $$ | $$ $$ | $$ |__$$ $$ | $$ | $$ | $$ | $$ $$$ \\ /$$$ $$ |__$$ | $$ | $$ | $$ | $$ $$$ \\$$ |\n' +
12+
' $$ | $$ $$< $$ | $$ | $$ $$ | $$ $$ | $$ $$ $$ | $$ | $$ | $$ | $$ $$$$ /$$$$ $$ $$ | $$ | $$ | $$ | $$ $$$$ $$ |\n' +
13+
' $$ | $$$$ \\ $$ | $$ | $$ $$ | $$ $$ | $$$$$$$$ $$ | $$ | $$ | $$ | $$ $$ $$ $$/$$ $$$$$$$$ | $$ | $$ | $$ | $$ $$ $$ $$ |\n' +
14+
' $$ | $$ /$$ | $$ | $$ \\__$$ $$ \\__$$ $$ |_____ $$ | $$ $$ \\__$$ | $$ | $$ \\__$$ $$ |$$$/ $$ $$ | $$ | $$ | _$$ |_$$ \\__$$ $$ |$$$$ |\n' +
15+
' $$ | $$ | $$ | $$ | $$ $$/$$ $$/$$ | $$ | $$ $$ $$/ $$ | $$ $$/$$ | $/ $$ $$ | $$ | $$ | / $$ $$ $$/$$ | $$$ |\n' +
16+
' $$/ $$/ $$/ $$/ $$$$$$/ $$$$$$/ $$$$$$$$/ $$/ $$/ $$$$$$/ $$/ $$$$$$/ $$/ $$/$$/ $$/ $$/ $$$$$$/ $$$$$$/ $$/ $$/ \n';
17+
18+
type LoginPageConstructor = new (window: Page) => DefaultLoginPage;
19+
20+
interface AppBootstrapperDependencies {
21+
launchHederaTransactionTool?: typeof defaultLaunchHederaTransactionTool;
22+
migrationDataExists?: typeof defaultMigrationDataExists;
23+
LoginPage?: LoginPageConstructor;
24+
}
25+
26+
export interface SetupAppResult {
27+
app: ElectronApplication;
28+
window: Page;
29+
}
30+
31+
export class AppBootstrapper {
32+
private readonly launchApp: typeof defaultLaunchHederaTransactionTool;
33+
private readonly canMigrateData: typeof defaultMigrationDataExists;
34+
private readonly LoginPageClass: LoginPageConstructor;
35+
36+
constructor({
37+
launchHederaTransactionTool = defaultLaunchHederaTransactionTool,
38+
migrationDataExists = defaultMigrationDataExists,
39+
LoginPage = DefaultLoginPage,
40+
}: AppBootstrapperDependencies = {}) {
41+
this.launchApp = launchHederaTransactionTool;
42+
this.canMigrateData = migrationDataExists;
43+
this.LoginPageClass = LoginPage;
44+
}
45+
46+
async setupApp(): Promise<SetupAppResult> {
47+
console.log(asciiArt);
48+
49+
console.log('[setupApp] Launching Hedera Transaction Tool...');
50+
const app = await this.launchApp();
51+
52+
const window = await app.firstWindow();
53+
await window.waitForLoadState('domcontentloaded');
54+
55+
const loginPage = this.createLoginPage(window);
56+
57+
console.log('[setupApp] Clearing browser storage...');
58+
await window.evaluate(() => {
59+
globalThis.localStorage.clear();
60+
globalThis.sessionStorage.clear();
61+
});
62+
63+
console.log('[setupApp] Handling startup modals...');
64+
await loginPage.closeImportantNoteModal();
65+
66+
const canMigrate = await this.canMigrateData(app);
67+
console.log(`[setupApp] migrationDataExists: ${canMigrate}`);
68+
69+
if (canMigrate) {
70+
await loginPage.closeMigrationModal();
71+
}
72+
73+
if (process.platform === 'darwin') {
74+
console.log('[setupApp] macOS detected -> checking Keychain modal...');
75+
await loginPage.closeKeyChainModal();
76+
}
77+
78+
console.log('[setupApp] Checking if existing user session exists...');
79+
const isSettingsButtonVisible = await loginPage.isSettingsButtonVisible();
80+
console.log(`[setupApp] isSettingsButtonVisible: ${isSettingsButtonVisible}`);
81+
82+
if (isSettingsButtonVisible) {
83+
console.log('[setupApp] Existing session detected -> resetting app state...');
84+
await this.resetAppState(window, app);
85+
}
86+
87+
console.log('[setupApp] App ready.');
88+
89+
return { app, window };
90+
}
91+
92+
async resetAppState(window: Page, app: ElectronApplication): Promise<void> {
93+
const loginPage = this.createLoginPage(window);
94+
await loginPage.resetState();
95+
const canMigrate = await this.canMigrateData(app);
96+
if (canMigrate) {
97+
await loginPage.closeMigrationModal();
98+
}
99+
}
100+
101+
async closeApp(app: ElectronApplication): Promise<void> {
102+
await app.close();
103+
}
104+
105+
private createLoginPage(window: Page): DefaultLoginPage {
106+
return new this.LoginPageClass(window);
107+
}
108+
}

0 commit comments

Comments
 (0)