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

Commit

Permalink
feat: snap notifications 137 (#187)
Browse files Browse the repository at this point in the history
* chore: node engine requirements (#184) (#186)

set engines compatibility for 16 and above

Co-authored-by: Bernard Stojanovi膰 <bero4net@gmail.com>

* pair improvements

* fix lint

* feat: add notify Snap method

* go back after getting all notifications

* remove timeout

* wip

* Add notification observe method

* update test

* remove unused code

* wip notifiction observer

* wip observer based notifications

* wip observer based notifications

* remove unused dependency

* cleanup

* fixes after merge

* revert method names

* clean waitForNotification method

* emitter solution - FP

* emitter solution - ClassBased

* Cleanup class based emitter

* fix lint

* fixes after merge

* remove p-event library

* remove eslint comment

* update test configuration

* return NotificationList from waitForNotification

* add getNotificationEmitter method

* remove back button

Co-authored-by: Marin Petruni膰 <mpetrunic@users.noreply.github.com>
Co-authored-by: Bernard Stojanovi膰 <bero4net@gmail.com>
Co-authored-by: Marin Petrunic <marin.petrunic@gmail.com>
  • Loading branch information
4 people committed Dec 15, 2022
1 parent a633120 commit 089e951
Show file tree
Hide file tree
Showing 18 changed files with 215 additions and 32 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"dependencies": {
"@metamask/providers": "^9.1.0",
"node-stream-zip": "^1.13.0",
"serve-handler": "5.0.8"
"serve-handler": "5.0.8",
"strict-event-emitter": "^0.2.8"
},
"devDependencies": {
"@chainsafe/eslint-config": "^1.0.0",
Expand All @@ -70,8 +71,8 @@
"web3": "1.3.4"
},
"peerDependencies": {
"puppeteer": ">13",
"playwright": ">=1"
"playwright": ">=1",
"puppeteer": ">13"
},
"peerDependenciesMeta": {
"soy-puppeteer": {
Expand All @@ -81,4 +82,4 @@
"optional": true
}
}
}
}
5 changes: 3 additions & 2 deletions src/metamask/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Dappeteer } from "..";
import { DappeteerBrowser } from "../browser";
import { DappeteerPage } from "../page";

import { acceptDialog } from "../snap/acceptDialog";
import { rejectDialog } from "../snap/rejectDialog";
import { getAllNotifications, installSnap, invokeSnap } from "../snap";
import { getNotificationEmitter } from "../snap/getNotificationEmitter";
import { acceptAddNetwork, rejectAddNetwork } from "./addNetwork";
import { approve } from "./approve";
import { confirmTransaction } from "./confirmTransaction";
Expand Down Expand Up @@ -59,10 +59,11 @@ export const getMetaMask = (page: DappeteerPage): Promise<Dappeteer> => {
deleteNetwork: deleteNetwork(page),
},
snaps: {
invokeSnap,
getNotificationEmitter: getNotificationEmitter(page),
getAllNotifications: getAllNotifications(page),
acceptDialog: acceptDialog(page),
rejectDialog: rejectDialog(page),
invokeSnap,
installSnap: installSnap(page),
},
page,
Expand Down
30 changes: 29 additions & 1 deletion src/page.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,52 @@
import { DappeteerBrowser } from "./browser";
import { DappeteerElementHandle } from "./element";
import { DappeteerBrowser } from "./browser";

export interface DappeteerPage<P = unknown> {
$(selector: string): Promise<DappeteerElementHandle | null>;

$eval<T>(
selector: string,
evalFn: (e: HTMLElement) => Promise<T> | T
): Promise<T>;

$$eval<T>(
selector: string,
evalFn: (e: HTMLElement[]) => Promise<T[]> | T[]
): Promise<T[]>;

$$(selector: string): Promise<DappeteerElementHandle[]>;

getSource(): P;

url(): string;

browser(): DappeteerBrowser;

bringToFront(): Promise<void>;

goto(
url: string,
options?: {
timeout?: number;
waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
}
): Promise<void>;

title(): Promise<string>;

close(options?: { runBeforeUnload?: boolean }): Promise<void>;

reload(): Promise<void>;

setViewport(opts: { height: number; width: number }): Promise<void>;

waitForResponse(
urlOrPredicate: string | ((res: Response) => boolean | Promise<boolean>),
options?: {
timeout?: number;
}
): Promise<Response>;

waitForSelector(
selector: string,
opts?: Partial<{
Expand All @@ -47,16 +61,30 @@ export interface DappeteerPage<P = unknown> {
selector: string,
opts?: Partial<{ timeout: number }>
): Promise<void>;

waitForXPath(
xpath: string,
opts?: Partial<{ visible: boolean; timeout: number }>
): Promise<DappeteerElementHandle>;

waitForTimeout(timeout: number): Promise<void>;

evaluate<Params extends Serializable, Result>(
evaluateFn: (params: Unboxed<Params>) => Result,
params?: Params
): Promise<Result>;

screenshot(path: string): Promise<void>;

waitForFunction<Params extends Serializable>(
pageFunction: Function | string,
params?: Params
): Promise<void>;

exposeFunction(
name: string,
callback: Function | { default: Function }
): Promise<void>;
}

export type Unboxed<Arg> = Arg extends DappeteerElementHandle<any, infer T>
Expand Down
16 changes: 16 additions & 0 deletions src/playwright/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,30 @@ export class DPlaywrightPage implements DappeteerPage<Page> {
): Promise<DappeteerElementHandle<ElementHandle>> {
return this.waitForSelector(xpath, opts);
}

waitForTimeout(timeout: number): Promise<void> {
return this.page.waitForTimeout(timeout);
}

evaluate<Params, Result>(
evaluateFn: (params?: Unboxed<Params>) => Result | Promise<Result>,
params?: Params
): Promise<Result> {
//@ts-expect-error
return this.page.evaluate(evaluateFn, params);
}

async waitForFunction<Args>(
pageFunction: (params?: Unboxed<Args>) => void | string,
params?: Args
): Promise<void> {
await this.page.waitForFunction(pageFunction, {}, params);
}

exposeFunction(
name: string,
callback: Function | { default: Function }
): Promise<void> {
return this.page.exposeFunction(name, <Function>callback);
}
}
13 changes: 13 additions & 0 deletions src/puppeteer/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,11 @@ export class DPupeteerPage implements DappeteerPage<Page> {
await this.page.waitForXPath(xpath, opts)
);
}

waitForTimeout(timeout: number): Promise<void> {
return this.page.waitForTimeout(timeout);
}

evaluate<Params extends Serializable, Result>(
evaluateFn: (params?: Unboxed<Params>) => Result | Promise<Result>,
params?: Params
Expand All @@ -141,4 +143,15 @@ export class DPupeteerPage implements DappeteerPage<Page> {
params
) as Promise<Result>;
}

async waitForFunction<Params extends Serializable>(
pageFunction: (params?: Unboxed<Params>) => void | string,
params?: Params
): Promise<void> {
await this.page.waitForFunction(pageFunction, {}, params);
}

exposeFunction(name: string, callback: Function): Promise<void> {
return this.page.exposeFunction(name, callback);
}
}
78 changes: 78 additions & 0 deletions src/snap/NotificationsEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { StrictEventEmitter } from "strict-event-emitter";
import { DappeteerPage } from "../page";
import { clickOnElement, profileDropdownClick } from "../helpers";
import { NotificationItem, NotificationList } from "./types";

interface EventsMap {
notification: (notification: NotificationItem) => void;
}

class NotificationsEmitter extends StrictEventEmitter<EventsMap> {
private notificationsTab: DappeteerPage;

constructor(
private page: DappeteerPage,
private notificationTimeout: number = 30000
) {
super();
}

private async exposeEmitNotificationToWindow(): Promise<void> {
await this.notificationsTab.exposeFunction(
"emitNotification",
(notification: NotificationItem) => {
this.emit("notification", notification);
}
);
}

private async openNotificationPage(): Promise<void> {
const newPage = await this.page.browser().newPage();
await newPage.goto(this.page.url());

await profileDropdownClick(newPage);
await clickOnElement(newPage, "Notifications");

await newPage.waitForSelector(".notifications__container", {
timeout: this.notificationTimeout,
});
this.notificationsTab = newPage;
}

private async observeNotificationsMutation(): Promise<void> {
await this.notificationsTab.evaluate(() => {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
const element = mutation.addedNodes[0] as HTMLElement;
window.emitNotification({ message: element.innerText });
observer.disconnect();
}
}
});
observer.observe(document.querySelector(".notifications__container"), {
childList: true,
});
});
}

public async setup(): Promise<void> {
await this.openNotificationPage();
await this.exposeEmitNotificationToWindow();
await this.observeNotificationsMutation();
}

public async cleanup(): Promise<void> {
this.removeAllListeners("notification");
await this.notificationsTab.close();
}

public async waitForNotification(): Promise<NotificationList> {
return (await NotificationsEmitter.once(
this,
"notification"
)) as NotificationList;
}
}

export default NotificationsEmitter;
8 changes: 1 addition & 7 deletions src/snap/getAllNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,9 @@ export const getAllNotifications =
await profileDropdownClick(page);
await clickOnElement(page, "Notifications");
await page.waitForSelector(".notifications__item__details__message");
const notificationList: NotificationList = await page.$$eval(
return await page.$$eval(
".notifications__item__details__message",
(elements) =>
elements.map((element) => ({ message: element.textContent }))
);
const backButton = await page.waitForSelector(
".notifications__header__title-container__back-button"
);

await backButton.click();
return notificationList;
};
9 changes: 9 additions & 0 deletions src/snap/getNotificationEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { DappeteerPage } from "../page";
import NotificationsEmitter from "./NotificationsEmitter";

export const getNotificationEmitter =
(page: DappeteerPage) => async (): Promise<NotificationsEmitter> => {
const emitter = new NotificationsEmitter(page);
await emitter.setup();
return emitter;
};
3 changes: 2 additions & 1 deletion src/snap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export interface InstallSnapResult {
};
}

export type NotificationList = { message: string }[];
export type NotificationItem = { message: string };
export type NotificationList = NotificationItem[];
13 changes: 9 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { launch as puppeteerLaunch } from "puppeteer";
import { DappeteerPage, Serializable } from "./page";
import { Path } from "./setup/utils/metaMaskDownloader";
import { InstallStep } from "./snap/install";
import { NotificationList } from "./snap/types";
import { NotificationItem, NotificationList } from "./snap/types";
import NotificationsEmitter from "./snap/NotificationsEmitter";
import { RECOMMENDED_METAMASK_VERSION } from "./index";

export type DappeteerLaunchOptions = {
Expand All @@ -27,6 +28,7 @@ export type DappeteerLaunchOptions = {
declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
emitNotification: (notification: NotificationItem) => void;
}
}

Expand Down Expand Up @@ -63,6 +65,10 @@ export type Dappeteer = {
};
page: DappeteerPage;
snaps: {
/**
* Returns emitter to listen for notifications appearance in notification page
*/
getNotificationEmitter: () => Promise<NotificationsEmitter>;
/**
* Returns all notifications in Metamask notifications page
*/
Expand All @@ -73,7 +79,7 @@ export type Dappeteer = {
* @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 wan't to invoke
* @param method snap method you want to invoke
* @param params required parameters of snap method
*/
invokeSnap: <Result = unknown, Params extends Serializable = Serializable>(
Expand All @@ -82,12 +88,11 @@ export type Dappeteer = {
method: string,
params?: Params
) => Promise<Partial<Result>>;

/**
* 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 wan't to invoke
* @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
Expand Down
4 changes: 1 addition & 3 deletions test/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { expect, use } from "chai";
import chaiAsPromised from "chai-as-promised";

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

import { DappeteerPage } from "../src";
import { PASSWORD, TestContext } from "./constant";
import { clickElement } from "./utils/utils";

Expand Down
2 changes: 1 addition & 1 deletion test/flask/methods-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "An example Snap written in TypeScript.",
"proposedName": "Methods Snap\n",
"source": {
"shasum": "xxu7kMfZ4zKgviSZ5K4gZKSDDGEYi2VqOK5bKAqoybo=",
"shasum": "65ylJ5v6kEUqE7jJEezI49brIzKhs3dSY8Y2uRxIIEY=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion test/flask/methods-snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
params: [
{
type: "inApp",
message: `Hello, in App notification`,
message: `Hello from methods snap in App notification`,
},
],
});
Expand Down

0 comments on commit 089e951

Please sign in to comment.