diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2bbb26c1..551be04f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,9 @@ jobs: checks: name: Checks runs-on: ubuntu-latest + strategy: + matrix: + mm-version: [mm, flask] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -25,7 +28,7 @@ jobs: - name: Lint run: yarn run lint - name: Tests - run: xvfb-run --auto-servernum yarn run test --timeout 50000 + run: 'xvfb-run --auto-servernum yarn run test:${{ matrix.mm-version }} --timeout 50000' - uses: actions/upload-artifact@v3 if: always() with: diff --git a/.mocharc.yml b/.mocharc.yml index 31b24d9a..88be8387 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -3,8 +3,5 @@ recursive: true color: true timeout: 20000 exit: true -require: - - 'ts-node/register' - - 'test/global.ts' spec: - 'test/**/*.spec.ts' \ No newline at end of file diff --git a/package.json b/package.json index 6ed5cf2b..4589cce4 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,9 @@ "build": "tsc -p tsconfig.build.json", "lint": "eslint --color --ext .ts src/ test/", "lint:fix": "yarn run lint --fix", - "test": "mocha", - "test:dev": "mocha --timeout 36000000" + "test": "yarn run test:*", + "test:mm": "mocha --require ts-node/register --require test/global.ts", + "test:flask": "mocha --require ts-node/register --require test/global_flask.ts" }, "repository": { "type": "git", diff --git a/src/helpers/actions.ts b/src/helpers/actions.ts index 1ef55cd1..51447bc6 100644 --- a/src/helpers/actions.ts +++ b/src/helpers/actions.ts @@ -36,14 +36,18 @@ export const openNetworkDropdown = async (page: Page): Promise => { }; export const openProfileDropdown = async (page: Page): Promise => { - const accountSwitcher = await page.waitForSelector(".identicon"); + const accountSwitcher = await page.waitForSelector(".identicon", { + visible: true, + }); await accountSwitcher.click(); }; export const openAccountDropdown = async (page: Page): Promise => { const accMenu = await getAccountMenuButton(page); await accMenu.click(); - await page.waitForSelector(".menu__container.account-options-menu"); + await page.waitForSelector(".menu__container.account-options-menu", { + visible: true, + }); }; export const clickOnElement = async ( @@ -64,7 +68,9 @@ export const clickOnButton = async ( }; export const clickOnLogo = async (page: Page): Promise => { - const header = await page.waitForSelector(".app-header__logo-container"); + const header = await page.waitForSelector(".app-header__logo-container", { + visible: true, + }); await header.click(); }; diff --git a/src/helpers/selectors.ts b/src/helpers/selectors.ts index 2f1b41e7..2383cb48 100644 --- a/src/helpers/selectors.ts +++ b/src/helpers/selectors.ts @@ -61,4 +61,6 @@ 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')]`, { + visible: true, + }); diff --git a/src/metamask/addToken.ts b/src/metamask/addToken.ts index b32770ab..407cc4f7 100644 --- a/src/metamask/addToken.ts +++ b/src/metamask/addToken.ts @@ -14,7 +14,9 @@ export const addToken = async ({ tokenAddress, symbol, decimals = 0 }: AddToken): Promise => { await page.bringToFront(); await clickOnButton(page, "Assets"); - await page.waitForSelector(".asset-list-item__token-button"); + await page.waitForSelector(".asset-list-item__token-button", { + visible: true, + }); await clickOnElement(page, "import tokens"); await clickOnButton(page, "Custom token"); diff --git a/src/setup/launch.ts b/src/setup/launch.ts index 808712b6..5ca2aaec 100644 --- a/src/setup/launch.ts +++ b/src/setup/launch.ts @@ -10,13 +10,15 @@ import { LaunchOptions } from "../types"; import { isNewerVersion } from "./isNewerVersion"; import downloader from "./metaMaskDownloader"; +export type DappeteerBrowser = puppeteer.Browser & { flask?: boolean }; + /** * Launch Puppeteer chromium instance with MetaMask plugin installed * */ export async function launch( puppeteerLib: typeof puppeteer, options: LaunchOptions -): Promise { +): Promise { if ( !options || (!options.metaMaskVersion && !(options as CustomOptions).metaMaskPath) @@ -50,11 +52,19 @@ export async function launch( `Seems you are running older version of MetaMask that recommended by dappeteer team. Use it at your own risk or set the recommended version "${RECOMMENDED_METAMASK_VERSION}".` ); - else console.log(`Running tests on MetaMask version ${metaMaskVersion}`); + else + console.log( + `Running tests on MetaMask version ${metaMaskVersion} (flask: ${String( + options.metaMaskFlask ?? false + )})` + ); console.log(); // new line - METAMASK_PATH = await downloader(metaMaskVersion, metaMaskLocation); + METAMASK_PATH = await downloader(metaMaskVersion, { + location: metaMaskLocation, + flask: options.metaMaskFlask, + }); } else { console.log(`Running tests on local MetaMask build`); @@ -62,7 +72,7 @@ export async function launch( /* eslint-enable no-console */ } - return puppeteerLib.launch({ + const browser = await puppeteerLib.launch({ headless: false, args: [ `--disable-extensions-except=${METAMASK_PATH}`, @@ -71,4 +81,9 @@ export async function launch( ], ...rest, }); + + if (options.metaMaskFlask) { + Object.assign(browser, { flask: true }); + } + return browser; } diff --git a/src/setup/metaMaskDownloader.ts b/src/setup/metaMaskDownloader.ts index 9b3458dd..045fad7c 100644 --- a/src/setup/metaMaskDownloader.ts +++ b/src/setup/metaMaskDownloader.ts @@ -15,7 +15,11 @@ export type Path = extract: string; }; -export default async (version: string, location?: Path): Promise => { +export default async ( + version: string, + options?: { location?: Path; flask?: boolean } +): Promise => { + const location = options.location; const metaMaskDirectory = typeof location === "string" ? location @@ -26,17 +30,22 @@ export default async (version: string, location?: Path): Promise => { : location?.download || path.resolve(defaultDirectory, "download"); if (version !== "latest") { - const extractDestination = path.resolve( - metaMaskDirectory, - version.replace(/\./g, "_") - ); + let filename = version.replace(/\./g, "_"); + if (options?.flask) { + filename = "flask_" + filename; + } + const extractDestination = path.resolve(metaMaskDirectory, filename); if (fs.existsSync(extractDestination)) return extractDestination; } - const { filename, downloadUrl, tag } = await getMetaMaskReleases(version); - const extractDestination = path.resolve( - metaMaskDirectory, - tag.replace(/\./g, "_") + const { filename, downloadUrl, tag } = await getMetaMaskReleases( + version, + options?.flask ?? false ); + let destFilename = tag.replace(/\./g, "_"); + if (options?.flask) { + destFilename = "flask_" + filename; + } + const extractDestination = path.resolve(metaMaskDirectory, destFilename); if (!fs.existsSync(extractDestination)) { const downloadedFile = await downloadMetaMaskReleases( filename, @@ -94,7 +103,10 @@ const downloadMetaMaskReleases = ( type MetaMaskReleases = { downloadUrl: string; filename: string; tag: string }; const metaMaskReleasesUrl = "https://api.github.com/repos/metamask/metamask-extension/releases"; -const getMetaMaskReleases = (version: string): Promise => +const getMetaMaskReleases = ( + version: string, + flask: boolean +): Promise => new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/naming-convention const request = get( @@ -118,16 +130,22 @@ const getMetaMaskReleases = (version: string): Promise => ) { for (const asset of result.assets) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - if (asset.name.includes("chrome")) + if ( + (!flask && asset.name.includes("chrome")) || + (flask && + asset.name.includes("flask") && + asset.name.includes("chrome")) + ) { resolve({ downloadUrl: asset.browser_download_url, filename: asset.name, tag: result.tag_name, }); + } } } } - reject(`Version ${version} not found!`); + reject(`Version ${version} (flask: ${String(flask)}) not found!`); }); } ); diff --git a/src/setup/setupActions.ts b/src/setup/setupActions.ts index 39d85a53..21732176 100644 --- a/src/setup/setupActions.ts +++ b/src/setup/setupActions.ts @@ -18,6 +18,10 @@ export async function showTestNets(metaMaskPage: Page): Promise { await clickOnLogo(metaMaskPage); } +export async function acceptTheRisks(metaMaskPage: Page): Promise { + await clickOnButton(metaMaskPage, "I accept the risks"); +} + export async function confirmWelcomeScreen(metaMaskPage: Page): Promise { await clickOnButton(metaMaskPage, "Get started"); } diff --git a/src/setup/setupMetaMask.ts b/src/setup/setupMetaMask.ts index eb82fa59..fe54fddd 100644 --- a/src/setup/setupMetaMask.ts +++ b/src/setup/setupMetaMask.ts @@ -3,7 +3,9 @@ import { Browser, BrowserContext, Page, Target } from "puppeteer"; import { getMetaMask } from "../metamask"; import { Dappeteer, MetaMaskOptions } from "../types"; +import { DappeteerBrowser } from "./launch"; import { + acceptTheRisks, closePortfolioTooltip, closeWhatsNewModal, confirmWelcomeScreen, @@ -25,14 +27,26 @@ const defaultMetaMaskSteps: Step[] = [ closeWhatsNewModal, closeWhatsNewModal, ]; +const flaskMetaMaskSteps: Step[] = [ + acceptTheRisks, + importAccount, + showTestNets, + closePortfolioTooltip, + closeWhatsNewModal, + closeWhatsNewModal, +]; export async function setupMetaMask( - browser: Browser | BrowserContext, + browser: Browser | BrowserContext | DappeteerBrowser, options?: Options, - steps: Step[] = defaultMetaMaskSteps + steps?: Step[] ): Promise { const page = await getMetamaskPage(browser); - await page.setViewport({ height: 1200, width: 800 }); + steps = steps ?? defaultMetaMaskSteps; + if ((browser as DappeteerBrowser).flask) { + steps = flaskMetaMaskSteps; + } + await page.setViewport({ height: 800, width: 800 }); // goes through the installation steps required by MetaMask for (const step of steps) { await step(page, options); diff --git a/src/types.ts b/src/types.ts index 52be7627..eb50f756 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,10 @@ import { Path } from "./setup/metaMaskDownloader"; import { RECOMMENDED_METAMASK_VERSION } from "./index"; -export type LaunchOptions = OfficialOptions | CustomOptions; +export type LaunchOptions = (OfficialOptions | CustomOptions) & { + //install flask (canary) version of metamask. + metaMaskFlask?: boolean; +}; type PuppeteerLaunchOptions = puppeteer.LaunchOptions & puppeteer.BrowserLaunchArgumentOptions & diff --git a/test/basic.spec.ts b/test/basic.spec.ts index 12cf422d..f443f205 100644 --- a/test/basic.spec.ts +++ b/test/basic.spec.ts @@ -5,7 +5,7 @@ import { Page } from "puppeteer"; import * as dappeteer from "../src"; import { openProfileDropdown } from "../src/helpers"; -import { PASSWORD, TestContext } from "./global"; +import { PASSWORD, TestContext } from "./constant"; import { clickElement } from "./utils/utils"; use(chaiAsPromised); diff --git a/test/constant.ts b/test/constant.ts new file mode 100644 index 00000000..eb030c7e --- /dev/null +++ b/test/constant.ts @@ -0,0 +1,25 @@ +import http from "http"; + +import { Provider, Server } from "ganache"; +import { Browser } from "puppeteer"; + +import { Dappeteer } from "../src"; + +import { Contract } from "./deploy"; + +export type InjectableContext = Readonly<{ + provider: Provider; + ethereum: Server<"ethereum">; + testPageServer: http.Server; + browser: Browser; + metamask: Dappeteer; + contract: Contract; + flask: boolean; +}>; + +// TestContext will be used by all the test +export type TestContext = Mocha.Context & InjectableContext; + +export const LOCAL_PREFUNDED_MNEMONIC = + "pioneer casual canoe gorilla embrace width fiction bounce spy exhibit another dog"; +export const PASSWORD = "password1234"; diff --git a/test/contract.spec.ts b/test/contract.spec.ts index a4922986..dca86307 100644 --- a/test/contract.spec.ts +++ b/test/contract.spec.ts @@ -3,8 +3,8 @@ import { Page } from "puppeteer"; import { Dappeteer } from "../src"; +import { TestContext } from "./constant"; import { Contract } from "./deploy"; -import { TestContext } from "./global"; import { clickElement, pause } from "./utils/utils"; describe("contract interactions", function () { diff --git a/test/global.ts b/test/global.ts index 5ed07f30..8cad7bc1 100644 --- a/test/global.ts +++ b/test/global.ts @@ -1,34 +1,16 @@ -import http from "http"; import path from "path"; -import { Provider, Server } from "ganache"; -import puppeteer, { Browser } from "puppeteer"; +import puppeteer from "puppeteer"; import * as dappeteer from "../src"; -import { Dappeteer } from "../src"; import { - Contract, - deployContract, - startLocalEthereum, - startTestServer, -} from "./deploy"; - -export type InjectableContext = Readonly<{ - provider: Provider; - ethereum: Server<"ethereum">; - testPageServer: http.Server; - browser: Browser; - metamask: Dappeteer; - contract: Contract; -}>; - -// TestContext will be used by all the test -export type TestContext = Mocha.Context & InjectableContext; - -export const LOCAL_PREFUNDED_MNEMONIC = - "pioneer casual canoe gorilla embrace width fiction bounce spy exhibit another dog"; -export const PASSWORD = "password1234"; + InjectableContext, + LOCAL_PREFUNDED_MNEMONIC, + PASSWORD, + TestContext, +} from "./constant"; +import { deployContract, startLocalEthereum, startTestServer } from "./deploy"; export const mochaHooks = { async beforeAll(this: Mocha.Context): Promise { @@ -57,6 +39,7 @@ export const mochaHooks = { browser, testPageServer: server, metamask, + flask: false, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment contract, }; diff --git a/test/global_flask.ts b/test/global_flask.ts new file mode 100644 index 00000000..4b2d2621 --- /dev/null +++ b/test/global_flask.ts @@ -0,0 +1,65 @@ +import path from "path"; + +import puppeteer from "puppeteer"; + +import * as dappeteer from "../src"; + +import { + InjectableContext, + LOCAL_PREFUNDED_MNEMONIC, + PASSWORD, + TestContext, +} from "./constant"; +import { deployContract, startLocalEthereum, startTestServer } from "./deploy"; + +export const mochaHooks = { + async beforeAll(this: Mocha.Context): Promise { + const ethereum = await startLocalEthereum({ + wallet: { + mnemonic: LOCAL_PREFUNDED_MNEMONIC, + defaultBalance: 100, + }, + }); + const browser = await dappeteer.launch(puppeteer, { + metaMaskVersion: + process.env.METAMASK_VERSION || dappeteer.RECOMMENDED_METAMASK_VERSION, + metaMaskFlask: true, + }); + const server = await startTestServer(); + const metamask = await dappeteer.setupMetaMask(browser, { + // optional, else it will use a default seed + seed: LOCAL_PREFUNDED_MNEMONIC, + password: PASSWORD, + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const contract = await deployContract(ethereum.provider); + + const context: InjectableContext = { + ethereum: ethereum, + provider: ethereum.provider, + browser, + testPageServer: server, + metamask, + flask: true, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + contract, + }; + + Object.assign(this, context); + }, + + async afterAll(this: TestContext): Promise { + this.testPageServer.close(); + await this.browser.close(); + await this.ethereum.close(); + }, + + async afterEach(this: TestContext): Promise { + if (this.currentTest.state === "failed") { + await this.metamask.page.screenshot({ + path: path.resolve(__dirname, `../${this.currentTest.fullTitle()}.png`), + fullPage: true, + }); + } + }, +};