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();