Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat: allow signing typed data (#191)
Browse files Browse the repository at this point in the history
* add test to sign typed data

* add a check to see if the sign button is actually disabled

* clean up

* fix and clean

* remove flackyness

* lint

* fix playwright

* add hidden option for pupeteer types waitForSelector

* remove reload between tests
  • Loading branch information
Tbaut authored and mpetrunic committed Dec 15, 2022
1 parent 7082c98 commit 086ecbd
Show file tree
Hide file tree
Showing 19 changed files with 252 additions and 30 deletions.
47 changes: 43 additions & 4 deletions src/helpers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,23 @@ export const openNetworkDropdown = async (
await networkSwitcher.click();
await page.waitForSelector(".network-dropdown-list", {
visible: true,
timeout: 3000,
timeout: 4000,
});
}
};

export const openProfileDropdown = async (
page: DappeteerPage
export const profileDropdownClick = async (
page: DappeteerPage,
expectToClose = false
): Promise<void> => {
const accountSwitcher = await page.waitForSelector(".identicon", {
const accountSwitcher = await page.waitForSelector(".account-menu__icon", {
visible: true,
});
await page.waitForTimeout(500);
await accountSwitcher.click();
await page.waitForSelector(".account-menu__accounts", {
hidden: expectToClose,
});
};

export const openAccountDropdown = async (
Expand Down Expand Up @@ -112,3 +117,37 @@ export const typeOnInputField = async (
await input.type(text);
return true;
};

/**
*
* @param page
*/
export const clickOnLittleDownArrowIfNeeded = async (
page: DappeteerPage
): Promise<void> => {
// wait for the signature page and content to be loaded
await page.waitForSelector('[data-testid="signature-cancel-button"]', {
visible: true,
});

// 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(
'[data-testid="signature-sign-button"]',
(button: HTMLButtonElement) => {
return button.disabled;
}
);

if (isSignButtonDisabled) {
const littleArrowDown = await page.waitForSelector(
".signature-request-message__scroll-button",
{
visible: true,
}
);

await littleArrowDown.click();
}
};
7 changes: 7 additions & 0 deletions src/metamask/addNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ export const acceptAddNetwork =
async (shouldSwitch = false): Promise<void> => {
await page.bringToFront();
await page.reload();
await page.waitForSelector(".confirmation-footer", {
visible: true,
});
await clickOnButton(page, "Approve");
if (shouldSwitch) {
await clickOnButton(page, "Switch network");
await page.waitForSelector(".new-network-info__wrapper", {
visible: true,
});
await clickOnButton(page, "Got it");
} else {
await clickOnButton(page, "Cancel");
}
Expand Down
4 changes: 2 additions & 2 deletions src/metamask/importPk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
clickOnButton,
clickOnElement,
getErrorMessage,
openProfileDropdown,
profileDropdownClick,
typeOnInputField,
} from "../helpers";
import { DappeteerPage } from "../page";
Expand All @@ -11,7 +11,7 @@ export const importPk =
(page: DappeteerPage) =>
async (privateKey: string): Promise<void> => {
await page.bringToFront();
await openProfileDropdown(page);
await profileDropdownClick(page);

await clickOnElement(page, "Import account");
await typeOnInputField(page, "your private key", privateKey);
Expand Down
2 changes: 2 additions & 0 deletions src/metamask/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { deleteAccount, deleteNetwork, getTokenBalance } from "./helpers";
import { importPk } from "./importPk";
import { lock } from "./lock";
import { sign } from "./sign";
import { signTypedData } from "./signTypedData";
import { switchAccount } from "./switchAccount";
import { switchNetwork } from "./switchNetwork";
import { unlock } from "./unlock";
Expand Down Expand Up @@ -46,6 +47,7 @@ export const getMetaMask = (page: DappeteerPage): Promise<Dappeteer> => {
importPK: importPk(page),
lock: lock(page, setSignedIn, getSingedIn),
sign: sign(page, getSingedIn),
signTypedData: signTypedData(page, getSingedIn),
switchAccount: switchAccount(page),
switchNetwork: switchNetwork(page),
unlock: unlock(page, setSignedIn, getSingedIn),
Expand Down
4 changes: 2 additions & 2 deletions src/metamask/lock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clickOnButton, openProfileDropdown } from "../helpers";
import { clickOnButton, profileDropdownClick } from "../helpers";
import { DappeteerPage } from "../page";

import { GetSingedIn, SetSignedIn } from "./index";
Expand All @@ -11,7 +11,7 @@ export const lock =
}
await page.bringToFront();

await openProfileDropdown(page);
await profileDropdownClick(page);
await clickOnButton(page, "Lock");

await setSignedIn(false);
Expand Down
5 changes: 5 additions & 0 deletions src/metamask/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ export const sign =
await page.reload();

await clickOnButton(page, "Sign");

// wait for MM to be back in a stable state
await page.waitForSelector(".app-header", {
visible: true,
});
};
20 changes: 20 additions & 0 deletions src/metamask/signTypedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { clickOnButton, clickOnLittleDownArrowIfNeeded } from "../helpers";

import { DappeteerPage } from "../page";
import { GetSingedIn } from ".";

export const signTypedData =
(page: DappeteerPage, getSingedIn: GetSingedIn) =>
async (): Promise<void> => {
await page.bringToFront();
if (!(await getSingedIn())) {
throw new Error("You haven't signed in yet");
}
await page.reload();
await clickOnLittleDownArrowIfNeeded(page);
await clickOnButton(page, "Sign");
// wait for MM to be back in a stable state
await page.waitForSelector(".app-header", {
visible: true,
});
};
4 changes: 2 additions & 2 deletions src/metamask/switchAccount.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { clickOnElement, openProfileDropdown } from "../helpers";
import { clickOnElement, profileDropdownClick } from "../helpers";
import { DappeteerPage } from "../page";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const switchAccount =
(page: DappeteerPage) =>
async (accountNumber: number): Promise<void> => {
await page.bringToFront();
await openProfileDropdown(page);
await profileDropdownClick(page);

// TODO: use different approach? maybe change param to account name
await clickOnElement(page, `Account ${accountNumber}`);
Expand Down
2 changes: 1 addition & 1 deletion src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface DappeteerPage<P = unknown> {
): Promise<Response>;
waitForSelector(
selector: string,
opts?: Partial<{ visible: boolean; timeout: number }>
opts?: Partial<{ visible: boolean; timeout: number; hidden: boolean }>
): Promise<DappeteerElementHandle>;
waitForXPath(
xpath: string,
Expand Down
4 changes: 2 additions & 2 deletions src/playwright/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ export class DPlaywrightPage implements DappeteerPage<Page> {

async waitForSelector(
selector: string,
opts?: Partial<{ visible: boolean; timeout: number }>
opts?: Partial<{ visible: boolean; timeout: number; hidden: boolean }>
): Promise<DappeteerElementHandle<ElementHandle<HTMLElement>>> {
return new DPlaywrightElementHandle(
(await this.page.waitForSelector(selector, {
timeout: opts?.timeout,
state: opts?.visible ? "visible" : "attached",
state: opts?.visible ? "visible" : opts?.hidden ? "hidden" : "attached",
})) as ElementHandle<HTMLElement>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/puppeteer/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ export class DPuppeteerBrowser
this.emit("targetcreated", page)
);
}

wsEndpoint(): string {
return this.browser.wsEndpoint();
}

async close(): Promise<void> {
await this.browser.close();
}
Expand All @@ -26,6 +28,7 @@ export class DPuppeteerBrowser
return new DPupeteerPage(p, this) as DappeteerPage<Page>;
});
}

async newPage(): Promise<DappeteerPage<Page>> {
return new DPupeteerPage(
await this.browser.newPage(),
Expand Down
2 changes: 1 addition & 1 deletion src/puppeteer/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class DPupeteerPage implements DappeteerPage<Page> {

async waitForSelector(
selector: string,
opts?: Partial<{ visible: boolean; timeout: number }>
opts?: Partial<{ visible: boolean; timeout: number; hidden: boolean }>
): Promise<DappeteerElementHandle<ElementHandle<HTMLElement>>> {
return new DPuppeteerElementHandle(
(await this.page.waitForSelector(
Expand Down
8 changes: 5 additions & 3 deletions src/setup/setupMetaMask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const flaskMetaMaskSteps: Step<MetaMaskOptions>[] = [
closeWhatsNewModal,
];

const MM_HOME_REGEX = "chrome-extension://[a-z]+/home.html";

export async function setupMetaMask<Options = MetaMaskOptions>(
browser: DappeteerBrowser,
options?: Options,
Expand All @@ -62,19 +64,19 @@ async function getMetamaskPage(
): Promise<DappeteerPage> {
const pages = await browser.pages();
for (const page of pages) {
if (page.url().match("chrome-extension://[a-z]+/home.html")) {
if (page.url().match(MM_HOME_REGEX)) {
return page;
}
}
return new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
browser.on("targetcreated", async (target: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
if (target.url().match("chrome-extension://[a-z]+/home.html")) {
if (target.url().match(MM_HOME_REGEX)) {
try {
const pages = await browser.pages();
for (const page of pages) {
if (page.url().match("chrome-extension://[a-z]+/home.html")) {
if (page.url().match(MM_HOME_REGEX)) {
resolve(page);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/snap/getAllNotifications.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { clickOnElement, openProfileDropdown } from "../helpers";
import { clickOnElement, profileDropdownClick } from "../helpers";
import { DappeteerPage } from "../page";
import { NotificationList } from "./types";

export const getAllNotifications =
(page: DappeteerPage) => async (): Promise<NotificationList> => {
await page.bringToFront();
await openProfileDropdown(page);
await profileDropdownClick(page);
await clickOnElement(page, "Notifications");
await page.waitForSelector(".notifications__item__details__message");
const notificationList: NotificationList = await page.$$eval(
Expand Down
4 changes: 2 additions & 2 deletions src/snap/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
clickOnButton,
clickOnElement,
clickOnLogo,
openProfileDropdown,
profileDropdownClick,
} from "../helpers";
import { DappeteerPage } from "../page";
import { startSnapServer, toUrl } from "./install-utils";
Expand Down Expand Up @@ -89,7 +89,7 @@ export async function isSnapInstalled(
snapId: string
): Promise<boolean> {
await page.bringToFront();
await openProfileDropdown(page);
await profileDropdownClick(page);

await clickOnElement(page, "Settings");
await clickOnElement(page, "Snaps");
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type Dappeteer = {
switchNetwork: (network: string) => Promise<void>;
confirmTransaction: (options?: TransactionOptions) => Promise<void>;
sign: () => Promise<void>;
signTypedData: () => Promise<void>;
approve: () => Promise<void>;
helpers: {
getTokenBalance: (tokenSymbol: string) => Promise<number>;
Expand Down
31 changes: 22 additions & 9 deletions test/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, use } from "chai";
import chaiAsPromised from "chai-as-promised";

import * as dappeteer from "../src";
import { openProfileDropdown } from "../src/helpers";
import { profileDropdownClick } from "../src/helpers";
import { DappeteerPage } from "../src/page";

import { PASSWORD, TestContext } from "./constant";
Expand All @@ -28,10 +28,6 @@ describe("basic interactions", function () {
}
});

afterEach(async function () {
await metamask.page.reload();
});

after(async function () {
await testPage.close();
});
Expand All @@ -44,6 +40,24 @@ describe("basic interactions", function () {
await testPage.waitForSelector("#signed", { visible: false });
});

it("should be able to sign typed data", async () => {
await clickElement(testPage, ".sign-typedData-button");

await metamask.signTypedData();

await testPage.waitForSelector("#signed-typedData", { visible: false });
});

it("should be able to sign short typed data", async () => {
await clickElement(testPage, ".sign-short-typedData-button");

await metamask.signTypedData();

await testPage.waitForSelector("#signed-short-typedData", {
visible: false,
});
});

it("should switch network", async () => {
await metamask.switchNetwork("localhost");

Expand Down Expand Up @@ -89,18 +103,17 @@ describe("basic interactions", function () {

it("should add network and switch", async () => {
await clickElement(testPage, ".add-network-button");
await metamask.page.waitForTimeout(500);
await metamask.page.waitForTimeout(1000);
await metamask.acceptAddNetwork(true);
await testPage.waitForSelector("#addNetworkResultSuccess");
await metamask.switchNetwork("mainnet");
});

it("should import private key", async () => {
const countAccounts = async (): Promise<number> => {
await openProfileDropdown(metamask.page);
await profileDropdownClick(metamask.page, false);
const container = await metamask.page.$(".account-menu__accounts");
const count = (await container.$$(".account-menu__account")).length;
await openProfileDropdown(metamask.page);
await profileDropdownClick(metamask.page, true);
return count;
};

Expand Down
5 changes: 5 additions & 0 deletions test/dapp/index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Local MetaMask test</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="web3.min.js"></script>
</head>

<body>
<button class="connect-button">Connect</button>
<button class="sign-button">Sign</button>
<button class="sign-typedData-button">Sign Typed Data</button>
<button class="sign-short-typedData-button">Sign Short Typed Data</button>
<button class="increase-button">Increase</button>
<button class="increase-fees-button">Increase Fees</button>
<button class="transfer-button">Transfer</button>
Expand All @@ -19,4 +23,5 @@
<script src="data.js"></script>
<script src="main.js"></script>
</body>

</html>

0 comments on commit 086ecbd

Please sign in to comment.