diff --git a/src/helpers/actions.ts b/src/helpers/actions.ts index de2f84ab..e3a54673 100644 --- a/src/helpers/actions.ts +++ b/src/helpers/actions.ts @@ -8,9 +8,15 @@ export const clickOnSettingsSwitch = async (page: Page, text: string): Promise => { - const networkSwitcher = await page.waitForSelector('.network-display'); - await networkSwitcher.click(); - await page.waitForSelector('li.dropdown-menu-item'); + const networkSwitcher = await page.waitForSelector('.network-display', { visible: true }); + try { + await networkSwitcher.click(); + await page.waitForSelector('.network-dropdown-list', { visible: true, timeout: 1000 }); + } catch (e) { + //retry on fail + await networkSwitcher.click(); + await page.waitForSelector('.network-dropdown-list', { visible: true, timeout: 1000 }); + } }; export const openProfileDropdown = async (page: Page): Promise => { diff --git a/src/helpers/selectors.ts b/src/helpers/selectors.ts index bc542a5b..d0d7672b 100644 --- a/src/helpers/selectors.ts +++ b/src/helpers/selectors.ts @@ -2,7 +2,7 @@ import { ElementHandle, Page } from 'puppeteer'; // TODO: change text() with '.'; export const getElementByContent = (page: Page, text: string, type = '*'): Promise => - page.waitForXPath(`//${type}[contains(text(), '${text}')]`, { timeout: 20000 }); + page.waitForXPath(`//${type}[contains(text(), '${text}')]`, { timeout: 20000, visible: true }); export const getInputByLabel = ( page: Page, @@ -23,7 +23,7 @@ export const getInputByLabel = ( ] : []), ].join('|'), - { timeout }, + { timeout, visible: true }, ); export const getSettingsSwitch = (page: Page, text: string): Promise => @@ -32,6 +32,7 @@ export const getSettingsSwitch = (page: Page, text: string): Promise => { @@ -47,4 +48,4 @@ export const getErrorMessage = async (page: Page): Promise => { }; export const getAccountMenuButton = (page: Page): Promise => - page.waitForXPath(`//button[contains(@title,'Account Options')]`); + page.waitForXPath(`//button[contains(@title,'Account options')]`); diff --git a/src/index.ts b/src/index.ts index afb559f9..0823c566 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,4 @@ export * from './setup'; export { DappateerJestConfig } from './jest/global'; // default constants -export const RECOMMENDED_METAMASK_VERSION = 'v10.15.0'; +export const RECOMMENDED_METAMASK_VERSION = 'v10.20.0'; diff --git a/src/metamask/addNetwork.ts b/src/metamask/addNetwork.ts index 8e1610a3..515be654 100644 --- a/src/metamask/addNetwork.ts +++ b/src/metamask/addNetwork.ts @@ -12,16 +12,18 @@ export const addNetwork = (page: Page, version?: string) => async ({ }: AddNetwork): Promise => { await page.bringToFront(); await openNetworkDropdown(page); - await clickOnButton(page, 'Add Network'); + await clickOnButton(page, 'Add network'); const responsePromise = page.waitForResponse( (response) => new URL(response.url()).pathname === new URL(rpc).pathname, ); - await typeOnInputField(page, 'Network Name', networkName); + await page.waitForTimeout(500); + + await typeOnInputField(page, 'Network name', networkName); await typeOnInputField(page, 'New RPC URL', rpc); await typeOnInputField(page, 'Chain ID', String(chainId)); - await typeOnInputField(page, 'Currency Symbol', symbol); + await typeOnInputField(page, 'Currency symbol', symbol); await responsePromise; await page.waitForTimeout(500); @@ -32,4 +34,5 @@ export const addNetwork = (page: Page, version?: string) => async ({ await clickOnButton(page, 'Save'); await page.waitForXPath(`//*[text() = '${networkName}']`); + await clickOnButton(page, 'Got it'); }; diff --git a/src/metamask/addToken.ts b/src/metamask/addToken.ts index 4a9b88ea..c39c9174 100644 --- a/src/metamask/addToken.ts +++ b/src/metamask/addToken.ts @@ -1,6 +1,6 @@ import { Page } from 'puppeteer'; -import { clickOnButton, clickOnElement, getInputByLabel, typeOnInputField } from '../helpers'; +import { clickOnButton, clickOnElement, clickOnLogo, getElementByContent, typeOnInputField } from '../helpers'; import { AddToken } from '../index'; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -12,23 +12,17 @@ export const addToken = (page: Page, version?: string) => async ({ await page.bringToFront(); await clickOnButton(page, 'Assets'); await page.waitForSelector('.asset-list-item__token-button'); - await clickOnElement(page, 'Import tokens'); - await typeOnInputField(page, 'Token Contract Address', tokenAddress); + await clickOnElement(page, 'import tokens'); + await clickOnButton(page, 'Custom token'); - // wait to metamask pull token data - // TODO: handle case when contract is not containing symbol - const symbolInput = await getInputByLabel(page, 'Token Symbol'); - await page.waitForFunction((node) => !!node.value, {}, symbolInput); + await typeOnInputField(page, 'Token decimal', String(decimals), true); + await typeOnInputField(page, 'Token contract address', tokenAddress); + await page.waitForTimeout(333); + await typeOnInputField(page, 'Token symbol', symbol, true); - if (symbol) { - await clickOnElement(page, 'Edit'); - await typeOnInputField(page, 'Token Symbol', symbol, true); - } + await clickOnButton(page, 'Add custom token'); + await clickOnButton(page, 'Import tokens'); - const decimalsInput = await getInputByLabel(page, 'Token Decimal'); - const isDisabled = await page.evaluate((node) => node.disabled, decimalsInput); - if (!isDisabled) await decimalsInput.type(String(decimals)); - - await clickOnButton(page, 'Add Custom Token'); - await clickOnButton(page, 'Import Tokens'); + await getElementByContent(page, symbol); + await clickOnLogo(page); }; diff --git a/src/metamask/importPk.ts b/src/metamask/importPk.ts index 4289e9eb..17e69fad 100644 --- a/src/metamask/importPk.ts +++ b/src/metamask/importPk.ts @@ -7,7 +7,7 @@ export const importPk = (page: Page, version?: string) => async (privateKey: str await page.bringToFront(); await openProfileDropdown(page); - await clickOnElement(page, 'Import Account'); + await clickOnElement(page, 'Import account'); await typeOnInputField(page, 'your private key', privateKey); await clickOnButton(page, 'Import'); diff --git a/src/metamask/switchNetwork.ts b/src/metamask/switchNetwork.ts index 7dd6f6f5..6d48662d 100644 --- a/src/metamask/switchNetwork.ts +++ b/src/metamask/switchNetwork.ts @@ -9,7 +9,7 @@ export const switchNetwork = (page: Page, version?: string) => async (network = await openNetworkDropdown(page); const networkIndex = await page.evaluate((network) => { - const elements = document.querySelectorAll('li.dropdown-menu-item'); + const elements = document.querySelectorAll('.network-name-item'); for (let i = 0; i < elements.length; i++) { const element = elements[i]; if ((element as HTMLLIElement).innerText.toLowerCase().includes(network.toLowerCase())) { @@ -20,12 +20,11 @@ export const switchNetwork = (page: Page, version?: string) => async (network = }, network); const networkFullName = await page.evaluate((index) => { - const elements = document.querySelectorAll(`li.dropdown-menu-item > span`); + const elements = document.querySelectorAll(`.network-name-item`); return (elements[index] as HTMLLIElement).innerText; }, networkIndex); - const networkButton = (await page.$$('li.dropdown-menu-item'))[networkIndex]; + const networkButton = (await page.$$('.network-name-item'))[networkIndex]; await networkButton.click(); - await page.reload(); await page.waitForXPath(`//*[text() = '${networkFullName}']`); }; diff --git a/src/setup/setupActions.ts b/src/setup/setupActions.ts index d4006ec7..18bd29dd 100644 --- a/src/setup/setupActions.ts +++ b/src/setup/setupActions.ts @@ -19,7 +19,11 @@ export async function showTestNets(metaMaskPage: Page): Promise { } export async function confirmWelcomeScreen(metaMaskPage: Page): Promise { - await clickOnButton(metaMaskPage, 'Get Started'); + await clickOnButton(metaMaskPage, 'Get started'); +} + +export async function declineAnalytics(metaMaskPage: Page): Promise { + await clickOnButton(metaMaskPage, 'No thanks'); } export async function importAccount( @@ -30,7 +34,6 @@ export async function importAccount( }: MetaMaskOptions, ): Promise { await clickOnButton(metaMaskPage, 'Import wallet'); - await clickOnButton(metaMaskPage, 'I Agree'); for (const [index, seedPart] of seed.split(' ').entries()) await typeOnInputField(metaMaskPage, `${index + 1}.`, seedPart); @@ -43,7 +46,7 @@ export async function importAccount( await acceptTerms.click(); await clickOnButton(metaMaskPage, 'Import'); - await clickOnButton(metaMaskPage, 'All Done'); + await clickOnButton(metaMaskPage, 'All done'); } export const closePopup = async (page: Page): Promise => { @@ -52,3 +55,17 @@ export const closePopup = async (page: Page): Promise => { await new Promise((resolve) => setTimeout(resolve, 1000)); await page.$eval('.popover-header__button', (node: HTMLElement) => node.click()); }; + +export const closePortfolioTooltip = async (page: Page): Promise => { + const closeButton = await page.waitForSelector(`div.home__subheader-link--tooltip-content-header > button`, { + timeout: 20000, + }); + await closeButton.click(); + await page.waitForTimeout(333); +}; + +export const closeWhatsNewModal = async (page: Page): Promise => { + await page.reload(); + await clickOnLogo(page); + await page.waitForTimeout(333); +}; diff --git a/src/setup/setupMetaMask.ts b/src/setup/setupMetaMask.ts index 54e21b60..52f342e0 100644 --- a/src/setup/setupMetaMask.ts +++ b/src/setup/setupMetaMask.ts @@ -3,20 +3,35 @@ import { Browser, BrowserContext, Page } from 'puppeteer'; import { getMetaMask } from '../metamask'; import { Dappeteer, MetaMaskOptions } from '../types'; -import { closePopup, confirmWelcomeScreen, importAccount, showTestNets } from './setupActions'; +import { + closePortfolioTooltip, + closeWhatsNewModal, + confirmWelcomeScreen, + declineAnalytics, + importAccount, + showTestNets, +} from './setupActions'; /** * Setup MetaMask with base account * */ type Step = (page: Page, options?: Options) => void; -const defaultMetaMaskSteps: Step[] = [confirmWelcomeScreen, importAccount, closePopup, showTestNets]; +const defaultMetaMaskSteps: Step[] = [ + confirmWelcomeScreen, + declineAnalytics, + importAccount, + showTestNets, + closePortfolioTooltip, + closeWhatsNewModal, + closeWhatsNewModal, +]; export async function setupMetaMask( browser: Browser | BrowserContext, options?: Options, steps: Step[] = defaultMetaMaskSteps, ): Promise { - const page = await closeHomeScreen(browser); + const page = await getMetamaskPage(browser); page.setViewport({ height: 1200, width: 800 }); // goes through the installation steps required by MetaMask for (const step of steps) { @@ -26,7 +41,13 @@ export async function setupMetaMask( return getMetaMask(page); } -async function closeHomeScreen(browser: Browser | BrowserContext): Promise { +async function getMetamaskPage(browser: Browser | BrowserContext): Promise { + const pages = await browser.pages(); + for (const page of pages) { + if (page.url().match('chrome-extension://[a-z]+/home.html')) { + return page; + } + } return new Promise((resolve, reject) => { browser.on('targetcreated', async (target) => { if (target.url().match('chrome-extension://[a-z]+/home.html')) { diff --git a/src/types.ts b/src/types.ts index 163d3871..c62a3dfc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { BrowserLaunchArgumentOptions, Page } from 'puppeteer'; +import * as puppeteer from 'puppeteer'; import { Path } from './setup/metaMaskDownloader'; @@ -6,14 +6,21 @@ import { RECOMMENDED_METAMASK_VERSION } from './index'; export type LaunchOptions = OfficialOptions | CustomOptions; -type DappaterBrowserLaunchArgumentOptions = Omit; +type PuppeteerLaunchOptions = puppeteer.LaunchOptions & + puppeteer.BrowserLaunchArgumentOptions & + puppeteer.BrowserConnectOptions & { + product?: puppeteer.Product; + extraPrefsFirefox?: Record; + }; + +type DappeteerLaunchOptions = Omit; -export type OfficialOptions = DappaterBrowserLaunchArgumentOptions & { +export type OfficialOptions = DappeteerLaunchOptions & { metaMaskVersion: typeof RECOMMENDED_METAMASK_VERSION | 'latest' | string; metaMaskLocation?: Path; }; -export type CustomOptions = DappaterBrowserLaunchArgumentOptions & { +export type CustomOptions = DappeteerLaunchOptions & { metaMaskVersion?: string; metaMaskPath: string; }; @@ -59,5 +66,5 @@ export type Dappeteer = { deleteAccount: (accountNumber: number) => Promise; deleteNetwork: (name: string) => Promise; }; - page: Page; + page: puppeteer.Page; }; diff --git a/test/basic.spec.ts b/test/basic.spec.ts index 1c1115c4..8aae4987 100644 --- a/test/basic.spec.ts +++ b/test/basic.spec.ts @@ -3,7 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { Page } from 'puppeteer'; import * as dappeteer from '../src'; -import { clickOnLogo, openProfileDropdown } from '../src/helpers'; +import { openProfileDropdown } from '../src/helpers'; import { PASSWORD, TestContext } from './global'; import { clickElement } from './utils/utils'; @@ -26,6 +26,10 @@ describe('basic interactions', async function () { } }); + afterEach(async function () { + await metamask.page.reload(); + }); + after(async function () { await testPage.close(); }); @@ -60,12 +64,11 @@ describe('basic interactions', async function () { // TODO: cover more cases it('should add token', async () => { - await metamask.switchNetwork('kovan'); + await metamask.switchNetwork('mainnet'); await metamask.addToken({ tokenAddress: '0x4f96fe3b7a6cf9725f59d353f723c1bdb64ca6aa', symbol: 'KAKI', }); - await metamask.switchNetwork('local'); }); it('should add network with required params', async () => { @@ -83,19 +86,6 @@ describe('basic interactions', async function () { await metamask.switchNetwork('local'); }); - it('should fail to add network with wrong chain ID', async () => { - await expect( - metamask.addNetwork({ - networkName: 'Optimistic Ethereum Testnet Kovan', - rpc: 'https://kovan.optimism.io/', - chainId: 420, - symbol: 'KUR', - }), - ).to.be.rejectedWith(SyntaxError); - - await clickOnLogo(metamask.page); - }); - it('should import private key', async () => { const countAccounts = async (): Promise => { await openProfileDropdown(metamask.page); diff --git a/test/contract.spec.ts b/test/contract.spec.ts index eea1c766..d7275033 100644 --- a/test/contract.spec.ts +++ b/test/contract.spec.ts @@ -8,7 +8,6 @@ import { TestContext } from './global'; import { clickElement, pause } from './utils/utils'; describe('contract interactions', async function () { - // eslint-disable-next-line @typescript-eslint/no-explicit-any let contract: Contract; let testPage: Page; let metamask: Dappeteer; @@ -24,6 +23,8 @@ describe('contract interactions', async function () { } catch (e) { //ignored } + await metamask.switchAccount(1); + await metamask.switchNetwork('local'); }); after(async function (this: TestContext) { @@ -31,8 +32,6 @@ describe('contract interactions', async function () { }); it('should have increased count', async () => { - await metamask.switchAccount(1); - await metamask.switchNetwork('local'); await pause(1); const counterBefore = await getCounterNumber(contract); // click increase button @@ -46,7 +45,6 @@ describe('contract interactions', async function () { const counterAfter = await getCounterNumber(contract); expect(counterAfter).to.be.equal(counterBefore + 1); - await metamask.switchNetwork('main'); }); }); diff --git a/test/global.ts b/test/global.ts index 455f96d2..a426b29d 100644 --- a/test/global.ts +++ b/test/global.ts @@ -1,4 +1,3 @@ -import { writeFileSync } from 'fs'; import http from 'http'; import path from 'path'; @@ -65,10 +64,10 @@ export const mochaHooks = { async afterEach(this: TestContext): Promise { if (this.currentTest.state === 'failed') { - writeFileSync( - path.resolve(__dirname, `../${this.currentTest.fullTitle()}.png`), - await this.metamask.page.screenshot({ encoding: 'binary', fullPage: true }), - ); + await this.metamask.page.screenshot({ + path: path.resolve(__dirname, `../${this.currentTest.fullTitle()}.png`), + fullPage: true, + }); } }, };