diff --git a/README.md b/README.md index d9962be1..c42a96b6 100644 --- a/README.md +++ b/README.md @@ -12,28 +12,89 @@ $ yarn add @chainsafe/dappeteer ## Usage ```js -import puppeteer from 'puppeteer'; import dappeteer from '@chainsafe/dappeteer'; async function main() { - const [metamask, page] = await dappeteer.bootstrap(puppeteer, { metaMaskVersion: 'v10.15.0' }); + const { metaMask, browser } = await dappeteer.bootstrap(); + + // create a new page and visit your dapp + const dappPage = browser.newPage(); + await dappPage.goto('http://my-dapp.com'); // you can change the network if you want - await metamask.switchNetwork('ropsten'); + await metaMask.switchNetwork('goerli'); - // you can import a token - await metamask.addToken({ - tokenAddress: '0x4f96fe3b7a6cf9725f59d353f723c1bdb64ca6aa', - symbol: 'KAKI', - }); + // do something in your dapp that prompts MetaMask to add a Token + const addTokenButton = await dappPage.$('#add-token'); + await addTokenButton.click(); + // instruct MetaMask to accept this request + await metaMask.acceptAddToken(); - // go to a dapp and do something that prompts MetaMask to confirm a transaction - await page.goto('http://my-dapp.com'); - const payButton = await page.$('#pay-with-eth'); + // do something that prompts MetaMask to confirm a transaction + const payButton = await dappPage.$('#pay-with-eth'); await payButton.click(); // 🏌 - await metamask.confirmTransaction(); + await metaMask.confirmTransaction(); +} + +main(); +``` + +## Usage with Snaps + +```js +import dappeteer from '@chainsafe/dappeteer'; +import { exec } from "child_process"; + +async function buildSnap(): Promise { + console.log(`Building my-snap...`); + await new Promise((resolve, reject) => { + exec(`cd ./my-snap && npx mm-snap build`, (error, stdout) => { + if (error) { + reject(error); + return; + } + resolve(stdout); + }); + }); + + return "./my-snap"; +} + +async function main() { + // you need to have a webpage open to interact with MetaMask, you can also visit a dApp page + const dappPage = browser.newPage(); + await dappPage.goto('http://example.org/'); + + // build your local snap + const builtSnapDir = await buildSnap() + + // setup dappateer and install your snap + const { snapId, metaMask, dappPage } = await dappeteer.initSnapEnv({ + snapIdOrLocation: builtSnapDir + hasPermissions: true, + hasKeyPermissions: false, + }); + + // invoke a method from your snap that promps users with approve/reject dialog + metaMask.snaps.invokeSnap(dappPage, snapId, "my-method") + + // instruct MetaMask to accept this request + await metaMask.snaps.acceptDialog(); + + // get the notification emitter and the promise that will receive the notifications + const emitter = await metaMask.snaps.getNotificationEmitter(); + const notificationPromise = emitter.waitForNotification(); + + // do something that prompts you snap to emit notifications + await metaMask.snaps.invokeSnap(dappPage, snapId, "notify"); + + // Make sure the notification promise has resolved + await notificationPromise; + + // You can now read the snap notifications and run tests against them + const notifications = await metaMask.snaps.getAllNotifications(); } main(); diff --git a/docs/API.md b/docs/API.md index 1519abd3..f116b29a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -6,23 +6,34 @@ For additional information read root [readme](../README.md) - [Launch dAppeteer](#launch) - [Setup MetaMask](#setup) - [Bootstrap dAppeteer](#bootstrap) +- [Initialize Snap Environment](#initSnapEnv) - [Get MetaMask Window](#getMetaMask) -- [dAppeteer methods](#methods) +- [metaMask methods](#methods) - [switchAccount](#switchAccount) - [importPK](#importPK) - [lock](#lock) - [unlock](#unlock) - [switchNetwork](#switchNetwork) - - [addNetwork](#addNetwork) - - [addToken](#addToken) + - [acceptAddNetwork](#acceptAddNetwork) + - [rejectAddNetwork](#rejectAddNetwork) + - [acceptAddToken](#acceptAddToken) + - [rejectAddToken](#rejectAddToken) - [confirmTransaction](#confirmTransaction) - [sign](#sign) + - [signTypedData](#signTypedData) - [approve](#approve) - [helpers](#helpers) - [getTokenBalance](#getTokenBalance) - [deleteAccount](#deleteAccount) - [deleteNetwork](#deleteNetwork) - [page](#page) + - [snaps methods](#snaps-methods) + - [installSnap](#installSnap) + - [invokeSnap](#invokeSnap) + - [acceptDialog](#acceptDialog) + - [rejectDialog](#rejectDialog) + - [getNotificationEmitter](#getNotificationEmitter) + - [getAllNotifications](#getAllNotifications) # dAppeteer setup methods @@ -36,10 +47,10 @@ interface OfficialOptions { type Path = string | { download: string; extract: string; }; ``` -or +or ```typescript interface CustomOptions { - metamaskPath: string; + metaMaskPath: string; }; ``` @@ -47,6 +58,7 @@ returns an instance of `browser` same as `puppeteer.launch`, but it also install ## `dappeteer.setupMetaMask(browser: Browser, options: MetaMaskOptions = {}, steps: Step[]): Promise` + ```typescript interface MetaMaskOptions { seed?: string; @@ -60,68 +72,120 @@ type Step = (page: Page, options?: Options) => void; ``` -## `dappeteer.bootstrap(puppeteerLib: typeof puppeteer, options: OfficialOptions & MetaMaskOptions): Promise<[Dappeteer, Page, Browser]>` +## `dappeteer.bootstrap(options: DappeteerLaunchOptions & MetaMaskOptions): Promise<{ + metaMask: Dappeteer; + browser: DappeteerBrowser; + metaMaskPage: DappeteerPage; +}>` + ```typescript -interface OfficialOptions { - metaMaskVersion: 'latest' | string; +type DappeteerLaunchOptions = { + metaMaskVersion?: + | "latest" + | "local" + | string; metaMaskLocation?: Path; + metaMaskPath?: string; + metaMaskFlask?: boolean; + automation?: "puppeteer" | "playwright"; + browser: "chrome"; + puppeteerOptions?: Omit[0], "headless">; + playwrightOptions?: Omit; +}; + +type MetaMaskOptions = { + seed?: string; + password?: string; + showTestNets?: boolean; }; ``` -it runs `dappeteer.launch` and `dappeteer.setup` and return array with dappetter, page and browser + +it runs it runs `dappeteer.launch` and `dappeteer.setupMetaMask` and returns an object with metaMask, metaMaskPage and browser. + + +## `dappeteer.initSnapEnv( opts: DappeteerLaunchOptions & MetaMaskOptions & InstallSnapOptions & { snapIdOrLocation: string }): Promise<{ metaMask: Dappeteer; browser: DappeteerBrowser; metaMaskPage: DappeteerPage; snapId: string;}` + +```typescript +type DappeteerLaunchOptions = { + metaMaskVersion?: + | "latest" + | "local" + | string; + metaMaskLocation?: Path; + metaMaskPath?: string; + metaMaskFlask?: boolean; + automation?: "puppeteer" | "playwright"; + browser: "chrome"; + puppeteerOptions?: Omit[0], "headless">; + playwrightOptions?: Omit; +}; + +type MetaMaskOptions = { + seed?: string; + password?: string; + showTestNets?: boolean; +}; + +type InstallSnapOptions = { + hasPermissions: boolean; + hasKeyPermissions: boolean; + customSteps?: InstallStep[]; + version?: string; + installationSnapUrl?: string; +} +``` + +it runs `dappeteer.launch` and `dappeteer.setupMetamask` and `snaps.installSnap` and returns an object with metaMask, metaMaskPage, browser and snapId. ## `dappeteer.getMetaMaskWindow(browser: Browser, version?: string): Promise` -# dAppeteer methods -`metamask` is used as placeholder for dAppeteer returned by [`setupMetaMask`](setup) or [`getMetaMaskWindow`](getMetaMask) - +# metaMask methods +`metaMask` is used as placeholder for dAppeteer returned by [`setupMetaMask`](setup) or [`getMetaMaskWindow`](getMetaMask) -## `metamask.switchAccount(accountNumber: number): Promise` +## `metaMask.switchAccount(accountNumber: number): Promise` it commands MetaMask to switch to a different account, by passing the index/position of the account in the accounts list. -## `metamask.importPK(privateKey: string): Promise` +## `metaMask.importPK(privateKey: string): Promise` it commands MetaMask to import an private key. It can only be used while you haven't signed in yet, otherwise it throws. -## `metamask.lock(): Promise` +## `metaMask.lock(): Promise` signs out from MetaMask. It can only be used if you arelady signed it, otherwise it throws. -## `metamask.unlock(password: string): Promise` +## `metaMask.unlock(password: string): Promise` it unlocks the MetaMask extension. It can only be used in you locked/signed out before, otherwise it throws. The password is optional, it defaults to `password1234`. -## `metamask.switchNetwork(network: string): Promise` -it changes the current selected network. `networkName` can take the following values: `"main"`, `"ropsten"`, `"rinkeby"`, `"kovan"`, `"localhost"`. +## `metaMask.switchNetwork(network: string): Promise` +it changes the current selected network. `networkName` can take the following values: `"mainnet"`, `"goerli"`, `"sepolia"`, `"ropsten"`, `"rinkeby"`, `"kovan"`, `"localhost"`. - -## `metamask.addNetwork(options: AddNetwork): Promise` -```typescript -interface AddNetwork { - networkName: string; - rpc: string; - chainId: number; - symbol: string; -} -``` -it adds a custom network to MetaMask. + +## `metaMask.acceptAddNetwork(shouldSwitch?: boolean): Promise` - -## `metamask.addToken(tokenAddress: string): Promise` -```typescript -interface AddToken { - tokenAddress: string; - symbol?: string; - decimals?: number; -} -``` -it adds a custom token to MetaMask. +commands MetaMask to accept a Network addition. For this to work MetaMask has to be in a Network addition state (basically prompting the user to accept/reject a Network addition). You can optionnaly tell MetaMask to switch to this network by passing the `true` parameter (default to `false`). + + +## `metaMask.rejectAddNetwork(): Promise` + +commands MetaMask to reject a Network addition. For this to work MetaMask has to be in a Network addition state (basically prompting the user to accept/reject a Network addition). + + +## `metaMask.acceptAddToken(): Promise` + +commands MetaMask to accept a Token addition. For this to work MetaMask has to be in a Token addition state (basically prompting the user to accept/reject a Token addition). + + +## `metaMask.rejectAddToken(): Promise` + +commands MetaMask to reject a Token addition. For this to work MetaMask has to be in a Token addition state (basically prompting the user to accept/reject a Token addition). -## `metamask.confirmTransaction(options?: TransactionOptions): Promise` +## `metaMask.confirmTransaction(options?: TransactionOptions): Promise` ```typescript interface TransactionOptions { gas?: number; @@ -129,32 +193,62 @@ interface TransactionOptions { priority?: number; } ``` -commands MetaMask to submit a transaction. For this to work MetaMask has to be in a transaction confirmation state (basically promting the user to submit/reject a transaction). You can (optionally) pass an object with `gas` and/or `gasLimit`, by default they are `20` and `50000` respectively. - +commands MetaMask to submit a transaction. For this to work MetaMask has to be in a transaction confirmation state (basically prompting the user to submit/reject a transaction). You can (optionally) pass an object with `gas` and/or `gasLimit`, by default they are `20` and `50000` respectively. -## `metamask.sign(): Promise` +## `metaMask.sign(): Promise` commands MetaMask to sign a message. For this to work MetaMask must be in a sign confirmation state. + +## `metaMask.signTypedData(): Promise` +commands MetaMask to sign a message. For this to work MetaMask must be in a sign typed data confirmation state. + -## `metamask.approve(): Promise` +## `metaMask.approve(): Promise` enables the app to connect to MetaMask account in privacy mode -## `metamask.helpers` +# Helpers methods -### `metamask.helpers.getTokenBalance(tokenSymbol: string): Promise` +## `metaMask.helpers.getTokenBalance(tokenSymbol: string): Promise` get balance of specific token -### `metamask.helpers.deleteAccount(accountNumber: number): Promise` +## `metaMask.helpers.deleteAccount(accountNumber: number): Promise` deletes account containing name with specified number -### `metamask.helpers.deleteNetwork(): Promise` -deletes custom network from metamask +## `metaMask.helpers.deleteNetwork(): Promise` +deletes custom network from metaMask -## `metamask.page` is MetaMask plugin `Page` +## `metaMask.page` is the MetaMask plugin `Page` **for advanced usages** in case you need custom features. + + +# Snaps methods + + +## `metaMask.snaps.installSnap: (snapIdOrLocation: string, opts: { hasPermissions: boolean; hasKeyPermissions: boolean; customSteps?: InstallStep[]; version?: string;},installationSnapUrl?: string`) => Promise; +installs the snap. The `snapIdOrLocation` param is either the snapId or the full path to your snap directory. + + +## `metaMask.snaps.invokeSnap(page: DappeteerPage,snapId: string,method: string,params?: Params): Promise>` +invokes a MetaMask snap method. The snapId is the id of your installed snap (result of invoking `installSnap` method). This function will throw if there is an error while invoking snap. + + +## `metaMask.snaps.acceptDialog(): Promise` +accepts a snap_confirm dialog + + +## `metaMask.snaps.rejectDialog(): Promise` +rejects snap_confirm dialog + + +## `metaMask.snaps.getNotificationEmitter(): Promise` +returns emitter to listen for notifications appearance in notification page + + +## `metaMask.snaps.getAllNotifications(): Promise` +Returns all notifications in MetaMask notifications page diff --git a/docs/JEST.md b/docs/JEST.md index 972e9e07..6497c2e6 100644 --- a/docs/JEST.md +++ b/docs/JEST.md @@ -17,7 +17,7 @@ describe('Ethereum', () => { await page.goto('https://ethereum.org/en/'); }); - it('should be titled "Google"', async () => { + it('should be titled "Home | ethereum.org"', async () => { await expect(page.title()).resolves.toMatch('Home | ethereum.org'); }); }); @@ -28,7 +28,7 @@ To configure Dappeteer to use custom config values as `metamaskVersion` or own ` **example of `dappeteer.config.js`** ``` js -/** @type {import('@chainsafe/dappeteer').DappateerJestConfig} */ +/** @type {import('@chainsafe/dappeteer').DappeteerJestConfig} */ const config = { dappeteer: { @@ -47,7 +47,7 @@ module.exports = config; In case you need more customisable use case you can rebuild it from scratch. -First lets define or entry `jest.config.js` +First let's define our entry `jest.config.js` ```js // jest.config.js @@ -58,7 +58,7 @@ module.exports = { }; ``` -Then create `setup.js` with responsibility to start Puppeteer with MetaMask and `teardown.js` for clean up after test's +Then create `setup.js` with responsibility to start Puppeteer with MetaMask and `teardown.js` for clean up after tests ```js // setup.js diff --git a/src/helpers/actions.ts b/src/helpers/actions.ts index eddbb942..c6da784c 100644 --- a/src/helpers/actions.ts +++ b/src/helpers/actions.ts @@ -133,7 +133,7 @@ export const clickOnLittleDownArrowIfNeeded = async ( visible: true, }); - // Metamask requires users to read all the data + // MetaMask requires users to read all the data // and scroll until the bottom of the message // before enabling the "Sign" button const isSignButtonDisabled = await page.$eval( diff --git a/src/metamask/index.ts b/src/metamask/index.ts index 8890e967..55f4f68d 100644 --- a/src/metamask/index.ts +++ b/src/metamask/index.ts @@ -84,7 +84,7 @@ export async function getMetaMaskWindow( for (const page of pages) { if (page.url().includes("chrome-extension")) resolve(page); } - reject("Metamask extension not found"); + reject("MetaMask extension not found"); }) .catch((e) => reject(e)); }); diff --git a/src/metamask/switchNetwork.ts b/src/metamask/switchNetwork.ts index 0135b6d7..b1a857dd 100644 --- a/src/metamask/switchNetwork.ts +++ b/src/metamask/switchNetwork.ts @@ -4,7 +4,7 @@ import { DappeteerPage } from "../page"; // TODO: validate - for now works fine as it is. export const switchNetwork = (page: DappeteerPage) => - async (network: string = "main"): Promise => { + async (network: string = "mainnet"): Promise => { await page.bringToFront(); await openNetworkDropdown(page); diff --git a/src/setup/index.ts b/src/setup/index.ts index 0d6b4a6e..a4b6dfb8 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -14,22 +14,21 @@ export const bootstrap = async ({ showTestNets, ...launchOptions }: DappeteerLaunchOptions & MetaMaskOptions): Promise<{ - dappeteer: Dappeteer; + metaMask: Dappeteer; browser: DappeteerBrowser; - page: DappeteerPage; + metaMaskPage: DappeteerPage; }> => { const browser = await launch(launchOptions); - const dappeteer = await setupMetaMask(browser, { + const metaMask = await setupMetaMask(browser, { seed, password, showTestNets, }); - const pages = await browser.pages(); return { - dappeteer, + metaMask, browser, - page: pages[0], + metaMaskPage: metaMask.page, }; }; @@ -38,9 +37,9 @@ export const initSnapEnv = async ( MetaMaskOptions & InstallSnapOptions & { snapIdOrLocation: string } ): Promise<{ - dappeteer: Dappeteer; + metaMask: Dappeteer; browser: DappeteerBrowser; - page: DappeteerPage; + metaMaskPage: DappeteerPage; snapId: string; }> => { const browser = await launch({ @@ -48,19 +47,19 @@ export const initSnapEnv = async ( metaMaskFlask: true, }); const { snapIdOrLocation, seed, password, showTestNets } = opts; - const dappeteer = await setupMetaMask(browser, { + const metaMask = await setupMetaMask(browser, { seed, password, showTestNets, }); - const page = dappeteer.page; + const metaMaskPage = metaMask.page; - const snapId = await dappeteer.snaps.installSnap(snapIdOrLocation, opts); + const snapId = await metaMask.snaps.installSnap(snapIdOrLocation, opts); return { - dappeteer, + metaMask, browser, - page, + metaMaskPage, snapId, }; }; diff --git a/src/setup/setupMetaMask.ts b/src/setup/setupMetaMask.ts index 1a00e3e9..9cd0940e 100644 --- a/src/setup/setupMetaMask.ts +++ b/src/setup/setupMetaMask.ts @@ -45,7 +45,7 @@ export async function setupMetaMask( options?: Options, steps?: Step[] ): Promise { - const page = await getMetamaskPage(browser); + const page = await getMetaMaskPage(browser); steps = steps ?? defaultMetaMaskSteps; if (browser.isMetaMaskFlask()) { steps = flaskMetaMaskSteps; @@ -59,7 +59,7 @@ export async function setupMetaMask( return getMetaMask(page); } -async function getMetamaskPage( +async function getMetaMaskPage( browser: DappeteerBrowser ): Promise { const pages = await browser.pages(); diff --git a/src/snap/install.ts b/src/snap/install.ts index 157000a6..84ca3c03 100644 --- a/src/snap/install.ts +++ b/src/snap/install.ts @@ -25,14 +25,14 @@ export type InstallSnapOptions = { }; export const installSnap = - (page: DappeteerPage) => + (flaskPage: DappeteerPage) => async ( snapIdOrLocation: string, opts: InstallSnapOptions ): Promise => { - flaskOnly(page); + flaskOnly(flaskPage); //need to open page to access window.ethereum - const installPage = await page.browser().newPage(); + const installPage = await flaskPage.browser().newPage(); await installPage.goto(opts.installationSnapUrl ?? "https://google.com"); let snapServer: http.Server | undefined; if (fs.existsSync(snapIdOrLocation)) { @@ -55,26 +55,26 @@ export const installSnap = { snapId: snapIdOrLocation, version: opts.version } ); - await page.bringToFront(); - await page.reload(); - await clickOnButton(page, "Connect"); + await flaskPage.bringToFront(); + await flaskPage.reload(); + await clickOnButton(flaskPage, "Connect"); if (opts.hasPermissions) { - await clickOnButton(page, "Approve & install"); + await clickOnButton(flaskPage, "Approve & install"); if (opts.hasKeyPermissions) { - await page.waitForSelector(".checkbox-label", { + await flaskPage.waitForSelector(".checkbox-label", { visible: true, }); - for await (const checkbox of await page.$$(".checkbox-label")) { + for await (const checkbox of await flaskPage.$$(".checkbox-label")) { await checkbox.click(); } - await clickOnButton(page, "Confirm"); + await clickOnButton(flaskPage, "Confirm"); } } else { - await clickOnButton(page, "Install"); + await clickOnButton(flaskPage, "Install"); } for (const step of opts.customSteps ?? []) { - await step(page); + await step(flaskPage); } const result = await installAction; @@ -87,23 +87,23 @@ export const installSnap = }; export async function isSnapInstalled( - page: DappeteerPage, + flaskPage: DappeteerPage, snapId: string ): Promise { - await page.bringToFront(); - await profileDropdownClick(page); + await flaskPage.bringToFront(); + await profileDropdownClick(flaskPage); - await clickOnElement(page, "Settings"); - await clickOnElement(page, "Snaps"); + await clickOnElement(flaskPage, "Settings"); + await clickOnElement(flaskPage, "Snaps"); let found = false; try { - await page.waitForXPath(`//*[contains(text(), '${snapId}')]`, { + await flaskPage.waitForXPath(`//*[contains(text(), '${snapId}')]`, { timeout: 5000, }); found = true; } catch (e) { found = false; } - await clickOnLogo(page); + await clickOnLogo(flaskPage); return found; } diff --git a/src/snap/invokeSnap.ts b/src/snap/invokeSnap.ts index f483243e..c46626f6 100644 --- a/src/snap/invokeSnap.ts +++ b/src/snap/invokeSnap.ts @@ -1,5 +1,5 @@ import { DappeteerPage, Serializable, Unboxed } from "../page"; -import { flaskOnly, isMetamaskErrorObject } from "./utils"; +import { flaskOnly, isMetaMaskErrorObject } from "./utils"; export async function invokeSnap< R = unknown, @@ -30,7 +30,7 @@ export async function invokeSnap< }, { snapId, method, params } ); - if (result instanceof Error || isMetamaskErrorObject(result)) { + if (result instanceof Error || isMetaMaskErrorObject(result)) { throw result; } else { return result; diff --git a/src/snap/utils.ts b/src/snap/utils.ts index 0362615a..1c3b14f4 100644 --- a/src/snap/utils.ts +++ b/src/snap/utils.ts @@ -3,12 +3,12 @@ import { DappeteerPage } from "../page"; export function flaskOnly(page: DappeteerPage): void { if (!page.browser().isMetaMaskFlask()) { throw new Error( - "This method is only available when running Metamask Flask" + "This method is only available when running MetaMask Flask" ); } } -export function isMetamaskErrorObject(e: unknown): boolean { +export function isMetaMaskErrorObject(e: unknown): boolean { if (e == undefined) return false; if (!(e instanceof Object)) return false; if (!("code" in e)) return false; diff --git a/src/types.ts b/src/types.ts index 46dd3193..d74f1e74 100644 --- a/src/types.ts +++ b/src/types.ts @@ -70,13 +70,13 @@ export type Dappeteer = { */ getNotificationEmitter: () => Promise; /** - * Returns all notifications in Metamask notifications page + * Returns all notifications in MetaMask notifications page */ getAllNotifications: () => Promise; /** - * Invoke Metamask snap method. Function will throw if there is an error while invoking snap. + * Invoke a MetaMask snap method. Function will throw if there is an error while invoking snap. * Use generic params to override result and parameter types. - * @param page Browser page where injected Metamask provider will be available. + * @param page Browser page where injected MetaMask provider will be available. * For most snaps, openning google.com will suffice. * @param snapId id of your installed snap (result of invoking `installSnap` method) * @param method snap method you want to invoke @@ -89,13 +89,13 @@ export type Dappeteer = { params?: Params ) => Promise>; /** - * Installs snap. Function will throw if there is an error while installing snap. - * @param snapIdOrLocation either pass in snapId or full path to your snap directory - * where we can find bundled snap (you need to ensure snap is built) - * @param opts {Object} snap method you want to invoke - * @param opts.hasPermissions Set to true if snap uses some permissions - * @param opts.hasKeyPermissions Set to true if snap uses key permissions - * @param installationSnapUrl url of your dapp. Defaults to google.com + * Installs snap. Function will throw if there is an error while installing the snap. + * @param snapIdOrLocation either the snapId or the full path to your snap directory + * where we can find the bundled snap (you need to ensure the snap is built) + * @param opts {Object} the snap method you want to invoke + * @param opts.hasPermissions Set to true if the snap uses some permissions + * @param opts.hasKeyPermissions Set to true if the snap uses the key permissions + * @param installationSnapUrl the url of your dapp. Defaults to google.com */ installSnap: ( snapIdOrLocation: string, diff --git a/test/basic.spec.ts b/test/basic.spec.ts index fa565f2a..79139e53 100644 --- a/test/basic.spec.ts +++ b/test/basic.spec.ts @@ -25,18 +25,20 @@ import { use(chaiAsPromised); describe("basic interactions", function () { - let metamask: dappeteer.Dappeteer; + let metaMask: dappeteer.Dappeteer; let testPage: DappeteerPage; + let metaMaskPage: DappeteerPage; before(async function (this: TestContext) { testPage = await this.browser.newPage(); await testPage.goto("http://localhost:8080/", { waitUntil: "networkidle", }); - metamask = this.metamask; + metaMask = this.metaMask; + metaMaskPage = this.metaMaskPage; try { const connectionPromise = testPage.evaluate(requestAccounts); - await metamask.approve(); + await metaMask.approve(); await connectionPromise; } catch (e) { //ignored @@ -53,7 +55,7 @@ describe("basic interactions", function () { message: MESSAGE_TO_SIGN, }); - await metamask.sign(); + await metaMask.sign(); const sig = await sigPromise; expect(sig).to.be.equal(EXPECTED_MESSAGE_SIGNATURE); }); @@ -62,7 +64,7 @@ describe("basic interactions", function () { const sigPromise = testPage.evaluate(signLongTypedData, { address: ACCOUNT_ADDRESS, }); - await metamask.signTypedData(); + await metaMask.signTypedData(); const sig = await sigPromise; expect(sig).to.be.equal(EXPECTED_LONG_TYPED_DATA_SIGNATURE); @@ -72,16 +74,16 @@ describe("basic interactions", function () { const sigPromise = testPage.evaluate(signShortTypedData, { address: ACCOUNT_ADDRESS, }); - await metamask.signTypedData(); + await metaMask.signTypedData(); const sig = await sigPromise; expect(sig).to.be.equal(EXPECTED_SHORT_TYPED_DATA_SIGNATURE); }); it("should switch network", async () => { - await metamask.switchNetwork("localhost"); + await metaMask.switchNetwork("localhost"); - const selectedNetwork = await metamask.page.evaluate( + const selectedNetwork = await metaMaskPage.evaluate( () => document.querySelector(".network-display > span:nth-child(2)").innerHTML ); @@ -89,14 +91,14 @@ describe("basic interactions", function () { }); it("should return eth balance", async () => { - await metamask.switchNetwork("localhost"); - const tokenBalance: number = await metamask.helpers.getTokenBalance("ETH"); + await metaMask.switchNetwork("localhost"); + const tokenBalance: number = await metaMask.helpers.getTokenBalance("ETH"); expect(tokenBalance).to.be.greaterThan(0); - await metamask.switchNetwork("mainnet"); + await metaMask.switchNetwork("mainnet"); }); it("should return 0 token balance when token not found", async () => { - const tokenBalance: number = await metamask.helpers.getTokenBalance( + const tokenBalance: number = await metaMask.helpers.getTokenBalance( "FARTBUCKS" ); expect(tokenBalance).to.be.equal(0); @@ -104,68 +106,66 @@ describe("basic interactions", function () { it("should not add token", async () => { const addTokenPromise = testPage.evaluate(addToken); - await metamask.rejectAddToken(); + await metaMask.rejectAddToken(); const res = await addTokenPromise; expect(res).to.equal(false); }); it("should add token", async () => { const addTokenPromise = testPage.evaluate(addToken); - await metamask.acceptAddToken(); + await metaMask.acceptAddToken(); const res = await addTokenPromise; expect(res).to.equal(true); }); it("should not add network", async () => { const addNetworkPromise = testPage.evaluate(addNetwork); - await metamask.rejectAddNetwork(); + await metaMask.rejectAddNetwork(); const res = await addNetworkPromise; expect(res).to.equal(false); }); it("should add network and switch", async () => { const addNetworkPromise = testPage.evaluate(addNetwork); - await metamask.acceptAddNetwork(); + await metaMask.acceptAddNetwork(); const res = await addNetworkPromise; expect(res).to.equal(true); }); it("should import private key", async () => { const countAccounts = async (): Promise => { - await profileDropdownClick(metamask.page, false); - const container = await metamask.page.$(".account-menu__accounts"); + await profileDropdownClick(metaMaskPage, false); + const container = await metaMaskPage.$(".account-menu__accounts"); const count = (await container.$$(".account-menu__account")).length; - await profileDropdownClick(metamask.page, true); + await profileDropdownClick(metaMaskPage, true); return count; }; const beforeImport = await countAccounts(); - await metamask.importPK( + await metaMask.importPK( "4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b10" ); const afterImport = await countAccounts(); expect(beforeImport + 1).to.be.equal(afterImport); - await metamask.helpers.deleteAccount(2); + await metaMask.helpers.deleteAccount(2); }); it("should throw error on wrong key", async () => { await expect( - metamask.importPK( + metaMask.importPK( "4f3edf983ac636a65a$@!ce7c78d9aa706d3b113bce9c46f30d7d21715b23b10" ) ).to.be.rejectedWith(SyntaxError); }); it("should lock and unlock", async () => { - await metamask.lock(); - const pageTitle = await metamask.page.waitForSelector( - ".unlock-page__title" - ); + await metaMask.lock(); + const pageTitle = await metaMaskPage.waitForSelector(".unlock-page__title"); expect(pageTitle).to.not.be.undefined; - await metamask.unlock(PASSWORD); - const accountSwitcher = await metamask.page.waitForSelector( + await metaMask.unlock(PASSWORD); + const accountSwitcher = await metaMaskPage.waitForSelector( ".account-menu__icon", { visible: true, diff --git a/test/constant.ts b/test/constant.ts index cb0ba80b..2c8e72fe 100644 --- a/test/constant.ts +++ b/test/constant.ts @@ -3,7 +3,7 @@ import http from "http"; import { Provider, Server } from "ganache"; import web3 from "web3"; -import { Dappeteer } from "../src"; +import { Dappeteer, DappeteerPage } from "../src"; import { DappeteerBrowser } from "../src/browser"; import { Contract, Snaps } from "./deploy"; @@ -14,7 +14,8 @@ export type InjectableContext = Readonly<{ testPageServer: http.Server; snapServers?: Record; browser: DappeteerBrowser; - metamask: Dappeteer; + metaMask: Dappeteer; + metaMaskPage: DappeteerPage; contract: Contract; flask: boolean; }>; diff --git a/test/contract.spec.ts b/test/contract.spec.ts index 742bb6d1..306e9a79 100644 --- a/test/contract.spec.ts +++ b/test/contract.spec.ts @@ -17,7 +17,7 @@ describe("contract interactions", function () { before(async function (this: TestContext) { testPage = await this.browser.newPage(); await testPage.goto("http://localhost:8080/", { waitUntil: "networkidle" }); - metamask = this.metamask; + metamask = this.metaMask; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment contract = this.contract; try { diff --git a/test/flask/snaps.spec.ts b/test/flask/snaps.spec.ts index 8cef9789..551524f1 100644 --- a/test/flask/snaps.spec.ts +++ b/test/flask/snaps.spec.ts @@ -1,14 +1,15 @@ import { expect } from "chai"; -import * as dappeteer from "../../src"; -import { DappeteerPage } from "../../src"; +import { DappeteerPage, Dappeteer } from "../../src"; import { TestContext } from "../constant"; import { Snaps } from "../deploy"; describe("snaps", function () { - let metamask: dappeteer.Dappeteer; + let metaMask: Dappeteer; + let metaMaskPage: DappeteerPage; before(function (this: TestContext) { - metamask = this.metamask; + metaMask = this.metaMask; + metaMaskPage = this.metaMaskPage; }); beforeEach(function (this: TestContext) { @@ -19,21 +20,21 @@ describe("snaps", function () { }); it("should install base snap from local server", async function (this: TestContext) { - await metamask.snaps.installSnap(this.snapServers[Snaps.BASE_SNAP], { + await metaMask.snaps.installSnap(this.snapServers[Snaps.BASE_SNAP], { hasPermissions: false, hasKeyPermissions: false, }); }); it("should install permissions snap local server", async function (this: TestContext) { - await metamask.snaps.installSnap(this.snapServers[Snaps.PERMISSIONS_SNAP], { + await metaMask.snaps.installSnap(this.snapServers[Snaps.PERMISSIONS_SNAP], { hasPermissions: true, hasKeyPermissions: false, }); }); it("should install keys snap from local server", async function (this: TestContext) { - await metamask.snaps.installSnap(this.snapServers[Snaps.KEYS_SNAP], { + await metaMask.snaps.installSnap(this.snapServers[Snaps.KEYS_SNAP], { hasPermissions: true, hasKeyPermissions: true, }); @@ -53,45 +54,44 @@ describe("snaps", function () { before(async function (this: TestContext) { if (!this.browser.isMetaMaskFlask()) { this.skip(); - return; } - snapId = await metamask.snaps.installSnap( + snapId = await metaMask.snaps.installSnap( this.snapServers[Snaps.METHODS_SNAP], { hasPermissions: true, hasKeyPermissions: false, } ); - testPage = await metamask.page.browser().newPage(); + testPage = await metaMaskPage.browser().newPage(); await testPage.goto("https://google.com"); return testPage; }); it("should invoke provided snap method and ACCEPT the dialog", async function (this: TestContext) { - const invokeAction = metamask.snaps.invokeSnap( + const invokeAction = metaMask.snaps.invokeSnap( testPage, snapId, "confirm" ); - await metamask.snaps.acceptDialog(); + await metaMask.snaps.acceptDialog(); expect(await invokeAction).to.equal(true); }); it("should invoke provided snap method and REJECT the dialog", async function (this: TestContext) { - const invokeAction = metamask.snaps.invokeSnap( + const invokeAction = metaMask.snaps.invokeSnap( testPage, snapId, "confirm" ); - await metamask.snaps.rejectDialog(); + await metaMask.snaps.rejectDialog(); expect(await invokeAction).to.equal(false); }); it("should return all notifications", async function (this: TestContext) { - const permissionSnapId = await metamask.snaps.installSnap( + const permissionSnapId = await metaMask.snaps.installSnap( this.snapServers[Snaps.PERMISSIONS_SNAP], { hasPermissions: true, @@ -99,18 +99,18 @@ describe("snaps", function () { } ); - const emitter = await metamask.snaps.getNotificationEmitter(); + const emitter = await metaMask.snaps.getNotificationEmitter(); const notificationPromise = emitter.waitForNotification(); - await metamask.snaps.invokeSnap(testPage, snapId, "notify_inApp"); - await metamask.snaps.invokeSnap( + await metaMask.snaps.invokeSnap(testPage, snapId, "notify_inApp"); + await metaMask.snaps.invokeSnap( testPage, permissionSnapId, "notify_inApp" ); await notificationPromise; - const notifications = await metamask.snaps.getAllNotifications(); + const notifications = await metaMask.snaps.getAllNotifications(); expect(notifications[0].message).to.contain( "Hello from permissions snap in App notification" diff --git a/test/global.ts b/test/global.ts index 348386d8..2326557b 100644 --- a/test/global.ts +++ b/test/global.ts @@ -19,19 +19,20 @@ export const mochaHooks = { defaultBalance: 100, }, }); - const browser = await dappeteer.launch({ + + const { browser, metaMask, metaMaskPage } = await dappeteer.bootstrap({ + // optional, else it will use a default seed + seed: LOCAL_PREFUNDED_MNEMONIC, + password: PASSWORD, automation: (process.env.AUTOMATION as "puppeteer" | "playwright") ?? "puppeteer", browser: "chrome", metaMaskVersion: process.env.METAMASK_VERSION || dappeteer.RECOMMENDED_METAMASK_VERSION, }); + 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); @@ -40,7 +41,8 @@ export const mochaHooks = { provider: ethereum.provider, browser, testPageServer: server, - metamask, + metaMask, + metaMaskPage, flask: false, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment contract, @@ -57,7 +59,7 @@ export const mochaHooks = { async afterEach(this: TestContext): Promise { if (this.currentTest.state === "failed") { - await this.metamask.page.screenshot( + await this.metaMaskPage.screenshot( path.resolve( __dirname, `../${ diff --git a/test/global_flask.ts b/test/global_flask.ts index 1f78e09f..76c05d42 100644 --- a/test/global_flask.ts +++ b/test/global_flask.ts @@ -22,21 +22,21 @@ export const mochaHooks = { defaultBalance: 100, }, }); - const browser = await dappeteer.launch({ - browser: "chrome", + const { browser, metaMask, metaMaskPage } = await dappeteer.bootstrap({ + // optional, else it will use a default seed + seed: LOCAL_PREFUNDED_MNEMONIC, + password: PASSWORD, automation: (process.env.AUTOMATION as "puppeteer" | "playwright") ?? "puppeteer", + browser: "chrome", metaMaskVersion: process.env.METAMASK_VERSION || dappeteer.RECOMMENDED_METAMASK_VERSION, metaMaskFlask: true, }); + const server = await startTestServer(); const snapServers = await buildSnaps(); - 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); @@ -46,7 +46,8 @@ export const mochaHooks = { browser, testPageServer: server, snapServers: snapServers, - metamask, + metaMask, + metaMaskPage, flask: true, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment contract, @@ -63,7 +64,7 @@ export const mochaHooks = { async afterEach(this: TestContext): Promise { if (this.currentTest.state === "failed") { - await this.metamask.page.screenshot( + await this.metaMaskPage.screenshot( path.resolve( __dirname, `../${