diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 279e7856..3207a0b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: run: yarn build - name: Install Playwright Browsers - run: npx playwright install --with-deps + run: yarn playwright install chromium --with-deps - name: Run wallets tests run: xvfb-run --auto-servernum -- yarn test:widgets @@ -42,12 +42,12 @@ jobs: WALLET_PASSWORD: ${{ secrets.WALLET_PASSWORD }} NODE_OPTIONS: --max-old-space-size=4096 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: playwright-report path: wallets-testing/playwright-report/ - retention-days: 30 + retention-days: 7 - name: Set embeds if: ${{ always() }} diff --git a/packages/wallets/src/xdefi/xdefi.constants.ts b/packages/wallets/src/ctrl/ctrl.constants.ts similarity index 52% rename from packages/wallets/src/xdefi/xdefi.constants.ts rename to packages/wallets/src/ctrl/ctrl.constants.ts index 0ad4d4e0..0307b40b 100644 --- a/packages/wallets/src/xdefi/xdefi.constants.ts +++ b/packages/wallets/src/ctrl/ctrl.constants.ts @@ -1,11 +1,11 @@ import { CommonWalletConfig } from '../wallets.constants'; -export const XDEFI_COMMON_CONFIG: CommonWalletConfig = { - WALLET_NAME: 'xdefi', - CONNECTED_WALLET_NAME: 'XDEFI', +export const CTRL_COMMON_CONFIG: CommonWalletConfig = { + WALLET_NAME: 'ctrl', + CONNECTED_WALLET_NAME: 'Ctrl', RPC_URL_PATTERN: 'https://mainnet.infura.io/v3/**', STORE_EXTENSION_ID: 'hmeobnfnfcmdkdcmlblgagmfpfboieaf', - CONNECT_BUTTON_NAME: 'XDEFI', + CONNECT_BUTTON_NAME: 'Ctrl', SIMPLE_CONNECT: false, - EXTENSION_START_PATH: '/app.html', + EXTENSION_START_PATH: '/popup.html', }; diff --git a/packages/wallets/src/ctrl/ctrl.page.ts b/packages/wallets/src/ctrl/ctrl.page.ts new file mode 100644 index 00000000..81cec221 --- /dev/null +++ b/packages/wallets/src/ctrl/ctrl.page.ts @@ -0,0 +1,135 @@ +import { WalletPage } from '../wallet.page'; +import { test, BrowserContext, Page } from '@playwright/test'; +import { WalletConfig } from '../wallets.constants'; +import { LoginPage, OnboardingPage, WalletOperations } from './pages'; + +export class CtrlPage implements WalletPage { + page: Page | undefined; + onboardingPage: OnboardingPage; + loginPage: LoginPage; + + constructor( + private browserContext: BrowserContext, + private extensionUrl: string, + public config: WalletConfig, + ) {} + + /** Init all page objects Classes included to wallet */ + async initLocators() { + this.page = await this.browserContext.newPage(); + this.onboardingPage = new OnboardingPage( + this.page, + this.extensionUrl, + this.config, + ); + this.loginPage = new LoginPage(this.page, this.config); + } + + /** 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) + */ + async navigate() { + await test.step('Navigate to Ctrl', async () => { + await this.initLocators(); + await this.goto(); + await this.loginPage.unlock(); + }); + } + + async setup() { + await test.step('Setup', async () => { + await this.initLocators(); + await this.onboardingPage.firstTimeSetup(); + }); + } + + /** Click `Connect` button */ + async connectWallet(page: Page) { + await test.step('Connect Ctrl wallet', async () => { + const operationPage = new WalletOperations(page); + await operationPage.connectBtn.waitFor({ + state: 'visible', + timeout: 10000, + }); + await operationPage.connectBtn.click(); + // need wait the page to be closed after the extension is connected + await new Promise((resolve) => { + operationPage.page.on('close', () => { + resolve(); + }); + }); + }); + } + + importKey(): Promise { + throw new Error('Method not implemented.'); + } + + assertTxAmount(): Promise { + throw new Error('Method not implemented.'); + } + + confirmTx(): Promise { + throw new Error('Method not implemented.'); + } + + cancelTx(): Promise { + throw new Error('Method not implemented.'); + } + + approveTokenTx(): Promise { + throw new Error('Method not implemented.'); + } + + openLastTxInEthplorer(): Promise { + throw new Error('Method not implemented.'); + } + + getTokenBalance(): Promise { + throw new Error('Method not implemented.'); + } + + confirmAddTokenToWallet(): Promise { + throw new Error('Method not implemented.'); + } + + assertReceiptAddress(): Promise { + throw new Error('Method not implemented.'); + } + + getWalletAddress(): Promise { + throw new Error('Method not implemented.'); + } + + setupNetwork(): Promise { + throw new Error('Method not implemented.'); + } + + addNetwork(): Promise { + throw new Error('Method not implemented.'); + } + + changeNetwork(): Promise { + throw new Error('Method not implemented.'); + } + + changeWalletAccountByName(): Promise { + throw new Error('Method not implemented.'); + } + + changeWalletAccountByAddress(): Promise { + throw new Error('Method not implemented.'); + } + + isWalletAddressExist(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/wallets/src/ctrl/index.ts b/packages/wallets/src/ctrl/index.ts new file mode 100644 index 00000000..2115c3dd --- /dev/null +++ b/packages/wallets/src/ctrl/index.ts @@ -0,0 +1,2 @@ +export * from './ctrl.page'; +export * from './ctrl.constants'; diff --git a/packages/wallets/src/ctrl/pages/index.ts b/packages/wallets/src/ctrl/pages/index.ts new file mode 100644 index 00000000..1d6628c9 --- /dev/null +++ b/packages/wallets/src/ctrl/pages/index.ts @@ -0,0 +1,3 @@ +export * from './onboarding.page'; +export * from './login.page'; +export * from './walletOperations.page'; diff --git a/packages/wallets/src/ctrl/pages/login.page.ts b/packages/wallets/src/ctrl/pages/login.page.ts new file mode 100644 index 00000000..46296c75 --- /dev/null +++ b/packages/wallets/src/ctrl/pages/login.page.ts @@ -0,0 +1,28 @@ +import { Locator, Page, test } from '@playwright/test'; +import { WalletConfig } from '../../wallets.constants'; + +export class LoginPage { + page: Page; + unlockBtn: Locator; + passwordInput: Locator; + homeBtn: Locator; + + constructor(page: Page, public config: WalletConfig) { + this.page = page; + this.unlockBtn = this.page.getByTestId('unlock-btn'); + this.passwordInput = this.page.locator('input[type="password"]'); + } + + async unlock() { + await test.step('Unlock wallet', async () => { + try { + await this.unlockBtn.waitFor({ state: 'visible', timeout: 2000 }); + await this.passwordInput.fill(this.config.PASSWORD); + await this.unlockBtn.click(); + await this.homeBtn.waitFor({ state: 'visible' }); + } catch { + console.log('The Wallet unlocking is not needed'); + } + }); + } +} diff --git a/packages/wallets/src/ctrl/pages/onboarding.page.ts b/packages/wallets/src/ctrl/pages/onboarding.page.ts new file mode 100644 index 00000000..15a33cca --- /dev/null +++ b/packages/wallets/src/ctrl/pages/onboarding.page.ts @@ -0,0 +1,100 @@ +import { Locator, Page, test, expect } from '@playwright/test'; +import { WalletConfig } from '../../wallets.constants'; + +export class OnboardingPage { + page: Page; + alreadyHaveWalletBtn: Locator; + importRecoveryPhraseBtn: Locator; + passwordInput: Locator; + nextBtn: Locator; + importBtn: Locator; + closeTourBtn: Locator; + notNowBtn: Locator; + createPasswordBtn: Locator; + confirmPasswordBtn: Locator; + + constructor( + page: Page, + private extensionUrl: string, + public config: WalletConfig, + ) { + this.page = page; + this.alreadyHaveWalletBtn = this.page.getByTestId( + 'i-already-have-a-wallet-btn', + ); + this.importRecoveryPhraseBtn = this.page.getByText( + 'Import with Recovery Phrase', + ); + this.passwordInput = this.page.locator('input[type="password"]'); + this.nextBtn = this.page.getByTestId('next-btn'); + this.importBtn = this.page.getByTestId('import-btn'); + this.closeTourBtn = this.page.locator( + '[testID="home-page-tour-close-icon"]', + ); + this.notNowBtn = this.page.getByText('Not now'); + this.createPasswordBtn = this.page.getByText('create a password'); + this.confirmPasswordBtn = this.page.getByTestId('button'); + } + + async firstTimeSetup() { + if (await this.isWalletSetup()) return; + + await test.step('First time set up', async () => { + // Without additional awaiting the extension breaks the next step and redirects a user back + await this.page.waitForTimeout(2000); + await this.alreadyHaveWalletBtn.click({ force: true }); + + await test.step('Import wallet with recovery phrase', async () => { + await this.importRecoveryPhraseBtn.click(); + const seedWords = this.config.SECRET_PHRASE.split(' '); + for (let i = 0; i < seedWords.length; i++) { + await this.passwordInput.nth(i).fill(seedWords[i]); + } + await this.nextBtn.click(); + await this.importBtn.waitFor({ state: 'visible', timeout: 60000 }); + await expect(this.importBtn).toBeEnabled({ timeout: 2000 }); + await this.importBtn.click(); + await this.nextBtn.click(); + }); + + await this.page.goto( + this.extensionUrl + this.config.COMMON.EXTENSION_START_PATH, + ); + + await test.step('Close wallet tour', async () => { + await this.closeTourBtn.waitFor({ state: 'visible', timeout: 2000 }); + await this.closeTourBtn.click(); + await this.notNowBtn.waitFor({ state: 'visible', timeout: 2000 }); + await this.page.waitForTimeout(1000); // Need to wait some time for button enabling + await this.notNowBtn.click({ force: true }); + }); + + await test.step('Create wallet password', async () => { + await this.createPasswordBtn.click(); + await this.passwordInput.nth(0).fill(this.config.PASSWORD); + await this.passwordInput.nth(1).fill(this.config.PASSWORD); + await this.confirmPasswordBtn.click(); + }); + }); + } + + async isWalletSetup() { + await test.step('Open the onboarding page', async () => { + // Need to open onboarding page cause the extension does not redirect from home url automatically + await this.page.goto( + this.extensionUrl + '/tabs/onboarding.html#onboarding', + ); + }); + return await test.step('Check the wallet is set up', async () => { + try { + await this.alreadyHaveWalletBtn.waitFor({ + state: 'visible', + timeout: 5000, + }); + } catch { + console.log('Ctrl wallet: Onboarding process is not needed'); + } + return !(await this.alreadyHaveWalletBtn.isVisible()); + }); + } +} diff --git a/packages/wallets/src/ctrl/pages/walletOperations.page.ts b/packages/wallets/src/ctrl/pages/walletOperations.page.ts new file mode 100644 index 00000000..e4f69193 --- /dev/null +++ b/packages/wallets/src/ctrl/pages/walletOperations.page.ts @@ -0,0 +1,11 @@ +import { Locator, Page } from '@playwright/test'; + +export class WalletOperations { + page: Page; + connectBtn: Locator; + + constructor(page: Page) { + this.page = page; + this.connectBtn = this.page.getByTestId('connect-dapp-button'); + } +} diff --git a/packages/wallets/src/index.ts b/packages/wallets/src/index.ts index d01a0d41..f041a47a 100644 --- a/packages/wallets/src/index.ts +++ b/packages/wallets/src/index.ts @@ -5,6 +5,6 @@ export * from './metamask'; export * from './trustwallet'; export * from './coinbase'; export * from './exodus'; -export * from './xdefi'; export * from './okx'; export * from './bitget'; +export * from './ctrl'; diff --git a/packages/wallets/src/okx/okx.page.ts b/packages/wallets/src/okx/okx.page.ts index ab042a20..8af865f9 100644 --- a/packages/wallets/src/okx/okx.page.ts +++ b/packages/wallets/src/okx/okx.page.ts @@ -65,7 +65,7 @@ export class OkxPage implements WalletPage { }); } - /** Checks the wallet is set correctly and starts ot wallet setup as the first time (if needed) */ + /** Checks the wallet is set correctly and starts the wallet setup as the first time (if needed) */ async setup() { await test.step('Setup', async () => { await this.navigate(); diff --git a/packages/wallets/src/xdefi/index.ts b/packages/wallets/src/xdefi/index.ts deleted file mode 100644 index b50948ca..00000000 --- a/packages/wallets/src/xdefi/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './xdefi.page'; -export * from './xdefi.constants'; diff --git a/packages/wallets/src/xdefi/xdefi.page.ts b/packages/wallets/src/xdefi/xdefi.page.ts deleted file mode 100644 index caa2cf95..00000000 --- a/packages/wallets/src/xdefi/xdefi.page.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { WalletConfig } from '../wallets.constants'; -import { WalletPage } from '../wallet.page'; -import expect from 'expect'; -import { test, BrowserContext, Page } from '@playwright/test'; - -export class XdefiPage implements WalletPage { - page: Page | undefined; - - constructor( - private browserContext: BrowserContext, - private extensionUrl: string, - public config: WalletConfig, - ) {} - - async navigate() { - await test.step('Navigate to xdefi', 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); - }); - } - - async setup() { - await test.step('Setup', async () => { - // added explicit route to /onboarding due to unexpected first time route from /home.html to /onboarding - page is close - this.page = await this.browserContext.newPage(); - await this.page.goto(this.extensionUrl + '/onboarding.html'); - if (!this.page) throw "Page isn't ready"; - const firstTime = await this.page.waitForSelector( - "text=Let's get started", - ); - if (firstTime) await this.firstTimeSetup(); - }); - } - - 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('button[data-testid="addAssetsBtn"]'); - await this.page.click('li[data-testid="customTab"]'); - await this.page.type('input[name="address"]', token); - await this.page.click('button[data-testid="nextBtn"]'); - }); - } - - async firstTimeSetup() { - await test.step('First time setup', async () => { - if (!this.page) throw "Page isn't ready"; - await this.page.click('text=Restore XDEFI Wallet'); - await this.page.click('text=Restore with secret phrase'); - const inputs = this.page.locator('input[data-testid=input]'); - const seedWords = this.config.SECRET_PHRASE.split(' '); - for (let i = 0; i < seedWords.length; i++) { - await inputs.nth(i).fill(seedWords[i]); - } - await this.page.click('text=Next'); - await this.page.fill('input[name="password"]', this.config.PASSWORD); - await this.page.fill('input[name="cpassword"]', this.config.PASSWORD); - await this.page.click('div[data-testid=termsAndConditionsCheckbox]'); - await this.page.click('button[type="submit"]'); - await this.page.fill( - 'input[name="walletName"]', - this.config.COMMON.WALLET_NAME, - ); - await this.page.click('button[type="submit"]'); - await this.page.click('div[data-testid=prioritiesXdefiToggle]'); - await this.page.click('button[data-testid=nextBtn]'); - }); - } - - 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('button[data-testid="menuBtn"]'); - await this.page.click('li[data-testid="walletManagementBtn"]'); - await this.page.click('div[data-testid="importWalletBtn"]'); - await this.page.click('text=Seed Phrase'); - await this.page.fill('textarea[name="[phrase"]', key); - await this.page.click('svg:below(input)'); - await this.page.click('button[data-testid="importBtn"]'); - }); - } - - async connectWallet(page: Page) { - await test.step('Connect wallet', async () => { - await page.click('button[data-testid="nextBtn"]'); - await page - .locator('svg[data-testid="checkbox-unchecked"]') - .first() - .locator('..') - .click(); - await page.click('button[data-testid="connectBtn"]'); - await page.close(); - }); - } - - async assertTxAmount(page: Page, expectedAmount: string) { - await test.step('Assert TX Amount', async () => { - expect(await page.locator(`text=${expectedAmount} ETH`).count()).toBe(1); - }); - } - - async confirmTx(page: Page) { - await test.step('Confirm TX', async () => { - await page.click('button[data-testid="confirmBtn"]'); - }); - } - - // eslint-disable-next-line - async signTx(page: Page) {} - - // eslint-disable-next-line - async assertReceiptAddress(page: Page, expectedAddress: string) {} - - // eslint-disable-next-line - async addNetwork(networkName: string, networkUrl: string, chainId: number, tokenSymbol: string) {} -} diff --git a/packages/widgets/src/ethereum/ethereum.page.ts b/packages/widgets/src/ethereum/ethereum.page.ts index bfd7a96a..c03291d2 100644 --- a/packages/widgets/src/ethereum/ethereum.page.ts +++ b/packages/widgets/src/ethereum/ethereum.page.ts @@ -9,9 +9,15 @@ import { test, Page, Locator } from '@playwright/test'; export class EthereumPage implements WidgetPage { private readonly logger = new Logger(EthereumPage.name); page: Page; + connectBtn: Locator; + stakeSubmitBtn: Locator; + termsCheckbox: Locator; constructor(page: Page, private stakeConfig: StakeConfig) { this.page = page; + this.connectBtn = this.page.getByTestId('connectBtn'); + this.stakeSubmitBtn = this.page.getByTestId('stakeSubmitBtn'); + this.termsCheckbox = this.page.locator('input[type=checkbox]'); } async navigate() { @@ -37,43 +43,43 @@ export class EthereumPage implements WidgetPage { } async connectWallet(walletPage: WalletPage) { - await test.step( - 'Connect wallet ' + walletPage.config.COMMON.WALLET_NAME, - async () => { - await this.page.waitForTimeout(2000); - const isConnected = - (await this.page.getByTestId('connectBtn').count()) === 0; - if (!isConnected) { - await this.page.getByTestId('connectBtn').first().click(); - await this.page.waitForTimeout(2000); - if ((await this.page.getByTestId('stakeSubmitBtn').count()) === 0) { - if (!(await this.page.isChecked('input[type=checkbox]'))) - await this.page.click('input[type=checkbox]', { force: true }); - if (walletPage.config.COMMON.SIMPLE_CONNECT) { - await this.page.click( - `button[type=button] :text('${walletPage.config.COMMON.CONNECT_BUTTON_NAME}')`, - ); - } else { - const [connectWalletPage] = await Promise.all([ - this.page.context().waitForEvent('page', { timeout: 5000 }), - this.page.click( - `button[type=button] :text('${walletPage.config.COMMON.CONNECT_BUTTON_NAME}')`, - ), - ]); - await walletPage.connectWallet(connectWalletPage); - } - expect( - await this.page.waitForSelector('data-testid=stakeSubmitBtn'), - ).not.toBeNaN(); - await this.page.locator('data-testid=accountSectionHeader').click(); - expect( - await this.page.textContent('div[data-testid="providerName"]'), - ).toContain(walletPage.config.COMMON.CONNECTED_WALLET_NAME); - await this.page.locator('div[role="dialog"] button').nth(0).click(); - } - } - }, - ); + await test.step(`Connect wallet ${walletPage.config.COMMON.WALLET_NAME}`, async () => { + await this.page.waitForTimeout(2000); + // If wallet connected -> return + if ((await this.connectBtn.count()) === 0) return; + await this.connectBtn.first().click(); + await this.page.waitForTimeout(2000); + // If Stake submit button is displayed -> return + if ((await this.stakeSubmitBtn.count()) > 0) return; + + if (!(await this.termsCheckbox.isChecked())) + await this.termsCheckbox.click({ force: true }); + + const walletButton = this.page + .getByRole('button') + .getByText(walletPage.config.COMMON.CONNECT_BUTTON_NAME, { + exact: true, + }); + + if (walletPage.config.COMMON.SIMPLE_CONNECT) { + await walletButton.click(); + } else { + const [connectWalletPage] = await Promise.all([ + this.page.context().waitForEvent('page', { timeout: 5000 }), + walletButton.click(), + ]); + await walletPage.connectWallet(connectWalletPage); + } + + expect( + await this.page.waitForSelector('data-testid=stakeSubmitBtn'), + ).not.toBeNaN(); + await this.page.locator('data-testid=accountSectionHeader').click(); + expect( + await this.page.textContent('div[data-testid="providerName"]'), + ).toContain(walletPage.config.COMMON.CONNECTED_WALLET_NAME); + await this.page.locator('div[role="dialog"] button').nth(0).click(); + }); } async doStaking(walletPage: WalletPage) { diff --git a/wallets-testing/browser/browser.constants.ts b/wallets-testing/browser/browser.constants.ts index d0ea2bae..c253cd43 100644 --- a/wallets-testing/browser/browser.constants.ts +++ b/wallets-testing/browser/browser.constants.ts @@ -4,9 +4,9 @@ import { TrustWalletPage, ExodusPage, CoinbasePage, - XdefiPage, OkxPage, BitgetPage, + CtrlPage, } from '@lidofinance/wallets-testing-wallets'; import { EthereumPage } from '@lidofinance/wallets-testing-widgets'; @@ -16,9 +16,9 @@ export const WALLET_PAGES = { trust: TrustWalletPage, exodus: ExodusPage, coinbase: CoinbasePage, - xdefi: XdefiPage, okx: OkxPage, bitget: BitgetPage, + ctrl: CtrlPage, }; export const WIDGET_PAGES = { diff --git a/wallets-testing/test/widgets/ethereum.spec.ts b/wallets-testing/test/widgets/ethereum.spec.ts index df801a45..9561b068 100644 --- a/wallets-testing/test/widgets/ethereum.spec.ts +++ b/wallets-testing/test/widgets/ethereum.spec.ts @@ -7,9 +7,9 @@ import { TRUST_WALLET_COMMON_CONFIG, COINBASE_COMMON_CONFIG, EXODUS_COMMON_CONFIG, - XDEFI_COMMON_CONFIG, OKX_COMMON_CONFIG, BITGET_COMMON_CONFIG, + CTRL_COMMON_CONFIG, } from '@lidofinance/wallets-testing-wallets'; import { ETHEREUM_WIDGET_CONFIG } from '@lidofinance/wallets-testing-widgets'; import { BrowserModule } from '../../browser/browser.module'; @@ -63,8 +63,8 @@ test.describe('Ethereum', () => { await browserService.connectWallet(); }); - test.skip(`Xdefi wallet connect`, async () => { - await browserService.setup(XDEFI_COMMON_CONFIG, ETHEREUM_WIDGET_CONFIG); + test(`Ctrl connect`, async () => { + await browserService.setup(CTRL_COMMON_CONFIG, ETHEREUM_WIDGET_CONFIG); await browserService.connectWallet(); });