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

feat: snap notifications 137 #187

Merged
merged 31 commits into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2d86a6d
chore: node engine requirements (#184) (#186)
mpetrunic Nov 15, 2022
3c4fd07
pair improvements
BeroBurny Oct 28, 2022
c628a86
fix lint
BeroBurny Oct 28, 2022
ada1274
feat: add notify Snap method
Lykhoyda Nov 1, 2022
56f3ac1
go back after getting all notifications
Lykhoyda Nov 7, 2022
c6f4821
remove timeout
mpetrunic Nov 9, 2022
0663d05
wip
Lykhoyda Nov 10, 2022
6f38415
Add notification observe method
Lykhoyda Nov 14, 2022
2506a33
update test
Lykhoyda Nov 14, 2022
a8bf66b
remove unused code
Lykhoyda Nov 16, 2022
abdc92c
wip notifiction observer
Lykhoyda Nov 17, 2022
840546b
wip observer based notifications
Lykhoyda Nov 17, 2022
711904a
wip observer based notifications
Lykhoyda Nov 17, 2022
4c9b429
remove unused dependency
Lykhoyda Nov 17, 2022
088c9ec
cleanup
Lykhoyda Nov 17, 2022
23a71fa
fixes after merge
Lykhoyda Nov 17, 2022
4e21814
revert method names
Lykhoyda Nov 17, 2022
a970759
clean waitForNotification method
Lykhoyda Nov 17, 2022
b6a5ed9
emitter solution - FP
Lykhoyda Nov 24, 2022
f5ac55e
emitter solution - ClassBased
Lykhoyda Nov 24, 2022
ea5ba86
Cleanup class based emitter
Lykhoyda Nov 25, 2022
14f4f31
fix lint
Lykhoyda Nov 25, 2022
1c08ce9
Merge branch 'unstable' into lykhoyda/snap-notifications_137
Lykhoyda Nov 25, 2022
370048c
fixes after merge
Lykhoyda Nov 25, 2022
4edd6bb
remove p-event library
Lykhoyda Nov 25, 2022
0f4bd83
remove eslint comment
Lykhoyda Nov 25, 2022
0d8cb49
update test configuration
Lykhoyda Nov 25, 2022
9cbd993
return NotificationList from waitForNotification
Lykhoyda Nov 25, 2022
fb71f83
add getNotificationEmitter method
Lykhoyda Nov 28, 2022
2a532ea
remove back button
Lykhoyda Nov 28, 2022
2f7c70b
Merge branch 'unstable', remote-tracking branch 'origin' into lykhoyd…
Lykhoyda Nov 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"dependencies": {
"@metamask/providers": "^9.1.0",
"node-stream-zip": "^1.13.0",
"serve-handler": "5.0.8"
"p-event": "4.2.0",
"serve-handler": "5.0.8",
"strict-event-emitter": "^0.2.8"
},
"devDependencies": {
"@chainsafe/eslint-config": "^1.0.0",
Expand All @@ -70,8 +72,8 @@
"web3": "1.3.4"
},
"peerDependencies": {
"puppeteer": ">13",
"playwright": ">=1"
"playwright": ">=1",
"puppeteer": ">13"
},
"peerDependenciesMeta": {
"soy-puppeteer": {
Expand All @@ -81,4 +83,4 @@
"optional": true
}
}
}
}
6 changes: 5 additions & 1 deletion src/metamask/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { DappeteerPage } from "../page";
import { acceptDialog } from "../snap/acceptDialog";
import { rejectDialog } from "../snap/rejectDialog";
import { getAllNotifications, installSnap, invokeSnap } from "../snap";
import { invokeNotification } from "../snap/invokeNotification";
import { waitForNotification } from "../snap/waitForNotification";
import { addNetwork } from "./addNetwork";
import { addToken } from "./addToken";
import { approve } from "./approve";
Expand Down Expand Up @@ -55,10 +57,12 @@ export const getMetaMask = (page: DappeteerPage): Promise<Dappeteer> => {
deleteNetwork: deleteNetwork(page),
},
snaps: {
invokeSnap,
waitForNotification,
invokeNotification: invokeNotification(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,52 +1,80 @@
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<{ visible: boolean; timeout: number }>
): Promise<DappeteerElementHandle>;

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,
puppeteerFunction: 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 @@ -104,14 +104,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 @@ -119,9 +119,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 @@ -131,4 +133,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);
}
}
89 changes: 89 additions & 0 deletions src/snap/NotificationsEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { StrictEventEmitter } from "strict-event-emitter";
// eslint-disable-next-line @typescript-eslint/no-require-imports
import pEvent = require("p-event");
import { DappeteerPage } from "../page";
import * as dappeteer from "../../src";
import { clickOnElement, openProfileDropdown } from "../helpers";
import { NotificationItem, NotificationList } from "./types";

interface EventsMap {
newNotification: (notification: NotificationItem) => void;
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
}

class NotificationsEmitter extends StrictEventEmitter<EventsMap> {
private notifications: NotificationList = [];
private notificationsTab: DappeteerPage;
private readonly emitter: StrictEventEmitter<EventsMap>;
private readonly page: DappeteerPage;

constructor(protected metamask: dappeteer.Dappeteer) {
super();
this.page = metamask.page;
this.emitter = new StrictEventEmitter<EventsMap>();
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
this.configureEmitterListener();
}

private configureEmitterListener(): void {
this.emitter.on("newNotification", (notification: NotificationItem) => {
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
this.notifications.push(notification);
});
}

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

private async openNotificationPage(): Promise<void> {
await this.page.bringToFront();
await openProfileDropdown(this.page);
await clickOnElement(this.page, "Notifications");

const newPage = await this.page.browser().newPage();
await newPage.goto(this.page.url());

await newPage.waitForSelector(".notifications__container");
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
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> {
await this.notificationsTab.close();
}

public waitForNotification(): pEvent.CancelablePromise<NotificationItem> {
return pEvent<any, NotificationItem>(this.emitter, "newNotification");
}

public getAllNotifications(): NotificationList {
return this.notifications;
}
}

export default NotificationsEmitter;
48 changes: 48 additions & 0 deletions src/snap/invokeNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { DappeteerPage, Serializable } from "../page";
import { clickOnElement, openProfileDropdown } from "../helpers";
import { invokeSnap } from "./invokeSnap";
import { NotificationItem } from "./types";

async function waitForNotification(
page: DappeteerPage
): Promise<NotificationItem> {
await page.waitForSelector(".notifications__container");
return await page.evaluate(
() =>
new Promise<NotificationItem>((resolve) => {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
const element = Array.from(mutation.addedNodes)[0] as HTMLElement;
observer.takeRecords();
observer.disconnect();
resolve({ message: element.innerText });
}
}
});
observer.observe(document.querySelector(".notifications__container"), {
childList: true,
});
})
);
}

export const invokeNotification =
(page: DappeteerPage) =>
async <R = unknown, P extends Serializable = Serializable>(
testPage: DappeteerPage,
snapId: string,
method: string,
params?: P
): Promise<NotificationItem> => {
await page.bringToFront();
await openProfileDropdown(page);
await clickOnElement(page, "Notifications");

const newPage = await page.browser().newPage();
await newPage.goto(page.url());

await invokeSnap<R, P>(testPage, snapId, method, params);

return await waitForNotification(page);
};
46 changes: 46 additions & 0 deletions src/snap/notificationEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { EventEmitter } from "events";
import { DappeteerPage } from "../page";
import { clickOnElement, openProfileDropdown } from "../helpers";
import { NotificationItem, NotificationList } from "./types";

export const notificationEmitter = async (
page: DappeteerPage
): Promise<{ emitter: EventEmitter; notifications: NotificationList }> => {
const notifications: NotificationList = [];
const emitter: EventEmitter = new EventEmitter();

emitter.on("newNotification", (notification: NotificationItem) => {
notifications.push(notification);
});

await page.bringToFront();
await openProfileDropdown(page);
await clickOnElement(page, "Notifications");

const newPage = await page.browser().newPage();
await newPage.goto(page.url());

await newPage.waitForSelector(".notifications__container");
await newPage.exposeFunction(
"emitNotification",
(notification: NotificationItem) => {
emitter.emit("newNotification", notification);
}
);
await newPage.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,
});
});

return { emitter, notifications };
};
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[];
Loading