diff --git a/package.json b/package.json index 04043943..27c48560 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,9 @@ "branches": [ "main", { - "name": "develop", + "name": "okx-wallet", "channel": "alpha", - "prerelease": "alpha" + "prerelease": "okx" } ] } diff --git a/packages/wallets/src/metamask/metamask.page.ts b/packages/wallets/src/metamask/metamask.page.ts index 82122392..9ae67f3e 100644 --- a/packages/wallets/src/metamask/metamask.page.ts +++ b/packages/wallets/src/metamask/metamask.page.ts @@ -12,12 +12,12 @@ import { AccountMenu, } from './pages/elements'; import { getAddress } from 'viem'; +import { isNetworkPopular } from './services/service'; export class MetamaskPage implements WalletPage { page: Page | undefined; header: Header; homePage: HomePage; - settingsPage: SettingsPage; loginPage: LoginPage; walletOperation: WalletOperationPage; onboardingPage: OnboardingPage; @@ -35,11 +35,6 @@ export class MetamaskPage implements WalletPage { this.page = await this.browserContext.newPage(); this.header = new Header(this.page); this.homePage = new HomePage(this.page, this.extensionUrl, this.config); - this.settingsPage = new SettingsPage( - this.page, - this.extensionUrl, - this.config, - ); this.loginPage = new LoginPage(this.page, this.config); this.walletOperation = new WalletOperationPage(this.page); this.onboardingPage = new OnboardingPage(this.page, this.config); @@ -70,7 +65,11 @@ export class MetamaskPage implements WalletPage { await this.popoverElements.closePopover(); await this.walletOperation.cancelAllTxInQueue(); // reject all tx in queue if exist } - await this.settingsPage.setupNetworkChangingSetting(); // need to make it possible to change the wallet network + await new SettingsPage( + await this.browserContext.newPage(), + this.extensionUrl, + this.config, + ).setupNetworkChangingSetting(); // need to make it possible to change the wallet network }); } @@ -79,21 +78,13 @@ export class MetamaskPage implements WalletPage { await this.navigate(); await this.header.networkListButton.click(); await this.header.networkList.clickToNetwork(networkName); - if (networkName === 'Linea Mainnet') { + if (networkName === 'Linea') { await this.popoverElements.closePopover(); //Linea network require additional confirmation } await this.page.close(); }); } - async switchNetwork(networkName = 'Linea Mainnet') { - await test.step(`Switch network to "${networkName}"`, async () => { - await this.navigate(); - await this.header.networkList.switchNetwork(networkName); - await this.page.close(); - }); - } - async setupNetwork(standConfig: Record) { await test.step(`Setup "${standConfig.chainName}" Network`, async () => { await this.header.networkListButton.click(); @@ -130,32 +121,21 @@ export class MetamaskPage implements WalletPage { ) { await test.step(`Add new network "${networkName}"`, async () => { await this.navigate(); - await this.header.networkList.addNetworkManually( - networkName, - networkUrl, - chainId, - tokenSymbol, - blockExplorer, - ); + if (await isNetworkPopular(networkName)) { + await this.header.networkList.addPopularNetwork(networkName); + } else { + await this.header.networkList.addNetworkManually( + networkName, + networkUrl, + chainId, + tokenSymbol, + blockExplorer, + ); + } if (isClosePage) await this.page.close(); }); } - async addPopularNetwork(networkName: string) { - await this.navigate(); - await this.header.networkListButton.click(); - const networkListText = await this.header.networkList.getNetworkListText(); - if (networkListText.includes(networkName)) { - await this.header.networkList.clickToNetworkItemButton(networkName); - } else { - await test.step(`Add popular network "${networkName}"`, async () => { - await this.header.networkList.networkDisplayCloseBtn.click(); - await this.header.networkList.addPopularNetwork(networkName); - }); - } - await this.page.close(); - } - async importKey(key: string) { await test.step('Import key', async () => { await this.navigate(); @@ -217,10 +197,14 @@ export class MetamaskPage implements WalletPage { .toString() .trim(); if (tokenNameFromValue === tokenName) { - tokenBalance = parseFloat(await value.textContent()); + await value.click(); + tokenBalance = parseFloat( + await this.homePage.tokensListItemValues.textContent(), + ); break; } } + await this.page.close(); return tokenBalance; }); } diff --git a/packages/wallets/src/metamask/pages/elements/networkList.element.ts b/packages/wallets/src/metamask/pages/elements/networkList.element.ts index 18f7de90..b1beda4a 100644 --- a/packages/wallets/src/metamask/pages/elements/networkList.element.ts +++ b/packages/wallets/src/metamask/pages/elements/networkList.element.ts @@ -119,28 +119,40 @@ export class NetworkList { } async addPopularNetwork(networkName: string) { - await test.step(`Open the form to add the popular network (${networkName})`, async () => { - await this.networkListButton.click(); - }); - await test.step(`Add the "${networkName}" network`, async () => { - await this.dialogSection - .getByText(networkName) - .locator('../../..') - .locator('button:has-text("Add")') - .click(); - // Without awaiting the button is not clickable - await this.page.waitForTimeout(500); - await this.approveAddNetworkButton.click(); - await this.dialogSection.waitFor({ state: 'hidden' }); - // Need to wait while the network to be added to the wallet - try { - await this.page - .getByText('Connecting to') - .waitFor({ state: 'visible', timeout: 5000 }); - await this.page.getByText('Connecting to').waitFor({ state: 'hidden' }); - } catch { - console.error('Connecting network was without loader'); - } - }); + await this.networkListButton.click(); + const networkListText = await this.getNetworkListText(); + if (networkListText.includes(networkName)) { + await this.clickToNetworkItemButton(networkName); + } else { + await test.step(`Add popular network "${networkName}"`, async () => { + await this.networkDisplayCloseBtn.click(); + await test.step(`Open the form to add the popular network (${networkName})`, async () => { + await this.networkListButton.click(); + }); + await test.step(`Add the "${networkName}" network`, async () => { + await this.dialogSection + .getByText(networkName) + .locator('../../..') + .locator('button:has-text("Add")') + .click(); + // Without awaiting the button is not clickable + await this.page.waitForTimeout(500); + await this.approveAddNetworkButton.click(); + await this.dialogSection.waitFor({ state: 'hidden' }); + // Need to wait while the network to be added to the wallet + try { + await this.page + .getByText('Connecting to') + .waitFor({ state: 'visible', timeout: 5000 }); + await this.page + .getByText('Connecting to') + .waitFor({ state: 'hidden' }); + } catch { + console.error('Connecting network was without loader'); + } + }); + }); + } + await this.page.close(); } } diff --git a/packages/wallets/src/metamask/services/service.ts b/packages/wallets/src/metamask/services/service.ts new file mode 100644 index 00000000..e2d7f1a7 --- /dev/null +++ b/packages/wallets/src/metamask/services/service.ts @@ -0,0 +1,13 @@ +const MMPopularNetworks = [ + 'zkSync Era Mainnet', + 'OP Mainnet', + 'Arbitrum One', + 'Polygon Mainnet', + 'Base Mainnet', + 'Binance Smart Chain', + 'Linea', +]; + +export async function isNetworkPopular(networkName: string) { + return MMPopularNetworks.includes(networkName); +} diff --git a/packages/wallets/src/okx/okx.page.ts b/packages/wallets/src/okx/okx.page.ts index 9ccd01af..f7f990cf 100644 --- a/packages/wallets/src/okx/okx.page.ts +++ b/packages/wallets/src/okx/okx.page.ts @@ -1,10 +1,30 @@ import { WalletConfig } from '../wallets.constants'; import { WalletPage } from '../wallet.page'; -import expect from 'expect'; -import { test, BrowserContext, Page } from '@playwright/test'; +import { test, BrowserContext, Page, expect } from '@playwright/test'; +import { + AccountList, + HomePage, + LoginPage, + ManageCryptoPage, + NetworkList, + OnboardingPage, + WalletOperations, +} from './pages'; +import { + checkNetworkName, + closeUnnecessaryPages, + isNeedAddNetwork, +} from './services/service'; export class OkxPage implements WalletPage { page: Page | undefined; + homePage: HomePage; + loginPage: LoginPage; + onboardingPage: OnboardingPage; + networkListPage: NetworkList; + manageCryptoPage: ManageCryptoPage; + accountList: AccountList; + walletOperations: WalletOperations; constructor( private browserContext: BrowserContext, @@ -12,190 +32,237 @@ export class OkxPage implements WalletPage { public config: WalletConfig, ) {} + /** Init all page objects Classes included to wallet */ + async initLocators() { + this.page = await this.browserContext.newPage(); + this.homePage = new HomePage(this.page); + this.loginPage = new LoginPage(this.page, this.config); + this.onboardingPage = new OnboardingPage(this.page, this.config); + this.networkListPage = new NetworkList(this.page); + this.manageCryptoPage = new ManageCryptoPage(this.page); + this.accountList = new AccountList(this.page); + this.walletOperations = new WalletOperations(this.page); + } + + /** Open the home page of the wallet extension */ + async goto() { + await this.page.goto( + this.extensionUrl + this.config.COMMON.EXTENSION_START_PATH, + ); + } + + /** Navigate to home page of OXK Wallet extension: + * - open the wallet extension + * - unlock extension (if needed) + * - cancel awaited transactions (if needed) + */ async navigate() { - await test.step('Navigate to okx', async () => { - this.page = await this.browserContext.newPage(); - await this.page.goto( - this.extensionUrl + this.config.COMMON.EXTENSION_START_PATH, - ); - await this.page.reload(); - await this.page.waitForTimeout(1000); - await this.unlock(); + await test.step('Navigate to OKX', async () => { + await this.initLocators(); + await this.goto(); + await this.loginPage.unlock(); + await this.walletOperations.cancelAllTxInQueue(); }); } + /** Checks the wallet is set correctly and starts ot wallet setup as the first time (if needed) */ async setup() { await test.step('Setup', async () => { await this.navigate(); - if (!this.page) throw "Page isn't ready"; try { - await this.page.waitForURL('**/initialize', { timeout: 5000 }); - const firstTime = - (await this.page - .locator("button:has-text('Import wallet')") - .count()) > 0; - if (firstTime) await this.firstTimeSetup(); + await this.onboardingPage.importWalletButton.waitFor({ + timeout: 3000, + }); + await this.onboardingPage.firstTimeSetup(); } catch { console.error('Import is not necessary'); } + await closeUnnecessaryPages(this.browserContext); }); } - async unlock() { - await test.step('Unlock', async () => { - if (!this.page) throw "Page isn't ready"; - try { - await this.page.waitForURL('**unlock', { timeout: 5000 }); - if ((await this.page.locator('id=password').count()) > 0) { - await this.page.fill('id=password', this.config.PASSWORD); - await this.page.click('text=Unlock'); - } - } catch { - console.error('Login is not needed'); - } + /** Checks the is installed the needed network and add new network to wallet (if needed) */ + async setupNetwork(standConfig: Record) { + await test.step(`Setup "${standConfig.chainName}" Network`, async () => { + await this.addNetwork( + standConfig.chainName, + standConfig.rpcUrl, + standConfig.chainId, + standConfig.tokenSymbol, + standConfig.scan, + ); }); } async importTokens(token: string) { - await test.step('Import token', async () => { - await this.navigate(); - if (!this.page) throw "Page isn't ready"; - await this.page.click("text='import tokens'"); - await this.page.click('text=Custom token'); - await this.page.type('id=custom-address', token); - }); + await this.navigate(); + await this.homePage.manageCryptoButton.click(); + await this.manageCryptoPage.importToken(token); + await this.page.close(); } - async firstTimeSetup() { - await test.step('First time setup', async () => { - if (!this.page) throw "Page isn't ready"; - await this.page.click("button:has-text('Import wallet')"); - await this.page.getByText('Import wallet').last().click(); - await this.page.click('text=Seed Phrase'); - const inputs = this.page.locator('div[data-testid="okd-popup"] >> input'); - const seedWords = this.config.SECRET_PHRASE.split(' '); - for (let i = 0; i < seedWords.length; i++) { - await inputs.nth(i).fill(seedWords[i]); - if ( - i === seedWords.length - 1 && - (await this.page - .locator( - `div[class="mnemonic-words-inputs__container__candidate-word"]`, - ) - .getByText(`${seedWords[i]}`) - .count()) > 0 - ) { - await this.page - .locator( - `div[class="mnemonic-words-inputs__container__candidate-word"]`, - ) - .getByText(`${seedWords[i]}`, { exact: true }) - .click(); - } - } - await this.page.getByRole('button', { name: 'Confirm' }).click(); - await this.page.getByRole('button', { name: 'Next' }).click(); - await this.page - .getByTestId('okd-input') - .nth(0) - .fill(this.config.PASSWORD); - await this.page - .getByTestId('okd-input') - .nth(1) - .fill(this.config.PASSWORD); - await this.page.waitForTimeout(2000); - await this.page.getByRole('button', { name: 'Confirm' }).click(); - - // Dive into wallet main page after installation - await this.page.click("button:has-text('Start your Web3 journey')"); - // Wait until extension to be loaded after installation with ETH value display. - // ETH value displayed with ETH symbol - await this.page.waitForSelector('text=ETH', { state: 'visible' }); - //Looks like after installation and load extension mainPage there we should to wait a bit for extension make sure to be installed in some memory - await this.page.waitForTimeout(2000); + /** Get token balance from wallet extension using `tokenName` */ + async getTokenBalance(tokenName: string) { + return await test.step(`Get ${tokenName} token balance`, async () => { + await this.navigate(); + const amount = parseFloat(await this.homePage.getTokenBalance(tokenName)); + await this.page.close(); + return amount; }); } + /** Add new network to wallet */ async addNetwork( networkName: string, networkUrl: string, chainId: number, tokenSymbol: string, + blockExplorer?: string, ) { - await test.step('Add network', async () => { - if (!this.page) throw "Page isn't ready"; - await this.navigate(); - await this.page.click('data-testid=account-options-menu-button'); - await this.page.click('text=Settings'); - await this.page.click("text='Networks'"); - await this.page.click('text=Add a network'); - await this.page.click("a :has-text('Add a network manually')"); - await this.page.fill( - ".form-field :has-text('Network Name') >> input", + if (!(await isNeedAddNetwork(networkName))) { + return; + } + await this.navigate(); + await this.homePage.networkListButton.click(); + if ( + !(await this.networkListPage.isNetworkExist( + await checkNetworkName(networkName), + )) + ) { + await this.networkListPage.addCustomNetwork( networkName, - ); - await this.page.fill( - ".form-field :has-text('New RPC URL') >> input", networkUrl, - ); - await this.page.fill( - ".form-field :has-text('Chain ID') >> input", - String(chainId), - ); - await this.page.fill( - ".form-field :has-text('Currency symbol') >> input", + chainId, tokenSymbol, + blockExplorer, ); - await this.page.click('text=Save'); + } + await this.page.close(); + } + + /** Switch network in the wallet + * - switch in wallet extension + * - switch in the connected dApp (switch in the extension doesn't switch the dApp network, but we do it to sync states) + * */ + async changeNetwork(networkName: string) { + await test.step(`Switch network to "${networkName}"`, async () => { + networkName = await checkNetworkName(networkName); await this.navigate(); + + // switch network for wallet + await this.homePage.networkListButton.click(); + await this.networkListPage.selectNetwork(networkName); + + // switch network for connected dApp + await this.homePage.switchNetworkForDApp(networkName); + await this.page.close(); }); } + /** To add new wallet address using wallet `privateKey` */ async importKey(key: string) { - await test.step('Import key', async () => { - if (!this.page) throw "Page isn't ready"; - await this.navigate(); - await this.page.click('data-testid=account-menu-icon'); - await this.page.click('text=Import account'); - await this.page.fill('id=private-key-box', key); - await this.page.click("text='Import'"); - }); + await this.navigate(); + await this.homePage.accountListButton.click(); + await this.accountList.importKey(key); + await this.page.close(); } - //+ + + /** Click `Confirm` button on the transaction `page` */ async connectWallet(page: Page) { - await test.step('Connect wallet', async () => { - await page.waitForSelector('button:has-text("Connect")'); - await page.waitForTimeout(10000); - await page.getByRole('button', { name: 'Connect' }).click(); - await page.close(); + await test.step('Connect OKX wallet', async () => { + const operationPage = new WalletOperations(page); + await operationPage.connectButton.waitFor({ + state: 'visible', + timeout: 10000, + }); + await operationPage.connectButton.click(); + // need wait the page to be closed after the extension is connected + await new Promise((resolve) => { + operationPage.page.on('close', () => { + resolve(); + }); + }); }); } + /** Get the `amount` from transaction and comply with the `expectedAmount` */ async assertTxAmount(page: Page, expectedAmount: string) { await test.step('Assert TX Amount', async () => { - expect(await page.textContent('.text-ellipsis-tooltip__text')).toBe( - expectedAmount, - ); + const txAmount = await new WalletOperations(page).getTxAmount(); + if (txAmount) { + expect(txAmount).toBe(expectedAmount); + } }); } - async confirmTx(page: Page) { + /** Cancel transaction */ + async cancelTx(page: Page) { await test.step('Confirm TX', async () => { - await page.getByRole('button', { name: 'Confirm' }).click(); + await new WalletOperations(page).cancelTxButton.click(); }); } - // eslint-disable-next-line - async signTx(page: Page) {} + /** Confirm transaction */ + async confirmTx(page: Page) { + await test.step('Confirm TX', async () => { + await new WalletOperations(page).confirmTxButton.click({ + timeout: 30000, // sometimes button is disabled awaits rpc + }); + }); + } + /** Approve token transaction */ async approveTokenTx(page: Page) { await test.step('Approve token tx', async () => { - await page.getByRole('button', { name: 'Confirm' }).click(); - await page.waitForTimeout(2000); - await page.getByRole('button', { name: 'Confirm' }).click(); + const walletOperations = new WalletOperations(page); + await walletOperations.confirmTxButton.click(); + }); + } + + /** Get the `address` from transaction and comply with the `expectedAddress` */ + async assertReceiptAddress(page: Page, expectedAddress: string) { + await test.step('Assert receiptAddress/Contract', async () => { + const recipientAddress = await new WalletOperations( + page, + ).getReceiptAddress(); + expect(recipientAddress).toBe(expectedAddress.toLowerCase()); + }); + } + + /** Confirm tx to add token to wallet */ + async confirmAddTokenToWallet(confirmPage: Page) { + await test.step('Confirm add token to wallet', async () => { + await new WalletOperations(confirmPage).confirmTxButton.click(); + }); + } + + /** Get wallet address from wallet extension*/ + async getWalletAddress() { + return await test.step('Get current wallet address', async () => { + await this.navigate(); + const address = await this.homePage.getWalletAddress(); + await this.page.close(); + return address; }); } - // eslint-disable-next-line - async assertReceiptAddress(page: Page, expectedAmount: string) {} + async changeWalletAccountByName(accountName: string) { + accountName = accountName === 'Account 1' ? 'Account 01' : accountName; + await test.step('Change wallet account by address', async () => { + await this.navigate(); + await this.homePage.accountListButton.click(); + await this.page.getByText(accountName).first().click(); + // need wait to set up address correct + await this.page.waitForTimeout(2000); + await this.page.close(); + }); + } + + // need realize for mainnet + async openLastTxInEthplorer(txIndex = 0) { + console.error( + `OKX wallet does not display the transaction history for testnet (param ${txIndex})`, + ); + return null; + } } diff --git a/packages/wallets/src/okx/pages/accountList.page.ts b/packages/wallets/src/okx/pages/accountList.page.ts new file mode 100644 index 00000000..c8c13601 --- /dev/null +++ b/packages/wallets/src/okx/pages/accountList.page.ts @@ -0,0 +1,52 @@ +import { Locator, Page, test } from '@playwright/test'; + +export class AccountList { + page: Page; + addWalletButton: Locator; + editWallet: Locator; + importSelect: Locator; + importBySeedPhraseOrPrivateKeySelect: Locator; + confirmButton: Locator; + // seed phrase tab + seedPhraseTabButton: Locator; + seedPhraseInputs: Locator; + // private key tab + privateKeyTabButton: Locator; + privateKeyInput: Locator; + + constructor(page: Page) { + this.page = page; + this.addWalletButton = this.page.locator('button:has-text("Add wallet")'); + this.editWallet = this.page.locator('button:has-text("Edit wallet")'); + this.importSelect = this.page.getByText('Import'); + this.importBySeedPhraseOrPrivateKeySelect = this.page.getByText( + 'Seed phrase or private key', + ); + this.confirmButton = this.page.locator('button[type="submit"]'); + this.seedPhraseTabButton = this.page.locator( + 'div[data-e2e-okd-tabs-pane="1"]', + ); + this.seedPhraseInputs = this.page.locator( + 'div[data-testid="okd-popup"] >> input', + ); + this.privateKeyTabButton = this.page.locator( + 'div[data-e2e-okd-tabs-pane="2"]', + ); + this.privateKeyInput = this.page.locator('textarea[type="password"]'); + } + + async importKey(key: string) { + await test.step('Import wallet with key', async () => { + await this.addWalletButton.click(); + await this.importSelect.click(); + await this.importBySeedPhraseOrPrivateKeySelect.click(); + await this.privateKeyTabButton.click(); + await this.privateKeyInput.fill(key); + await this.confirmButton.click(); + await this.page.getByText('Select network').waitFor({ state: 'visible' }); + await this.page.getByText('Confirm').click(); + // need to wait for the setup account correct + await this.page.waitForTimeout(2000); + }); + } +} diff --git a/packages/wallets/src/okx/pages/home.page.ts b/packages/wallets/src/okx/pages/home.page.ts new file mode 100644 index 00000000..6cb745b5 --- /dev/null +++ b/packages/wallets/src/okx/pages/home.page.ts @@ -0,0 +1,69 @@ +import { Locator, Page, test } from '@playwright/test'; + +export class HomePage { + page: Page; + accountListButton: Locator; + copyAddressButton: Locator; + settingButton: Locator; + networkListButton: Locator; + manageCryptoButton: Locator; + + constructor(page: Page) { + this.page = page; + this.accountListButton = this.page.locator('img[alt="wallet-avatar"]'); + this.copyAddressButton = this.page + .getByTestId('okd-select-reference-value-box') + .locator('../../div') + .first(); + this.settingButton = this.page + .getByTestId('okd-select-reference-value-box') + .locator('div') + .first(); + this.networkListButton = this.page + .getByTestId('okd-select-reference-value-box') + .locator('../../div') + .nth(2); + this.manageCryptoButton = this.page.locator( + 'button:has-text("Manage crypto")', + ); + } + + async getTokenBalance(tokenName: string) { + return await this.page + .locator(`a :text-is("${tokenName}")`) + .locator('../../../div') + .last() + .textContent(); + } + + async switchNetworkForDApp(networkName: string) { + await test.step('Open "DApps connection" page', async () => { + await this.settingButton.hover(); + await this.page.getByText('DApps connection').click(); + }); + + await test.step('Switch network', async () => { + await this.page + .getByText('EVM-compatible network') + .locator('..') + .locator('div') + .nth(1) + .click(); + await this.page + .getByTestId('okd-dialog-container') + .getByText(networkName, { exact: true }) + .click(); + // need wait some time to correct network install + await this.page.waitForTimeout(2000); + }); + } + + async getWalletAddress() { + await this.copyAddressButton.click(); + return await this.page + .locator('span:has-text("address copied")') + .locator('../span') + .nth(1) + .textContent(); + } +} diff --git a/packages/wallets/src/okx/pages/index.ts b/packages/wallets/src/okx/pages/index.ts new file mode 100644 index 00000000..8654f8bd --- /dev/null +++ b/packages/wallets/src/okx/pages/index.ts @@ -0,0 +1,7 @@ +export * from './login.page'; +export * from './onboarding.page'; +export * from './networkList.page'; +export * from './walletOperations.page'; +export * from './home.page'; +export * from './manageCrypto.page'; +export * from './accountList.page'; diff --git a/packages/wallets/src/okx/pages/login.page.ts b/packages/wallets/src/okx/pages/login.page.ts new file mode 100644 index 00000000..d7cfc05f --- /dev/null +++ b/packages/wallets/src/okx/pages/login.page.ts @@ -0,0 +1,30 @@ +import { Locator, Page, test } from '@playwright/test'; +import { WalletConfig } from '../../wallets.constants'; + +export class LoginPage { + page: Page; + passwordInput: Locator; + submitButton: Locator; + + constructor(page: Page, public config: WalletConfig) { + this.page = page; + this.passwordInput = this.page.locator( + 'input[data-testid="okd-input"][type="password"]', + ); + this.submitButton = this.page.locator( + 'button[data-testid="okd-button"][type="submit"]', + ); + } + + async unlock() { + await test.step('Unlock wallet', async () => { + try { + await this.passwordInput.waitFor({ state: 'visible', timeout: 2000 }); + await this.passwordInput.fill(this.config.PASSWORD); + await this.submitButton.click(); + } catch { + console.log('The Wallet unlocking is not needed'); + } + }); + } +} diff --git a/packages/wallets/src/okx/pages/manageCrypto.page.ts b/packages/wallets/src/okx/pages/manageCrypto.page.ts new file mode 100644 index 00000000..b9d210dc --- /dev/null +++ b/packages/wallets/src/okx/pages/manageCrypto.page.ts @@ -0,0 +1,31 @@ +import { Locator, Page, test } from '@playwright/test'; + +export class ManageCryptoPage { + page: Page; + customCryptoButton: Locator; + tokenAddressInput: Locator; + confirmButton: Locator; + + constructor(page: Page) { + this.page = page; + this.customCryptoButton = this.page.locator( + 'button:has-text("Custom crypto")', + ); + this.tokenAddressInput = this.page.locator( + 'textarea[data-testid="okd-input"]', + ); + this.confirmButton = this.page.locator('button:has-text("Confirm")'); + } + + async importToken(token: string) { + await test.step(`Import token "${token}"`, async () => { + await this.customCryptoButton.click(); + await this.tokenAddressInput.fill(token); + while (!(await this.confirmButton.isEnabled())) { + // wait while button to be enabled + await this.page.waitForTimeout(1000); + } + await this.confirmButton.click(); + }); + } +} diff --git a/packages/wallets/src/okx/pages/networkList.page.ts b/packages/wallets/src/okx/pages/networkList.page.ts new file mode 100644 index 00000000..5097f1d2 --- /dev/null +++ b/packages/wallets/src/okx/pages/networkList.page.ts @@ -0,0 +1,142 @@ +import { Locator, Page, test } from '@playwright/test'; + +export class NetworkList { + page: Page; + popularNetworkList: Locator; + popularNetworkTabButton: Locator; + userNetworkList: Locator; + userNetworkTabButton: Locator; + addCustomNetworkButton: Locator; + createNetworkInputs: Locator; + saveNetworkButton: Locator; + + constructor(page: Page) { + this.page = page; + this.popularNetworkTabButton = this.page.locator( + 'div[data-e2e-okd-tabs-pane="0"]', + ); + this.popularNetworkList = this.page.getByTestId('okd-tabs-panel-0'); + this.userNetworkTabButton = this.page.locator( + 'div[data-e2e-okd-tabs-pane="2"]', + ); + this.userNetworkList = this.page.getByTestId('okd-tabs-panel-2'); + this.addCustomNetworkButton = this.page.locator( + 'button:has-text("Add custom network")', + ); + this.createNetworkInputs = this.page.getByTestId('okd-input'); + this.saveNetworkButton = this.page.locator('button:has-text("Save")'); + } + + async isNetworkExist(networkName: string): Promise { + const isNetworkFoundInPopularList = + await test.step('Check popular network list', async () => { + await this.popularNetworkTabButton.click(); + return ( + (await this.popularNetworkList + .getByText(networkName, { exact: true }) + .count()) > 0 + ); + }); + + const isNetworkFoundInUserList = + await test.step('Check user network list', async () => { + await this.userNetworkTabButton.click(); + return ( + (await this.userNetworkList + .getByText(networkName, { exact: true }) + .count()) > 0 + ); + }); + + return isNetworkFoundInPopularList || isNetworkFoundInUserList; + } + + async selectNetwork(networkName: string) { + return await test.step('Check network list', async () => { + await this.popularNetworkTabButton.click(); + if ( + (await this.popularNetworkList + .getByText(networkName, { exact: true }) + .count()) > 0 + ) { + await this.popularNetworkList + .getByText(networkName, { exact: true }) + .click(); + // wait some time to network setup + await this.page.waitForTimeout(2000); + return; + } + + await this.userNetworkTabButton.click(); + if ( + (await this.userNetworkList + .getByText(networkName, { exact: true }) + .count()) > 0 + ) { + await this.userNetworkList + .getByText(networkName, { exact: true }) + .first() + .click(); + // wait some time to network setup + try { + await this.page + .getByText('Connecting to') + .waitFor({ state: 'visible', timeout: 2000 }); + await this.page + .getByText('Connecting to') + .waitFor({ state: 'hidden' }); + } catch { + console.log('No need to await loading after changing network'); + } + return; + } + }); + } + + async addCustomNetwork( + networkName: string, + networkUrl: string, + chainId: number, + tokenSymbol: string, + scan: string, + ) { + await test.step('Add network', async () => { + await this.userNetworkTabButton.click(); + await this.addCustomNetworkButton.click(); + await this.createNetworkInputs.nth(0).fill(networkName); + await this.createNetworkInputs.nth(1).fill(networkUrl); + await this.createNetworkInputs.nth(1).blur(); + // wait for autofill by wallet + const chainIdInputValue = await this.createNetworkInputs + .nth(2) + .getAttribute('value', { timeout: 2000 }); + if (chainIdInputValue !== String(chainId)) { + await this.createNetworkInputs.nth(2).fill(String(chainId)); + } + // wait for autofill by wallet + const tokenSymbolInputValue = await this.createNetworkInputs + .nth(3) + .getAttribute('value', { timeout: 2000 }); + if (tokenSymbolInputValue !== tokenSymbol) { + await this.createNetworkInputs.nth(3).fill(tokenSymbol); + } + await this.createNetworkInputs.nth(4).fill(scan); + while (!(await this.saveNetworkButton.isEnabled())) { + // wait for Save button to be enabled + await this.page.waitForTimeout(1000); + } + await this.saveNetworkButton.click(); + // need to wait some time to correct install the network + await this.page.waitForTimeout(2000); + }); + } + + async getWalletNetwork() { + return await this.page + .locator('.okds-success-circle-fill') + .locator('../..') + .locator('div') + .nth(2) + .textContent(); + } +} diff --git a/packages/wallets/src/okx/pages/onboarding.page.ts b/packages/wallets/src/okx/pages/onboarding.page.ts new file mode 100644 index 00000000..e6487b79 --- /dev/null +++ b/packages/wallets/src/okx/pages/onboarding.page.ts @@ -0,0 +1,64 @@ +import { Locator, Page, test } from '@playwright/test'; +import { WalletConfig } from '../../wallets.constants'; + +export class OnboardingPage { + page: Page; + importWalletButton: Locator; + seedPhraseSelect: Locator; + seedPhraseInputs: Locator; + confirmSeedPhraseButton: Locator; + nextButton: Locator; + passwordInput: Locator; + confirmButton: Locator; + startJourneyButton: Locator; + + constructor(page: Page, public config: WalletConfig) { + this.page = page; + this.importWalletButton = this.page.locator( + 'button:has-text("Import wallet")', + ); + this.seedPhraseSelect = this.page.getByText('Seed phrase'); + this.seedPhraseInputs = this.page.locator( + 'div[data-testid="okd-popup"] >> input', + ); + this.confirmSeedPhraseButton = this.page.locator( + 'button:has-text("Confirm")', + ); + this.nextButton = this.page.locator('button:has-text("Next")'); + this.passwordInput = this.page.locator('input[data-testid="okd-input"]'); + this.confirmButton = this.page.locator('button:has-text("Confirm")'); + this.startJourneyButton = this.page.locator( + "button:has-text('Start your Web3 journey')", + ); + } + + async firstTimeSetup() { + await test.step('First time setup', async () => { + await this.importWalletButton.click(); + + await test.step('Fill the secret phrase', async () => { + await this.seedPhraseSelect.click(); + const seedWords = this.config.SECRET_PHRASE.split(' '); + for (let i = 0; i < seedWords.length; i++) { + await this.seedPhraseInputs.nth(i).fill(seedWords[i]); + } + await this.confirmSeedPhraseButton.click(); + await this.nextButton.click(); + }); + + await test.step('Fill the password', async () => { + await this.passwordInput.nth(0).fill(this.config.PASSWORD); + await this.passwordInput.nth(1).fill(this.config.PASSWORD); + await this.confirmButton.click(); + }); + + // Dive into wallet main page after installation + await this.startJourneyButton.click(); + // Wait until extension to be loaded after installation with ETH value display. + // ETH value displayed with ETH symbol + await this.page.waitForSelector('text=ETH', { state: 'visible' }); + //Looks like after installation and load extension mainPage there we should to wait a bit for extension make sure to be installed in some memory + await this.page.waitForTimeout(2000); + }); + } +} diff --git a/packages/wallets/src/okx/pages/walletOperations.page.ts b/packages/wallets/src/okx/pages/walletOperations.page.ts new file mode 100644 index 00000000..f7451bb5 --- /dev/null +++ b/packages/wallets/src/okx/pages/walletOperations.page.ts @@ -0,0 +1,63 @@ +import { Locator, Page, test } from '@playwright/test'; + +export class WalletOperations { + page: Page; + connectButton: Locator; + confirmTxButton: Locator; + cancelTxButton: Locator; + rejectTxButton: Locator; + txYouPayBlock: Locator; + txAmount: Locator; + txContract: Locator; + + constructor(page: Page) { + this.page = page; + this.connectButton = this.page.locator('button:has-text("Connect")'); + this.confirmTxButton = this.page.locator('button:has-text("Confirm")'); + this.cancelTxButton = this.page.locator('button:has-text("Cancel")'); + this.rejectTxButton = this.page.locator('button:has-text("Reject")'); + this.txYouPayBlock = this.page + .locator('div:has-text("You pay")') + .locator('../../../../..'); + this.txAmount = this.txYouPayBlock + .locator('picture') + .locator('../..') + .locator('span') + .first(); + this.txContract = this.page + .getByText('Interact with') + .locator('../../../../..') + .getByTestId('okd-popup') + .locator('../..'); + } + + async getTxAmount() { + if (await this.txYouPayBlock.isVisible()) { + return this.txAmount.textContent(); + } + return null; + } + + async getReceiptAddress() { + return this.txContract.textContent(); + } + + async cancelAllTxInQueue() { + await test.step('Cancel all tx in queue', async () => { + const needToCancelTx = + (await this.cancelTxButton.isVisible()) || + (await this.rejectTxButton.isVisible()); + while (needToCancelTx) { + let cancelButton: Locator; + if (await this.cancelTxButton.isVisible()) { + cancelButton = this.cancelTxButton; + } else if (await this.rejectTxButton.isVisible()) { + cancelButton = this.rejectTxButton; + } else { + break; + } + await cancelButton.click(); + } + }); + } +} diff --git a/packages/wallets/src/okx/services/service.ts b/packages/wallets/src/okx/services/service.ts new file mode 100644 index 00000000..6ecc77ff --- /dev/null +++ b/packages/wallets/src/okx/services/service.ts @@ -0,0 +1,47 @@ +import { BrowserContext } from '@playwright/test'; + +const incorrectNetworkNames = new Map([ + ['zkSync Era Mainnet', 'zkSync Era'], + ['OP Mainnet', 'Optimism'], + ['Polygon Mainnet', 'Polygon'], + ['Base Mainnet', 'Base'], + ['Binance Smart Chain', 'BNB Chain'], + ['Mantle Mainnet', 'Mantle'], + ['Scroll Mainnet', 'Scroll'], + ['Mode', 'Mode Network'], +]); + +const OkxIncludedNetwork = [ + 'Linea', + 'zkSync Era', + 'Polygon', + 'Base', + 'BNB Chain', + 'Mantle', + 'Scroll', + 'Mode Network', + 'Zircuit', +]; + +/** Check network name and return correct name suited for OKX Wallet*/ +export function checkNetworkName(networkName: string) { + for (const [incorrectName, correctName] of incorrectNetworkNames.entries()) { + if (networkName === incorrectName) { + return correctName; + } + } + return networkName; +} + +export async function closeUnnecessaryPages(browserContext: BrowserContext) { + const pages = browserContext.pages().slice(1); + for (const page of pages) { + await page.close(); + } +} + +/** Before AddNetwork() we check the network is included in wallet or not*/ +export async function isNeedAddNetwork(network: string) { + const networkName = await checkNetworkName(network); + return !OkxIncludedNetwork.includes(networkName); +} diff --git a/packages/wallets/src/wallet.page.ts b/packages/wallets/src/wallet.page.ts index a989fdd5..e974edd5 100644 --- a/packages/wallets/src/wallet.page.ts +++ b/packages/wallets/src/wallet.page.ts @@ -28,7 +28,7 @@ export interface WalletPage { assertReceiptAddress(page: Page, expectedAmount: string): Promise; getWalletAddress?(): Promise; - switchNetwork?(networkName: string): Promise; + setupNetwork?(standConfig: Record): Promise; addNetwork( @@ -40,8 +40,6 @@ export interface WalletPage { isClosePage?: boolean, ): Promise; - addPopularNetwork?(networkName: string): Promise; - changeNetwork?(networkName: string): Promise; changeWalletAccountByName?(accountName: string): Promise;