dappress 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ // Find MetaMask's pages in the browser Cypress launched: the full-screen home
3
+ // page, and the surface where dapp requests show up. On Chrome that surface
4
+ // is the side panel when it is open, and the "MetaMask Dialog" popup otherwise.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findExtensionId = findExtensionId;
7
+ exports.getHomePage = getHomePage;
8
+ exports.getRequestPages = getRequestPages;
9
+ exports.getConfirmationPage = getConfirmationPage;
10
+ exports.withUsableWindow = withUsableWindow;
11
+ const page_helpers_1 = require("./page-helpers");
12
+ const HOME_PATH = '/home.html';
13
+ const CONFIRMATION_PATHS = ['/notification.html', '/sidepanel.html'];
14
+ function extensionIdOf(url) {
15
+ const match = /^chrome-extension:\/\/([a-z]{32})\//.exec(url);
16
+ return match ? match[1] : null;
17
+ }
18
+ function isHomeRoute(url) {
19
+ return url.includes('/notification.html') && /notification\.html(#\/?)?$/.test(url);
20
+ }
21
+ function pagesOf(browser, extensionId, pathname) {
22
+ return browser.targets().filter((target) => target.type() === 'page' && target.url().startsWith(`chrome-extension://${extensionId}${pathname}`));
23
+ }
24
+ /**
25
+ * MetaMask's extension id. Cypress loads its own extension in headed
26
+ * Chromium browsers, so the id is the one whose home page is titled "MetaMask".
27
+ */
28
+ async function findExtensionId(browser, timeout = 30000) {
29
+ const deadline = Date.now() + timeout;
30
+ while (Date.now() < deadline) {
31
+ const ids = new Set(browser
32
+ .targets()
33
+ .map((target) => extensionIdOf(target.url()))
34
+ .filter((id) => id !== null));
35
+ for (const id of ids) {
36
+ if (await isMetaMask(browser, id))
37
+ return id;
38
+ }
39
+ await (0, page_helpers_1.sleep)(500);
40
+ }
41
+ throw new Error('[dappress] MetaMask extension not found in the browser. Is it loaded in before:browser:launch?');
42
+ }
43
+ async function isMetaMask(browser, extensionId) {
44
+ const [target] = pagesOf(browser, extensionId, HOME_PATH);
45
+ if (target) {
46
+ // A page still loading or redirecting loses its execution context while its
47
+ // title is read; not MetaMask yet, findExtensionId looks again
48
+ try {
49
+ const page = await pageOf(target);
50
+ return (await page.title()).startsWith('MetaMask');
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ const page = await browser.newPage();
57
+ try {
58
+ const response = await page.goto(`chrome-extension://${extensionId}${HOME_PATH}`, { waitUntil: 'domcontentloaded' });
59
+ return Boolean(response && response.ok()) && (await page.title()).startsWith('MetaMask');
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ finally {
65
+ await page.close();
66
+ }
67
+ }
68
+ // A target of type "page" has a page; Puppeteer types it as one that may not
69
+ async function pageOf(target) {
70
+ const page = await target.page();
71
+ if (!page)
72
+ throw new Error(`[dappress] No page behind ${target.url()}`);
73
+ return page;
74
+ }
75
+ /** The full-screen MetaMask page, opened if it isn't already. */
76
+ async function getHomePage(browser, extensionId) {
77
+ const [target] = pagesOf(browser, extensionId, HOME_PATH);
78
+ if (target)
79
+ return pageOf(target);
80
+ const page = await browser.newPage();
81
+ await page.goto(`chrome-extension://${extensionId}${HOME_PATH}`, { waitUntil: 'domcontentloaded' });
82
+ return page;
83
+ }
84
+ /** The pages open where dapp requests show up: the side panel, a popup. */
85
+ async function getRequestPages(browser, extensionId) {
86
+ const pages = [];
87
+ for (const pathname of CONFIRMATION_PATHS) {
88
+ for (const target of pagesOf(browser, extensionId, pathname)) {
89
+ const page = await target.page();
90
+ if (page)
91
+ pages.push(pathname === '/notification.html' ? await withUsableWindow(page) : page);
92
+ }
93
+ }
94
+ return pages;
95
+ }
96
+ /**
97
+ * The page a dapp request is shown on. Waits for the popup to open, or takes
98
+ * the side panel. A popup still on its home route is one closing after the
99
+ * previous request, or one that hasn't routed to the request yet: skipped.
100
+ */
101
+ async function getConfirmationPage(browser, extensionId, timeout) {
102
+ const deadline = Date.now() + timeout;
103
+ while (Date.now() < deadline) {
104
+ for (const pathname of CONFIRMATION_PATHS) {
105
+ const [target] = pagesOf(browser, extensionId, pathname).filter((candidate) => !isHomeRoute(candidate.url()));
106
+ if (target)
107
+ return pathname === '/notification.html' ? withUsableWindow(await pageOf(target)) : pageOf(target);
108
+ }
109
+ await (0, page_helpers_1.sleep)(250);
110
+ }
111
+ const seen = browser
112
+ .targets()
113
+ .filter((target) => extensionIdOf(target.url()) === extensionId)
114
+ .map((target) => `${target.type()} ${target.url()}`)
115
+ .join(', ');
116
+ throw new Error(`[dappress] MetaMask showed no confirmation within ${timeout}ms. Did the dapp send a request? MetaMask pages seen: ${seen || 'none'}`);
117
+ }
118
+ // MetaMask asks Chrome for a 400x620 popup. Under Xvfb on a GitHub runner the
119
+ // window it gets is 1x1: nothing is laid out, the pane of a confirmation that
120
+ // must be read to the end has no height, so it can never be scrolled there and
121
+ // its Confirm button stays disabled. The popup is given its size back first.
122
+ const POPUP = { width: 400, height: 620 };
123
+ async function withUsableWindow(page) {
124
+ const session = await page.target().createCDPSession();
125
+ try {
126
+ const { windowId, bounds } = await session.send('Browser.getWindowForTarget');
127
+ if (bounds.windowState === 'normal' && (bounds.width ?? 0) >= 200 && (bounds.height ?? 0) >= 200)
128
+ return page;
129
+ console.log(`[dappress] MetaMask's popup is ${bounds.width}x${bounds.height} (${bounds.windowState}); resizing it to ${POPUP.width}x${POPUP.height}`);
130
+ if (bounds.windowState !== 'normal')
131
+ await session.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });
132
+ await session.send('Browser.setWindowBounds', { windowId, bounds: { ...POPUP } });
133
+ // Let the page lay itself out at its new size before anything is looked for in it
134
+ await (0, page_helpers_1.sleep)(300);
135
+ }
136
+ catch (error) {
137
+ console.warn(`[dappress] Could not check the popup window's size: ${error.message}`);
138
+ }
139
+ finally {
140
+ await session.detach().catch(() => { });
141
+ }
142
+ return page;
143
+ }
@@ -0,0 +1,53 @@
1
+ import type { Page } from 'puppeteer-core';
2
+ import type { ConnectOptions, ResolvedOptions, TransactionOptions, WalletState } from './types';
3
+ export declare const decisions: {
4
+ connectToDapp: string[];
5
+ rejectConnection: string[];
6
+ approveNewNetwork: string[];
7
+ rejectNewNetwork: string[];
8
+ approveSwitchNetwork: string[];
9
+ rejectSwitchNetwork: string[];
10
+ approveAddToken: string[];
11
+ rejectAddToken: string[];
12
+ confirmSignature: string[];
13
+ rejectSignature: string[];
14
+ confirmTransaction: string[];
15
+ rejectTransaction: string[];
16
+ };
17
+ export type Decision = keyof typeof decisions;
18
+ /** Which screen the extension home page shows, once MetaMask has started. */
19
+ export declare function walletState(page: Page): Promise<WalletState | 'unknown'>;
20
+ /** Import a wallet from its seed phrase on a fresh MetaMask install. */
21
+ export declare function onboard(page: Page, { seedPhrase, password, backupAndSync }: Pick<ResolvedOptions, 'seedPhrase' | 'password' | 'backupAndSync'>): Promise<void>;
22
+ export declare function unlock(page: Page, { password }: Pick<ResolvedOptions, 'password'>): Promise<void>;
23
+ /**
24
+ * A page that was showing the unlock form when the wallet got unlocked from
25
+ * another one keeps showing it, and the dapp's requests wait behind it. The
26
+ * password is given there too, as a user would; what follows is the wallet
27
+ * or a request, so only the form is watched.
28
+ */
29
+ export declare function leaveUnlockForm(page: Page, { password }: Pick<ResolvedOptions, 'password'>): Promise<void>;
30
+ /**
31
+ * Add an account to the wallet and select it. Yields its name, "Account N".
32
+ * A click that lands while the list is still settling is lost, so the button
33
+ * is pressed again when no account shows up.
34
+ */
35
+ export declare function addAccount(page: Page): Promise<string>;
36
+ /** Make `name` the selected account of the wallet. */
37
+ export declare function switchAccount(page: Page, name: string): Promise<void>;
38
+ /** Import an account from its private key and select it. Yields its name. */
39
+ export declare function importAccount(page: Page, privateKey: string): Promise<string>;
40
+ /** Lock the wallet from the menu of its home page. Nothing to do on a wallet already locked. */
41
+ export declare function lock(page: Page): Promise<void>;
42
+ /**
43
+ * Disconnect the site at `origin` from the wallet, on its page of the
44
+ * "Permissions" screen. The dapp gets an empty accountsChanged.
45
+ */
46
+ export declare function disconnectSite(page: Page, origin: string): Promise<void>;
47
+ /** The options a command took from the test: those of the commands that take any. */
48
+ type CommandOptions = ConnectOptions & TransactionOptions;
49
+ /** Press the button of `decision` on the confirmation shown on `page`, once it is set as `options` ask. */
50
+ export declare function decide(decision: Decision, page: Page, timeout: number, options?: CommandOptions): Promise<void>;
51
+ /** wallet_addEthereumChain on a network MetaMask knows behaves like a switch: either prompt may show. */
52
+ export declare function approveNetworkChange(page: Page, timeout: number): Promise<void>;
53
+ export {};