From d7c51f948d01230c04603e289375af03e872289d Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Thu, 8 Dec 2022 15:58:32 +0000 Subject: [PATCH] feat: Simplify `installSnap` and `initSnapEnv` apis (#206) * simplify api * remove stray comments * fix undefined opts * double question mark back :) * Update src/snap/utils.ts * 2s for my use case * test perf * elegant race with bad name --- README.md | 2 -- docs/API.md | 4 +-- src/snap/install.ts | 36 ++++++++++++++++++--------- src/snap/utils.ts | 53 ++++++++++++++++++++++++++++++++++++++++ src/types.ts | 16 +++++------- test/constant.ts | 2 +- test/flask/snaps.spec.ts | 27 ++++---------------- 7 files changed, 90 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index c42a96b6..914e6435 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,6 @@ async function main() { // 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 diff --git a/docs/API.md b/docs/API.md index f116b29a..79a0fc61 100644 --- a/docs/API.md +++ b/docs/API.md @@ -127,8 +127,6 @@ type MetaMaskOptions = { }; type InstallSnapOptions = { - hasPermissions: boolean; - hasKeyPermissions: boolean; customSteps?: InstallStep[]; version?: string; installationSnapUrl?: string; @@ -230,7 +228,7 @@ deletes custom network from metaMask # Snaps methods -## `metaMask.snaps.installSnap: (snapIdOrLocation: string, opts: { hasPermissions: boolean; hasKeyPermissions: boolean; customSteps?: InstallStep[]; version?: string;},installationSnapUrl?: string`) => Promise; +## `metaMask.snaps.installSnap: (snapIdOrLocation: string, opts?: { 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. diff --git a/src/snap/install.ts b/src/snap/install.ts index b66c6b95..2f90a26f 100644 --- a/src/snap/install.ts +++ b/src/snap/install.ts @@ -10,7 +10,7 @@ import { import { DappeteerPage } from "../page"; import { EXAMPLE_WEBSITE } from "../../test/constant"; import { startSnapServer, toUrl } from "./install-utils"; -import { flaskOnly } from "./utils"; +import { flaskOnly, isFirstElementAppearsFirst } from "./utils"; import { InstallSnapResult } from "./types"; declare let window: { ethereum: MetaMaskInpageProvider }; @@ -18,8 +18,6 @@ declare let window: { ethereum: MetaMaskInpageProvider }; export type InstallStep = (page: DappeteerPage) => Promise; export type InstallSnapOptions = { - hasPermissions: boolean; - hasKeyPermissions: boolean; customSteps?: InstallStep[]; version?: string; installationSnapUrl?: string; @@ -29,12 +27,12 @@ export const installSnap = (flaskPage: DappeteerPage) => async ( snapIdOrLocation: string, - opts: InstallSnapOptions + opts?: InstallSnapOptions ): Promise => { flaskOnly(flaskPage); //need to open page to access window.ethereum const installPage = await flaskPage.browser().newPage(); - await installPage.goto(opts.installationSnapUrl ?? EXAMPLE_WEBSITE); + await installPage.goto(opts?.installationSnapUrl ?? EXAMPLE_WEBSITE); let snapServer: http.Server | undefined; if (fs.existsSync(snapIdOrLocation)) { //snap dist location @@ -42,26 +40,40 @@ export const installSnap = snapIdOrLocation = `local:${toUrl(snapServer.address())}`; } const installAction = installPage.evaluate( - (opts: { snapId: string; version?: string }) => + ({ snapId, version }: { snapId: string; version?: string }) => window.ethereum.request({ method: "wallet_enable", params: [ { - [`wallet_snap_${opts.snapId}`]: { - version: opts.version ?? "latest", + [`wallet_snap_${snapId}`]: { + version: version ?? "latest", }, }, ], }), - { snapId: snapIdOrLocation, version: opts.version } + { snapId: snapIdOrLocation, version: opts?.version } ); await flaskPage.bringToFront(); await flaskPage.reload(); await clickOnButton(flaskPage, "Connect"); - if (opts.hasPermissions) { + + const isAskingForPermissions = await isFirstElementAppearsFirst({ + selectorOrXpath1: `//*[contains(text(), 'Approve & install')]`, + selectorOrXpath2: `//*[contains(text(), 'Install')]`, + page: flaskPage, + }); + + if (isAskingForPermissions) { await clickOnButton(flaskPage, "Approve & install"); - if (opts.hasKeyPermissions) { + + const isShowingWarning = await isFirstElementAppearsFirst({ + selectorOrXpath1: ".popover-wrap.snap-install-warning", + selectorOrXpath2: ".app-header__metafox-logo--icon", + page: flaskPage, + }); + + if (isShowingWarning) { await flaskPage.waitForSelector(".checkbox-label", { visible: true, }); @@ -74,7 +86,7 @@ export const installSnap = await clickOnButton(flaskPage, "Install"); } - for (const step of opts.customSteps ?? []) { + for (const step of opts?.customSteps ?? []) { await step(flaskPage); } diff --git a/src/snap/utils.ts b/src/snap/utils.ts index 1c3b14f4..dabd6abc 100644 --- a/src/snap/utils.ts +++ b/src/snap/utils.ts @@ -1,3 +1,4 @@ +import { DappeteerElementHandle } from "../element"; import { DappeteerPage } from "../page"; export function flaskOnly(page: DappeteerPage): void { @@ -17,3 +18,55 @@ export function isMetaMaskErrorObject(e: unknown): boolean { if (!("originalError" in e["data"])) return false; return true; } + +export function isElementVisible( + page: DappeteerPage, + selector: string, + timeout = 1000 +): Promise { + return new Promise((resolve) => { + page + .waitForSelector(selector, { visible: true, timeout }) + .then(() => { + resolve(true); + }) + .catch(() => { + resolve(false); + }); + }); +} + +function getWaitingPromise( + page: DappeteerPage, + selectorOrXpath: string, + timeout: number +): Promise> { + if (selectorOrXpath.startsWith("//")) { + return page.waitForXPath(selectorOrXpath, { timeout }); + } else { + return page.waitForSelector(selectorOrXpath, { timeout }); + } +} + +interface IsFirstElementAppearsFirstParams { + selectorOrXpath1: string; + selectorOrXpath2: string; + timeout?: number; + page: DappeteerPage; +} + +export async function isFirstElementAppearsFirst({ + selectorOrXpath1, + selectorOrXpath2, + page, + timeout = 2000, +}: IsFirstElementAppearsFirstParams): Promise { + const promise1 = getWaitingPromise(page, selectorOrXpath1, timeout).then( + () => true + ); + const promise2 = getWaitingPromise(page, selectorOrXpath2, timeout).then( + () => false + ); + + return await Promise.race([promise1, promise2]); +} diff --git a/src/types.ts b/src/types.ts index ac98ea52..5747f65e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -89,19 +89,15 @@ export type Dappeteer = { params?: Params ) => Promise>; /** - * 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 example.org + * 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 installationSnapUrl url of your dapp. Defaults to example.org */ installSnap: ( snapIdOrLocation: string, - opts: { - hasPermissions: boolean; - hasKeyPermissions: boolean; + opts?: { customSteps?: InstallStep[]; version?: string; }, diff --git a/test/constant.ts b/test/constant.ts index 3723bbcf..005d027d 100644 --- a/test/constant.ts +++ b/test/constant.ts @@ -17,7 +17,7 @@ export type InjectableContext = Readonly<{ flask: boolean; }>; -export const EXAMPLE_WEBSITE = "http://example.org/"; +export const EXAMPLE_WEBSITE = "http://example.org"; // TestContext will be used by all the test export type TestContext = Mocha.Context & InjectableContext; diff --git a/test/flask/snaps.spec.ts b/test/flask/snaps.spec.ts index cc831a89..8cb54ded 100644 --- a/test/flask/snaps.spec.ts +++ b/test/flask/snaps.spec.ts @@ -20,24 +20,15 @@ describe("snaps", function () { }); it("should install base snap from local server", async function (this: TestContext) { - await metaMask.snaps.installSnap(this.snapServers[Snaps.BASE_SNAP], { - hasPermissions: false, - hasKeyPermissions: false, - }); + await metaMask.snaps.installSnap(this.snapServers[Snaps.BASE_SNAP]); }); it("should install permissions snap local server", async function (this: TestContext) { - await metaMask.snaps.installSnap(this.snapServers[Snaps.PERMISSIONS_SNAP], { - hasPermissions: true, - hasKeyPermissions: false, - }); + await metaMask.snaps.installSnap(this.snapServers[Snaps.PERMISSIONS_SNAP]); }); it("should install keys snap from local server", async function (this: TestContext) { - await metaMask.snaps.installSnap(this.snapServers[Snaps.KEYS_SNAP], { - hasPermissions: true, - hasKeyPermissions: true, - }); + await metaMask.snaps.installSnap(this.snapServers[Snaps.KEYS_SNAP]); }); describe("should test snap methods", function () { @@ -56,11 +47,7 @@ describe("snaps", function () { this.skip(); } snapId = await metaMask.snaps.installSnap( - this.snapServers[Snaps.METHODS_SNAP], - { - hasPermissions: true, - hasKeyPermissions: false, - } + this.snapServers[Snaps.METHODS_SNAP] ); testPage = await metaMaskPage.browser().newPage(); await testPage.goto(EXAMPLE_WEBSITE); @@ -92,11 +79,7 @@ describe("snaps", function () { it("should return all notifications", async function (this: TestContext) { const permissionSnapId = await metaMask.snaps.installSnap( - this.snapServers[Snaps.PERMISSIONS_SNAP], - { - hasPermissions: true, - hasKeyPermissions: false, - } + this.snapServers[Snaps.PERMISSIONS_SNAP] ); const emitter = await metaMask.snaps.getNotificationEmitter();