From fa183ef58d6183b56b91ab8a41fbe2ff60a73937 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Tue, 6 Dec 2022 11:44:59 +0100 Subject: [PATCH 1/8] simplify api --- src/snap/install.ts | 33 +++++++++++++++++++++++---------- src/snap/utils.ts | 19 +++++++++++++++++++ src/types.ts | 6 +----- test/flask/snaps.spec.ts | 28 +++++----------------------- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/snap/install.ts b/src/snap/install.ts index 157000a6..754d3e40 100644 --- a/src/snap/install.ts +++ b/src/snap/install.ts @@ -9,7 +9,7 @@ import { } from "../helpers"; import { DappeteerPage } from "../page"; import { startSnapServer, toUrl } from "./install-utils"; -import { flaskOnly } from "./utils"; +import { flaskOnly, isElementVisible } from "./utils"; import { InstallSnapResult } from "./types"; declare let window: { ethereum: MetaMaskInpageProvider }; @@ -17,8 +17,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; @@ -28,12 +26,12 @@ export const installSnap = (page: DappeteerPage) => async ( snapIdOrLocation: string, - opts: InstallSnapOptions + opts?: InstallSnapOptions ): Promise => { flaskOnly(page); //need to open page to access window.ethereum const installPage = await page.browser().newPage(); - await installPage.goto(opts.installationSnapUrl ?? "https://google.com"); + await installPage.goto(opts?.installationSnapUrl ?? "https://google.com"); let snapServer: http.Server | undefined; if (fs.existsSync(snapIdOrLocation)) { //snap dist location @@ -41,13 +39,13 @@ 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", }, }, ], @@ -58,9 +56,24 @@ export const installSnap = await page.bringToFront(); await page.reload(); await clickOnButton(page, "Connect"); - if (opts.hasPermissions) { + + // if the snap is requesting for permissions + const isAskingForPermissions = await isElementVisible( + page, + ".permissions-connect-permission-list" + ); + + if (isAskingForPermissions) { await clickOnButton(page, "Approve & install"); - if (opts.hasKeyPermissions) { + + // if the snap requires key permissions + // a dedicated warning will apprear + const isShowingWarning = await isElementVisible( + page, + ".popover-wrap.snap-install-warning" + ); + + if (isShowingWarning) { await page.waitForSelector(".checkbox-label", { visible: true, }); diff --git a/src/snap/utils.ts b/src/snap/utils.ts index 0362615a..ed0fa3e0 100644 --- a/src/snap/utils.ts +++ b/src/snap/utils.ts @@ -17,3 +17,22 @@ export function isMetamaskErrorObject(e: unknown): boolean { if (!("originalError" in e["data"])) return false; return true; } + +export function isElementVisible( + page: DappeteerPage, + selector: string, + timeout = 2000 +): Promise { + return new Promise((resolve) => { + page + .waitForSelector(selector, { visible: true, timeout }) + .then(() => { + resolve(true); + console.log("yes", selector); + }) + .catch(() => { + resolve(false); + console.log("nope", selector); + }); + }); +} diff --git a/src/types.ts b/src/types.ts index 46dd3193..8b7d3eb6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,15 +93,11 @@ export type Dappeteer = { * @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 */ installSnap: ( snapIdOrLocation: string, - opts: { - hasPermissions: boolean; - hasKeyPermissions: boolean; + opts?: { customSteps?: InstallStep[]; version?: string; }, diff --git a/test/flask/snaps.spec.ts b/test/flask/snaps.spec.ts index 8cef9789..b1447521 100644 --- a/test/flask/snaps.spec.ts +++ b/test/flask/snaps.spec.ts @@ -19,24 +19,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 () { @@ -53,14 +44,9 @@ describe("snaps", function () { before(async function (this: TestContext) { if (!this.browser.isMetaMaskFlask()) { this.skip(); - return; } snapId = await metamask.snaps.installSnap( - this.snapServers[Snaps.METHODS_SNAP], - { - hasPermissions: true, - hasKeyPermissions: false, - } + this.snapServers[Snaps.METHODS_SNAP] ); testPage = await metamask.page.browser().newPage(); await testPage.goto("https://google.com"); @@ -92,11 +78,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(); From 49a72e17cb4a1c0a45ead376ba0d311f1acce277 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Tue, 6 Dec 2022 12:06:29 +0100 Subject: [PATCH 2/8] remove stray comments --- src/snap/utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/snap/utils.ts b/src/snap/utils.ts index d69f50e0..e929f1b4 100644 --- a/src/snap/utils.ts +++ b/src/snap/utils.ts @@ -28,11 +28,9 @@ export function isElementVisible( .waitForSelector(selector, { visible: true, timeout }) .then(() => { resolve(true); - console.log("yes", selector); }) .catch(() => { resolve(false); - console.log("nope", selector); }); }); } From 3e18e9e82c80c9cca51bea45418ae262e54f23eb Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Tue, 6 Dec 2022 12:59:06 +0100 Subject: [PATCH 3/8] fix undefined opts --- src/snap/install.ts | 8 ++++---- test/constant.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/snap/install.ts b/src/snap/install.ts index d21f8261..a4ea821d 100644 --- a/src/snap/install.ts +++ b/src/snap/install.ts @@ -32,7 +32,7 @@ export const installSnap = 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 @@ -46,12 +46,12 @@ export const installSnap = params: [ { [`wallet_snap_${snapId}`]: { - version: version ?? "latest", + version: version || "latest", }, }, ], }), - { snapId: snapIdOrLocation, version: opts.version } + { snapId: snapIdOrLocation, version: opts?.version } ); await flaskPage.bringToFront(); @@ -87,7 +87,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/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; From 138876d4e0b9ff77074ccc610305f271a5c0b924 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Tue, 6 Dec 2022 13:07:54 +0100 Subject: [PATCH 4/8] double question mark back :) --- src/snap/install.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/snap/install.ts b/src/snap/install.ts index a4ea821d..5a2b8ad9 100644 --- a/src/snap/install.ts +++ b/src/snap/install.ts @@ -32,7 +32,7 @@ export const installSnap = 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 @@ -46,7 +46,7 @@ export const installSnap = params: [ { [`wallet_snap_${snapId}`]: { - version: version || "latest", + version: version ?? "latest", }, }, ], @@ -87,7 +87,7 @@ export const installSnap = await clickOnButton(flaskPage, "Install"); } - for (const step of opts?.customSteps || []) { + for (const step of opts?.customSteps ?? []) { await step(flaskPage); } From 6981183fa5081a55dc7556324d5c43346ce12251 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Tue, 6 Dec 2022 16:03:23 +0100 Subject: [PATCH 5/8] Update src/snap/utils.ts --- src/snap/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snap/utils.ts b/src/snap/utils.ts index e929f1b4..c97102e1 100644 --- a/src/snap/utils.ts +++ b/src/snap/utils.ts @@ -21,7 +21,7 @@ export function isMetaMaskErrorObject(e: unknown): boolean { export function isElementVisible( page: DappeteerPage, selector: string, - timeout = 2000 + timeout = 1000 ): Promise { return new Promise((resolve) => { page From c879ebf1305349e42d25c37b2466f36b9a1d2f38 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Tue, 6 Dec 2022 16:55:39 +0100 Subject: [PATCH 6/8] 2s for my use case --- src/snap/install.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/snap/install.ts b/src/snap/install.ts index 5a2b8ad9..9365fb33 100644 --- a/src/snap/install.ts +++ b/src/snap/install.ts @@ -61,7 +61,8 @@ export const installSnap = // if the snap is requesting for permissions const isAskingForPermissions = await isElementVisible( flaskPage, - ".permissions-connect-permission-list" + ".permissions-connect-permission-list", + 2000 ); if (isAskingForPermissions) { @@ -71,7 +72,8 @@ export const installSnap = // a dedicated warning will apprear const isShowingWarning = await isElementVisible( flaskPage, - ".popover-wrap.snap-install-warning" + ".popover-wrap.snap-install-warning", + 2000 ); if (isShowingWarning) { From be5cb3e2b0df96b7dcfe09046fec9aa8579a53e5 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Wed, 7 Dec 2022 10:28:37 +0100 Subject: [PATCH 7/8] test perf --- src/snap/install.ts | 10 ++++++++-- test/flask/snaps.spec.ts | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/snap/install.ts b/src/snap/install.ts index 9365fb33..bc5d2400 100644 --- a/src/snap/install.ts +++ b/src/snap/install.ts @@ -58,23 +58,29 @@ export const installSnap = await flaskPage.reload(); await clickOnButton(flaskPage, "Connect"); + const t2 = performance.now(); // if the snap is requesting for permissions const isAskingForPermissions = await isElementVisible( flaskPage, ".permissions-connect-permission-list", - 2000 + 1500 ); + const t3 = performance.now(); + console.log("isAskingForPermissions", t3 - t2); if (isAskingForPermissions) { await clickOnButton(flaskPage, "Approve & install"); // if the snap requires key permissions // a dedicated warning will apprear + const t0 = performance.now(); const isShowingWarning = await isElementVisible( flaskPage, ".popover-wrap.snap-install-warning", - 2000 + 500 ); + const t1 = performance.now(); + console.log("isShowingWarning", t1 - t0); if (isShowingWarning) { await flaskPage.waitForSelector(".checkbox-label", { diff --git a/test/flask/snaps.spec.ts b/test/flask/snaps.spec.ts index 8cb54ded..36905c21 100644 --- a/test/flask/snaps.spec.ts +++ b/test/flask/snaps.spec.ts @@ -19,15 +19,15 @@ describe("snaps", function () { } }); - it("should install base snap from local server", async function (this: TestContext) { + it.only("should install base snap from local server", async function (this: TestContext) { await metaMask.snaps.installSnap(this.snapServers[Snaps.BASE_SNAP]); }); - it("should install permissions snap local server", async function (this: TestContext) { + it.only("should install permissions snap local server", async function (this: TestContext) { await metaMask.snaps.installSnap(this.snapServers[Snaps.PERMISSIONS_SNAP]); }); - it("should install keys snap from local server", async function (this: TestContext) { + it.only("should install keys snap from local server", async function (this: TestContext) { await metaMask.snaps.installSnap(this.snapServers[Snaps.KEYS_SNAP]); }); From 984b40e141589fdbf7bd8009e8da950c0f4180ed Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Wed, 7 Dec 2022 11:46:18 +0100 Subject: [PATCH 8/8] elegant race with bad name --- src/snap/install.ts | 31 +++++++++++-------------------- src/snap/utils.ts | 36 ++++++++++++++++++++++++++++++++++++ test/flask/snaps.spec.ts | 6 +++--- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/snap/install.ts b/src/snap/install.ts index bc5d2400..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, isElementVisible } from "./utils"; +import { flaskOnly, isFirstElementAppearsFirst } from "./utils"; import { InstallSnapResult } from "./types"; declare let window: { ethereum: MetaMaskInpageProvider }; @@ -58,29 +58,20 @@ export const installSnap = await flaskPage.reload(); await clickOnButton(flaskPage, "Connect"); - const t2 = performance.now(); - // if the snap is requesting for permissions - const isAskingForPermissions = await isElementVisible( - flaskPage, - ".permissions-connect-permission-list", - 1500 - ); - const t3 = performance.now(); - console.log("isAskingForPermissions", t3 - t2); + const isAskingForPermissions = await isFirstElementAppearsFirst({ + selectorOrXpath1: `//*[contains(text(), 'Approve & install')]`, + selectorOrXpath2: `//*[contains(text(), 'Install')]`, + page: flaskPage, + }); if (isAskingForPermissions) { await clickOnButton(flaskPage, "Approve & install"); - // if the snap requires key permissions - // a dedicated warning will apprear - const t0 = performance.now(); - const isShowingWarning = await isElementVisible( - flaskPage, - ".popover-wrap.snap-install-warning", - 500 - ); - const t1 = performance.now(); - console.log("isShowingWarning", t1 - t0); + 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", { diff --git a/src/snap/utils.ts b/src/snap/utils.ts index c97102e1..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 { @@ -34,3 +35,38 @@ export function isElementVisible( }); }); } + +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/test/flask/snaps.spec.ts b/test/flask/snaps.spec.ts index 36905c21..8cb54ded 100644 --- a/test/flask/snaps.spec.ts +++ b/test/flask/snaps.spec.ts @@ -19,15 +19,15 @@ describe("snaps", function () { } }); - it.only("should install base snap from local server", async function (this: TestContext) { + it("should install base snap from local server", async function (this: TestContext) { await metaMask.snaps.installSnap(this.snapServers[Snaps.BASE_SNAP]); }); - it.only("should install permissions snap local server", async function (this: TestContext) { + it("should install permissions snap local server", async function (this: TestContext) { await metaMask.snaps.installSnap(this.snapServers[Snaps.PERMISSIONS_SNAP]); }); - it.only("should install keys snap from local server", async function (this: TestContext) { + it("should install keys snap from local server", async function (this: TestContext) { await metaMask.snaps.installSnap(this.snapServers[Snaps.KEYS_SNAP]); });