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 17 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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@
"web3": "1.3.4"
},
"peerDependencies": {
"puppeteer": ">13",
"playwright": ">=1"
"playwright": ">=1",
"puppeteer": ">13"
},
"peerDependenciesMeta": {
"soy-puppeteer": {
Expand All @@ -81,4 +81,4 @@
"optional": true
}
}
}
}
2 changes: 2 additions & 0 deletions src/metamask/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 { addNetwork } from "./addNetwork";
import { addToken } from "./addToken";
import { approve } from "./approve";
Expand Down Expand Up @@ -55,6 +56,7 @@ export const getMetaMask = (page: DappeteerPage): Promise<Dappeteer> => {
deleteNetwork: deleteNetwork(page),
},
snaps: {
invokeNotification: invokeNotification(page),
getAllNotifications: getAllNotifications(page),
acceptDialog: acceptDialog(page),
rejectDialog: rejectDialog(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,
puppeteerFunction: Function | { default: Function }
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
): Promise<void> {
return this.page.exposeFunction(name, <Function>puppeteerFunction);
}
}
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);
}
}
47 changes: 47 additions & 0 deletions src/snap/invokeNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { DappeteerPage, Serializable } from "../page";
import { clickOnElement, openProfileDropdown } from "../helpers";
import { invokeSnap } from "./invokeSnap";

async function waitForNotification(page: DappeteerPage): Promise<void> {
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
await page.waitForSelector(".notifications__container");
await page.evaluate(
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
() =>
new Promise((resolve) => {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
const element = mutation.addedNodes[0] as HTMLElement;
resolve(element.innerText);
observer.disconnect();
}
}
});
observer.observe(document.querySelector(".notifications__container"), {
attributes: false,
childList: true,
});
})
);
}

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

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

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

await waitForNotification(newPage);

return snapResult;
};
21 changes: 18 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,29 @@ export type Dappeteer = {
* Returns all notifications in Metamask notifications page
*/
getAllNotifications: () => Promise<NotificationList>;
/** Invoke notification snap method. The function will open a notification
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
page, invoke the snap, and it will wait until notification is rendered
in the DOM. Can be used in pair with getAllNotifications method f.e:
await metamask.snaps.invokeNotification(page, "snapid", "method_name")
const notifications = await metamask.snaps.getAllNotifications()
expect(notifications[0].message).to.equal("Notification message")
*/
invokeNotification: <
Result = unknown,
Params extends Serializable = Serializable
>(
page: DappeteerPage,
snapId: string,
method: string,
params?: Params
) => Promise<Partial<Result>>;
/**
* Invoke Metamask snap method. Function will throw if there is an error while invoking snap.
* Use generic params to override result and parameter types.
* @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 @@ -92,12 +108,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
2 changes: 1 addition & 1 deletion test/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { expect, use } from "chai";
import chaiAsPromised from "chai-as-promised";

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

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
2 changes: 1 addition & 1 deletion test/flask/permissions-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": "Permissions Snap\n",
"source": {
"shasum": "sApNS/DJjjs8psniSQzWlOdkzJ75IsbtlpdipfKarRI=",
"shasum": "LSrgdJ2wzwzWkfVsqwATqBpEFxjjanFr9JdZHu0DmdU=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
11 changes: 11 additions & 0 deletions test/flask/permissions-snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ export const onRpcRequest: OnRpcRequestHandler = ({ origin, request }) => {
},
],
});
case "notify_inApp": {
return wallet.request({
method: "snap_notify",
params: [
{
type: "inApp",
message: `Hello from permissions snap in App notification`,
},
],
});
}
default:
throw new Error("Method not found.");
}
Expand Down
28 changes: 22 additions & 6 deletions test/flask/snaps.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from "chai";
import * as dappeteer from "../../src";
import { DappeteerPage } from "../../src/page";
import { DappeteerPage } from "../../src";
import { TestContext } from "../constant";
import { Snaps } from "../deploy";

Expand Down Expand Up @@ -86,18 +86,34 @@ describe("snaps", function () {
snapId,
"confirm"
);

await metamask.snaps.rejectDialog();

expect(await invokeAction).to.equal(false);
});

it("should invoke IN APP NOTIFICATIONS and check for a text", async function (this: TestContext) {
await metamask.snaps.invokeSnap(testPage, snapId, "notify_inApp");
it("should invoke IN APP NOTIFICATIONS", async function (this: TestContext) {
const permissionSnapId = await metamask.snaps.installSnap(
this.snapServers[Snaps.PERMISSIONS_SNAP],
{
hasPermissions: true,
hasKeyPermissions: false,
}
);

await metamask.snaps.invokeNotification(testPage, snapId, "notify_inApp");
await metamask.snaps.invokeNotification(
testPage,
permissionSnapId,
"notify_inApp"
);

const notifications = await metamask.snaps.getAllNotifications();

expect(notifications[0].message).to.equal("Hello, in App notification");
expect(notifications[0].message).to.equal(
"Hello from permissions snap in App notification"
);
expect(notifications[1].message).to.equal(
"Hello from methods snap in App notification"
);
});
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"target": "ES2021",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["es2017", "dom"],
"lib": ["es2021", "dom"],
"typeRoots": ["./node_modules/@types", "./types"],
"declaration": true,
"esModuleInterop": true,
Expand Down