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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ What changes for the users of Dappress, version by version. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the versions [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.5.0] - 2026-10-03
8
+
9
+ ### Changed
10
+
11
+ - The sources are in TypeScript. The package ships compiled JavaScript in `dist`, and its type declarations are generated from the code. `dappress` and `dappress/support` are imported as before.
12
+ - `cy.useNetwork()` goes on as soon as MetaMask has answered a switch it makes without asking, in place of a fixed pause of 1.5 s, and waits for the provider's `chainChanged` in place of asking for the chain every half second.
13
+
14
+ ### Fixed
15
+
16
+ - The Transaction Shield offer is closed by its cross when MetaMask is not in English. Its "Learn more" button was pressed instead, which opened a tab over MetaMask's page: the next command timed out.
17
+
18
+ ## [0.4.0] - 2026-10-03
19
+
20
+ ### Added
21
+
22
+ - `cy.lockWallet()`, `cy.unlockWallet()` and `cy.disconnectFromDapp()`.
23
+ - `cy.connectToDapp({ accounts })` chooses the accounts the dapp is connected with.
24
+ - `cy.confirmTransaction({ spendingCap, gas })` sets the spending cap of an ERC-20 approval and the network fee before confirming.
25
+
26
+ ## [0.3.3] - 2026-10-03
27
+
28
+ ### Fixed
29
+
30
+ - MetaMask's extension is looked for again when its page is read while it loads.
31
+
32
+ ## [0.3.2] - 2026-10-03
33
+
34
+ ### Added
35
+
36
+ - A failed command says what the MetaMask page shows, and what may keep a button disabled.
37
+
38
+ ### Fixed
39
+
40
+ - A confirmation is scrolled to its end while its button stays disabled.
41
+ - An element is taken again before each check and click: MetaMask re-renders its screens.
42
+ - MetaMask's popup is given its size back when the window it gets is too small to lay out.
43
+ - A modal over the home screen is dismissed before the account list is opened, by its cross first, and the account menu is pressed again when the list doesn't open.
44
+ - The browser that builds the cached profile starts without the sandbox on Linux, and keeps MetaMask's page in front.
45
+
46
+ ## [0.3.1] - 2026-10-02
47
+
48
+ ### Added
49
+
50
+ - Headless runs: Dappress passes MetaMask to Chrome itself, since Cypress leaves extensions out of a headless launch.
51
+
52
+ ## [0.3.0] - 2026-10-02
53
+
54
+ ### Changed
55
+
56
+ - Without a seed phrase, a new wallet is made for each run.
57
+
58
+ ## [0.2.0] - 2026-10-02
59
+
60
+ ### Added
61
+
62
+ - `cy.addAccount()`, `cy.importAccount()` and `cy.switchAccount()`.
63
+
64
+ ### Changed
65
+
66
+ - MetaMask's backup and sync is turned off while the wallet is imported, unless `backupAndSync` is set.
67
+
68
+ ## [0.1.1] - 2026-10-02
69
+
70
+ ### Changed
71
+
72
+ - The error of a confirmation that stays after its button was pressed says whether it is the same request, and what MetaMask logged.
73
+
74
+ ## [0.1.0] - 2026-10-02
75
+
76
+ ### Added
77
+
78
+ - MetaMask loaded into the browser Cypress launches, the wallet imported from a seed phrase, and commands to answer a dapp's requests: connection, signatures, transactions, networks and tokens.
79
+ - The wallet setup file, `cypress/wallet.setup.ts`, and the opt-in profile cache.
80
+ - TypeScript declarations.
81
+
82
+ [Unreleased]: https://github.com/blassaut/dappress/compare/v0.5.0...HEAD
83
+ [0.5.0]: https://github.com/blassaut/dappress/compare/v0.4.0...v0.5.0
84
+ [0.4.0]: https://github.com/blassaut/dappress/compare/v0.3.3...v0.4.0
85
+ [0.3.3]: https://github.com/blassaut/dappress/compare/v0.3.2...v0.3.3
86
+ [0.3.2]: https://github.com/blassaut/dappress/compare/v0.3.1...v0.3.2
87
+ [0.3.1]: https://github.com/blassaut/dappress/compare/v0.3.0...v0.3.1
88
+ [0.3.0]: https://github.com/blassaut/dappress/compare/v0.2.0...v0.3.0
89
+ [0.2.0]: https://github.com/blassaut/dappress/compare/v0.1.1...v0.2.0
90
+ [0.1.1]: https://github.com/blassaut/dappress/compare/v0.1.0...v0.1.1
91
+ [0.1.0]: https://github.com/blassaut/dappress/releases/tag/v0.1.0
package/README.md CHANGED
@@ -126,10 +126,12 @@ The wallet is imported once, before the first test of each spec. Each command wa
126
126
  | Command | What it does |
127
127
  |---|---|
128
128
  | `cy.connectToDapp()` | Accepts the connection request, then moves the dapp onto the network of the wallet setup |
129
+ | `cy.connectToDapp({ accounts })` | The same, with the accounts named in `accounts` and no other, such as `['Account 1', 'Account 2']`, in place of the wallet's selected account |
129
130
  | `cy.rejectConnection()` | Rejects the connection request |
131
+ | `cy.disconnectFromDapp()` | Disconnects the dapp from MetaMask's permissions screen. The dapp receives an empty `accountsChanged`. |
130
132
  | `cy.confirmSignature()` | Signs the message (`personal_sign`, `eth_signTypedData_*`) |
131
133
  | `cy.rejectSignature()` | Rejects the signature request |
132
- | `cy.confirmTransaction()` | Sends the transaction, including ERC-20 approvals |
134
+ | `cy.confirmTransaction(options?)` | Sends the transaction, including ERC-20 approvals. With options, sets its spending cap or its network fee first: see [Transaction options](#transaction-options). |
133
135
  | `cy.rejectTransaction()` | Rejects the transaction |
134
136
  | `cy.approveNewNetwork()` | Adds the network requested by `wallet_addEthereumChain` |
135
137
  | `cy.rejectNewNetwork()` | Rejects the network |
@@ -140,10 +142,35 @@ The wallet is imported once, before the first test of each spec. Each command wa
140
142
  | `cy.addAccount()` | Adds an account to the wallet and selects it. Yields its name, such as `Account 2`. |
141
143
  | `cy.importAccount(privateKey)` | Imports an account from its private key and selects it. Yields its name. The key stays out of the command log. |
142
144
  | `cy.switchAccount(name)` | Selects an account of the wallet by its name |
145
+ | `cy.lockWallet()` | Locks the wallet from MetaMask's menu. Requests wait behind its unlock screen until it is unlocked. |
146
+ | `cy.unlockWallet()` | Unlocks the wallet with the configured password. Yields how it was found: `locked`, or `unlocked` when there was nothing to do. |
143
147
  | `cy.useNetwork(network?)` | Moves the dapp onto a network, adding it to MetaMask when needed. Defaults to the network of the wallet setup. |
144
148
  | `cy.getAccountAddress()` | Yields the address the dapp is connected with |
145
149
  | `cy.setupMetaMask()` | Imports the wallet, or unlocks it. Called automatically before each spec. |
146
150
 
151
+ ### Transaction options
152
+
153
+ `cy.confirmTransaction()` takes what a user may change on the confirmation before confirming it:
154
+
155
+ ```ts
156
+ // An ERC-20 approval: let the spender use 2.5 tokens, whatever the dapp asked for
157
+ cy.confirmTransaction({ spendingCap: '2.5' });
158
+
159
+ // The network fee, from the advanced form of MetaMask's fee editor
160
+ cy.confirmTransaction({ gas: { maxBaseFee: 30, priorityFee: 2, gasLimit: 100000 } });
161
+
162
+ // Or one of the estimates the fee editor lists
163
+ cy.confirmTransaction({ gas: 'aggressive' });
164
+ ```
165
+
166
+ | Option | Value |
167
+ |---|---|
168
+ | `spendingCap` | The amount the spender may use, in tokens, as typed in MetaMask: `5` or `'2.5'`. The command fails on a transaction that is not an ERC-20 approval. |
169
+ | `gas` | `'low'`, `'market'` or `'aggressive'` on a network MetaMask has fee estimates for; `'networkSuggested'` on one it only has a gas price for, such as a local node. The command fails when MetaMask does not list the estimate. |
170
+ | `gas` | Or custom values, named as MetaMask labels them: `maxBaseFee` and `priorityFee` in GWEI, which become the transaction's `maxFeePerGas` and `maxPriorityFeePerGas`, and `gasLimit`. A field left out keeps MetaMask's value. EIP-1559 transactions only. |
171
+
172
+ Both options can be given together. MetaMask remembers the fee chosen for an account on a network, and starts the next transactions from it.
173
+
147
174
  ### Configuration
148
175
 
149
176
  Settings normally live in `cypress/wallet.setup.ts`. Secrets go in `cypress.env.json` or in environment variables, which take precedence. The full order, first one found wins:
@@ -189,7 +216,26 @@ The GitHub workflow runs the suite daily, in the three modes, against the latest
189
216
 
190
217
  1. Run the suite. Each failure names a screenshot of the MetaMask screen at that moment.
191
218
  2. Locate the new selector in MetaMask's page objects: `github.com/MetaMask/metamask-extension/tree/v<version>/test/e2e/page-objects/pages`.
192
- 3. Update `src/metamask.js`, run the suite against the new and the previous release, bump the default version in `src/config.js`, and commit the report.
219
+ 3. Update `src/metamask.ts`, run the suite against the new and the previous release, bump the default version in `src/config.ts`, and commit the report.
220
+
221
+ ## Development
222
+
223
+ ```bash
224
+ npm run lint # ESLint
225
+ npm run format # Prettier, on everything but the Markdown files
226
+ npm run check-types # TypeScript, on the sources, the scripts and the suite
227
+ npm test # unit tests
228
+ npm run build # the package, compiled into dist
229
+ ```
230
+
231
+ The CI workflow runs these on every pull request, with `format:check` in place of `format`.
232
+
233
+ ### Releasing
234
+
235
+ 1. Move what `CHANGELOG.md` lists under "Unreleased" to a section for the new version, and set the version in `package.json` (`npm version <version> --no-git-tag-version`).
236
+ 2. Commit, then tag the commit `v<version>` and push the tag.
237
+
238
+ The release workflow checks the tag against `package.json`, publishes the package to npm and makes a GitHub release from the version's section of the changelog. npm trusts the workflow itself ([trusted publishing](https://docs.npmjs.com/trusted-publishers)): no npm token is stored.
193
239
 
194
240
  ## Architecture
195
241
 
@@ -202,17 +248,17 @@ Cypress leaves extensions out of a headless launch, so Dappress passes MetaMask
202
248
  With `cache: true`, Dappress imports the wallet once before the run, in a browser of its own, and keeps the profile; MetaMask's storage is copied from it into the profile Cypress is about to launch, so each run starts from a wallet that has never seen the dapp.
203
249
 
204
250
  ```
205
- src/index.js configureDappress(): plugin entry point
206
- src/config.js options and wallet setup file
207
- src/download.js download and cache of MetaMask builds
208
- src/browser.js Puppeteer connection to the Cypress browser
209
- src/profile.js wallet profile built once and reused across runs
210
- src/metamask-pages.js discovery of MetaMask's pages
211
- src/metamask.js MetaMask screens: selectors and flows
212
- src/page-helpers.js wait, click and fill primitives with fallback selectors
213
- src/actions.js Cypress tasks behind the commands
214
- src/support.js cy.* commands
215
- types/ TypeScript declarations
251
+ src/index.ts configureDappress(): plugin entry point
252
+ src/config.ts options and wallet setup file
253
+ src/download.ts download and cache of MetaMask builds
254
+ src/browser.ts Puppeteer connection to the Cypress browser
255
+ src/profile.ts wallet profile built once and reused across runs
256
+ src/metamask-pages.ts discovery of MetaMask's pages
257
+ src/metamask.ts MetaMask screens: selectors and flows
258
+ src/page-helpers.ts wait, click and fill primitives with fallback selectors
259
+ src/actions.ts Cypress tasks behind the commands
260
+ src/support.ts cy.* commands
261
+ src/types.ts types of the public API
216
262
  conformance/ conformance suite
217
263
  ```
218
264
 
@@ -0,0 +1,2 @@
1
+ import type { ResolvedOptions } from './types';
2
+ export declare function createTasks(options: ResolvedOptions): Cypress.Tasks;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ // The Node side of every cy.* command, registered as Cypress tasks. Each
3
+ // action gets a Puppeteer browser connected to the Cypress browser, finds the
4
+ // MetaMask page it needs and drives it through src/metamask.ts.
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createTasks = createTasks;
40
+ const browser_1 = require("./browser");
41
+ const metamask = __importStar(require("./metamask"));
42
+ const metamask_pages_1 = require("./metamask-pages");
43
+ function createTasks(options) {
44
+ // Found once: the browser is the same for the whole run
45
+ let extensionId;
46
+ async function metamaskId(browser) {
47
+ extensionId = extensionId || (await (0, metamask_pages_1.findExtensionId)(browser));
48
+ return extensionId;
49
+ }
50
+ /** Onboard the wallet or unlock it, so the dapp can talk to it. Safe to call repeatedly. */
51
+ const setupWallet = (browser) => openWallet(browser, { onboard: true });
52
+ async function openWallet(browser, { onboard }) {
53
+ const home = await (0, metamask_pages_1.getHomePage)(browser, await metamaskId(browser));
54
+ const state = await metamask.walletState(home);
55
+ console.log(`[dappress] MetaMask is ${state}`);
56
+ if (state === 'onboarding' && onboard)
57
+ await metamask.onboard(home, options);
58
+ if (state === 'locked')
59
+ await metamask.unlock(home, options);
60
+ if (state === 'unknown')
61
+ throw new Error(`[dappress] Unexpected MetaMask screen at ${home.url()}`);
62
+ await home.close();
63
+ return state;
64
+ }
65
+ /** A task that drives the wallet's own screens, from its full-screen page. */
66
+ const onHomePage = (flow) => async (browser, argument) => {
67
+ const home = await (0, metamask_pages_1.getHomePage)(browser, await metamaskId(browser));
68
+ try {
69
+ await home.bringToFront();
70
+ return await flow(home, argument);
71
+ }
72
+ finally {
73
+ await home.close();
74
+ }
75
+ };
76
+ /**
77
+ * Unlock a locked wallet with the configured password, and say how it was
78
+ * found. Nothing to do on one already unlocked.
79
+ */
80
+ async function unlockWallet(browser) {
81
+ const state = await openWallet(browser, { onboard: false });
82
+ if (state === 'onboarding')
83
+ throw new Error('[dappress] No wallet to unlock: MetaMask is at its onboarding');
84
+ // The side panel, or a popup opened by a request, keeps its unlock form after an unlock made elsewhere
85
+ for (const page of await (0, metamask_pages_1.getRequestPages)(browser, await metamaskId(browser)))
86
+ await metamask.leaveUnlockForm(page, options);
87
+ return state;
88
+ }
89
+ /** Approve the prompt a network change raised: "Add network" or the permission to switch. */
90
+ async function approveNetworkChange(browser) {
91
+ const page = await (0, metamask_pages_1.getConfirmationPage)(browser, await metamaskId(browser), options.timeout);
92
+ await metamask.approveNetworkChange(page, options.timeout);
93
+ }
94
+ /** The task for a decision: find the confirmation, press its button. */
95
+ const decide = (decision) => async (browser, argument) => {
96
+ const page = await (0, metamask_pages_1.getConfirmationPage)(browser, await metamaskId(browser), options.timeout);
97
+ await metamask.decide(decision, page, options.timeout, argument);
98
+ };
99
+ const actions = {
100
+ setupWallet,
101
+ approveNetworkChange,
102
+ addAccount: onHomePage(metamask.addAccount),
103
+ switchAccount: onHomePage(metamask.switchAccount),
104
+ importAccount: onHomePage(metamask.importAccount),
105
+ lockWallet: onHomePage(metamask.lock),
106
+ unlockWallet,
107
+ disconnectFromDapp: onHomePage(metamask.disconnectSite),
108
+ };
109
+ for (const decision of Object.keys(metamask.decisions))
110
+ actions[decision] = decide(decision);
111
+ // cy.task() needs a value back: null when the action has nothing to say
112
+ const asTask = (action) => async (argument) => (await (0, browser_1.withBrowser)((browser) => action(browser, argument))) ?? null;
113
+ return Object.fromEntries(Object.entries(actions).map(([name, action]) => [`dappress:${name}`, asTask(action)]));
114
+ }
@@ -0,0 +1,7 @@
1
+ import { type Browser } from 'puppeteer-core';
2
+ export declare function captureDebuggerUrl(on: Cypress.PluginEvents): void;
3
+ /**
4
+ * Run `action` with a Puppeteer browser connected to Cypress's browser, then
5
+ * give the focus back to the Cypress tab and disconnect. The browser stays open.
6
+ */
7
+ export declare function withBrowser<T>(action: (browser: Browser) => Promise<T>): Promise<T>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ // Connects Puppeteer to the browser Cypress launched, through the debugging
3
+ // URL Cypress hands out in after:browser:launch (Cypress 13.6+).
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.captureDebuggerUrl = captureDebuggerUrl;
9
+ exports.withBrowser = withBrowser;
10
+ const puppeteer_core_1 = __importDefault(require("puppeteer-core"));
11
+ let debuggerUrl;
12
+ function captureDebuggerUrl(on) {
13
+ on('after:browser:launch', (browser, options) => {
14
+ debuggerUrl = options.webSocketDebuggerUrl;
15
+ });
16
+ }
17
+ /**
18
+ * Run `action` with a Puppeteer browser connected to Cypress's browser, then
19
+ * give the focus back to the Cypress tab and disconnect. The browser stays open.
20
+ */
21
+ async function withBrowser(action) {
22
+ if (!debuggerUrl) {
23
+ throw new Error('[dappress] No browser to connect to. Is configureDappress() called in setupNodeEvents, with Cypress 13.6 or later?');
24
+ }
25
+ const browser = await puppeteer_core_1.default.connect({ browserWSEndpoint: debuggerUrl, defaultViewport: null });
26
+ try {
27
+ return await action(browser);
28
+ }
29
+ catch (error) {
30
+ // The browser side only gets the message, so keep the full stack in the terminal
31
+ console.error(error.stack || error);
32
+ throw error;
33
+ }
34
+ finally {
35
+ await focusCypressTab(browser).catch(() => { });
36
+ await browser.disconnect();
37
+ }
38
+ }
39
+ // MetaMask opens its own tabs and popups; Chrome throttles the Cypress tab
40
+ // while it is in the background. The Cypress runner lives under /__/.
41
+ async function focusCypressTab(browser) {
42
+ const target = browser.targets().find((candidate) => candidate.type() === 'page' && candidate.url().includes('/__/'));
43
+ if (target)
44
+ await (await target.page())?.bringToFront();
45
+ }
@@ -0,0 +1,21 @@
1
+ import type { DappressOptions, Network, PublicOptions, ResolvedOptions } from './types';
2
+ export declare const DEFAULTS: {
3
+ metamaskVersion: string;
4
+ seedPhrase: string | null;
5
+ password: string;
6
+ network: Network | null;
7
+ backupAndSync: boolean;
8
+ autoSetup: boolean;
9
+ cache: boolean;
10
+ timeout: number;
11
+ cacheDir: string;
12
+ };
13
+ /**
14
+ * The plugin options, in order of precedence: environment variables, the
15
+ * Cypress `env` block (cypress.env.json), the wallet setup file
16
+ * (cypress/wallet.setup.ts or .js), the options given to configureDappress(),
17
+ * then the defaults above.
18
+ */
19
+ export declare function resolveOptions(userOptions?: DappressOptions, cypressConfig?: Partial<Pick<Cypress.PluginConfigOptions, 'env' | 'projectRoot'>>): ResolvedOptions;
20
+ /** The subset of options that is safe to expose to the browser side (no secrets). */
21
+ export declare function publicOptions(options: ResolvedOptions): PublicOptions;
package/dist/config.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULTS = void 0;
7
+ exports.resolveOptions = resolveOptions;
8
+ exports.publicOptions = publicOptions;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const bip39_1 = require("@scure/bip39");
13
+ const english_1 = require("@scure/bip39/wordlists/english");
14
+ exports.DEFAULTS = {
15
+ metamaskVersion: '13.50.0',
16
+ // None: a new wallet is made for each run. A phrase known to others is not
17
+ // blank, since MetaMask restores the accounts saved for it elsewhere.
18
+ seedPhrase: null,
19
+ password: 'Tester@1234',
20
+ // The network cy.connectToDapp() moves the dapp onto, as a wallet_addEthereumChain parameter; none by default
21
+ network: null,
22
+ // MetaMask's backup and sync saves the accounts and contacts of a seed phrase
23
+ // and restores them on other installs. Off, a test's accounts don't come back.
24
+ backupAndSync: false,
25
+ // Run cy.setupMetaMask() automatically before the first test of each spec
26
+ autoSetup: true,
27
+ // Import the wallet once and reuse the profile across runs (keeps the vault under cacheDir)
28
+ cache: false,
29
+ // How long to wait for MetaMask to show a confirmation after a dapp request (ms)
30
+ timeout: 20000,
31
+ cacheDir: node_path_1.default.join(node_os_1.default.homedir(), '.cache', 'dappress'),
32
+ };
33
+ // A .ts file loads when the Cypress config is itself in TypeScript
34
+ const WALLET_SETUP_FILES = ['cypress/wallet.setup.ts', 'cypress/wallet.setup.js'];
35
+ /**
36
+ * The plugin options, in order of precedence: environment variables, the
37
+ * Cypress `env` block (cypress.env.json), the wallet setup file
38
+ * (cypress/wallet.setup.ts or .js), the options given to configureDappress(),
39
+ * then the defaults above.
40
+ */
41
+ function resolveOptions(userOptions = {}, cypressConfig = {}) {
42
+ const env = cypressConfig.env || {};
43
+ const fromEnv = {
44
+ metamaskVersion: process.env.DAPPRESS_METAMASK_VERSION || env.DAPPRESS_METAMASK_VERSION,
45
+ seedPhrase: process.env.DAPPRESS_SEED_PHRASE || env.DAPPRESS_SEED_PHRASE,
46
+ password: process.env.DAPPRESS_PASSWORD || env.DAPPRESS_PASSWORD,
47
+ };
48
+ const options = { ...exports.DEFAULTS, ...userOptions, ...loadWalletSetup(cypressConfig.projectRoot) };
49
+ for (const [key, value] of Object.entries(fromEnv)) {
50
+ if (value)
51
+ options[key] = value;
52
+ }
53
+ const seedPhrase = options.seedPhrase || newWallet(options);
54
+ return { ...options, seedPhrase };
55
+ }
56
+ /** No seed phrase was given: make one for this run. It is never written anywhere. */
57
+ function newWallet(options) {
58
+ console.log('[dappress] No seed phrase configured: using a new wallet for this run');
59
+ if (options.cache) {
60
+ console.warn('[dappress] The profile cache needs a seed phrase of your own: importing the wallet in this run instead');
61
+ options.cache = false;
62
+ }
63
+ return (0, bip39_1.generateMnemonic)(english_1.wordlist);
64
+ }
65
+ /** The project's wallet setup file: { seedPhrase?, network? }. */
66
+ function loadWalletSetup(projectRoot = process.cwd()) {
67
+ const file = WALLET_SETUP_FILES.map((name) => node_path_1.default.join(projectRoot, name)).find((candidate) => node_fs_1.default.existsSync(candidate));
68
+ if (!file)
69
+ return {};
70
+ // The file is the project's, found at run time: there is nothing to import statically
71
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
72
+ const loaded = require(file);
73
+ const setup = loaded.default || loaded; // `export default` or `module.exports`
74
+ const unknown = Object.keys(setup).filter((key) => !['seedPhrase', 'network'].includes(key));
75
+ if (unknown.length)
76
+ throw new Error(`[dappress] Unknown keys in ${node_path_1.default.basename(file)}: ${unknown.join(', ')}`);
77
+ return setup;
78
+ }
79
+ /** The subset of options that is safe to expose to the browser side (no secrets). */
80
+ function publicOptions(options) {
81
+ return { metamaskVersion: options.metamaskVersion, autoSetup: options.autoSetup, network: options.network };
82
+ }
@@ -0,0 +1,7 @@
1
+ import type { ResolvedOptions } from './types';
2
+ /**
3
+ * Download and unzip the MetaMask Chrome build for `version` into the cache.
4
+ * Returns the path of the unpacked extension (the folder holding manifest.json).
5
+ * Already cached versions are reused.
6
+ */
7
+ export declare function prepareExtension({ metamaskVersion, cacheDir }: Pick<ResolvedOptions, 'metamaskVersion' | 'cacheDir'>): Promise<string>;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.prepareExtension = prepareExtension;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const promises_1 = require("node:stream/promises");
10
+ const node_stream_1 = require("node:stream");
11
+ const extract_zip_1 = __importDefault(require("extract-zip"));
12
+ function releaseUrl(version) {
13
+ return `https://github.com/MetaMask/metamask-extension/releases/download/v${version}/metamask-chrome-${version}.zip`;
14
+ }
15
+ function extensionDir(cacheDir, version) {
16
+ return node_path_1.default.join(cacheDir, 'metamask', version);
17
+ }
18
+ async function download(url, destination) {
19
+ const response = await fetch(url, { redirect: 'follow' });
20
+ if (!response.ok || !response.body) {
21
+ throw new Error(`[dappress] Could not download ${url}: HTTP ${response.status}`);
22
+ }
23
+ await node_fs_1.default.promises.mkdir(node_path_1.default.dirname(destination), { recursive: true });
24
+ await (0, promises_1.pipeline)(node_stream_1.Readable.fromWeb(response.body), node_fs_1.default.createWriteStream(destination));
25
+ }
26
+ /**
27
+ * Download and unzip the MetaMask Chrome build for `version` into the cache.
28
+ * Returns the path of the unpacked extension (the folder holding manifest.json).
29
+ * Already cached versions are reused.
30
+ */
31
+ async function prepareExtension({ metamaskVersion, cacheDir }) {
32
+ const dir = extensionDir(cacheDir, metamaskVersion);
33
+ const manifest = node_path_1.default.join(dir, 'manifest.json');
34
+ if (node_fs_1.default.existsSync(manifest))
35
+ return dir;
36
+ const zipPath = node_path_1.default.join(cacheDir, 'metamask', `metamask-chrome-${metamaskVersion}.zip`);
37
+ if (!node_fs_1.default.existsSync(zipPath)) {
38
+ console.log(`[dappress] Downloading MetaMask ${metamaskVersion}…`);
39
+ await download(releaseUrl(metamaskVersion), zipPath);
40
+ }
41
+ console.log(`[dappress] Unpacking MetaMask ${metamaskVersion}…`);
42
+ await node_fs_1.default.promises.rm(dir, { recursive: true, force: true });
43
+ await (0, extract_zip_1.default)(zipPath, { dir });
44
+ if (!node_fs_1.default.existsSync(manifest)) {
45
+ throw new Error(`[dappress] ${zipPath} does not look like a MetaMask build: no manifest.json after extraction`);
46
+ }
47
+ return dir;
48
+ }
@@ -0,0 +1,14 @@
1
+ /// <reference types="cypress" preserve="true" />
2
+ import type { DappressOptions } from './types';
3
+ export type { DappressOptions, Network, WalletSetup } from './types';
4
+ /**
5
+ * Wire Dappress into a Cypress project. Call it from setupNodeEvents:
6
+ *
7
+ * setupNodeEvents(on, config) {
8
+ * return configureDappress(on, config);
9
+ * }
10
+ *
11
+ * It loads the MetaMask extension into the browser Cypress launches, and
12
+ * registers the tasks behind the cy.* commands from 'dappress/support'.
13
+ */
14
+ export declare function configureDappress(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions, userOptions?: DappressOptions): Cypress.PluginConfigOptions;
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /// <reference types="cypress" preserve="true" />
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.configureDappress = configureDappress;
5
+ const config_1 = require("./config");
6
+ const browser_1 = require("./browser");
7
+ const download_1 = require("./download");
8
+ const profile_1 = require("./profile");
9
+ const actions_1 = require("./actions");
10
+ /**
11
+ * Wire Dappress into a Cypress project. Call it from setupNodeEvents:
12
+ *
13
+ * setupNodeEvents(on, config) {
14
+ * return configureDappress(on, config);
15
+ * }
16
+ *
17
+ * It loads the MetaMask extension into the browser Cypress launches, and
18
+ * registers the tasks behind the cy.* commands from 'dappress/support'.
19
+ */
20
+ function configureDappress(on, config, userOptions = {}) {
21
+ const options = (0, config_1.resolveOptions)(userOptions, config);
22
+ if (config.chromeWebSecurity === false) {
23
+ console.warn('[dappress] chromeWebSecurity is off: MetaMask cannot start its snaps, so adding or importing an account will hang');
24
+ }
25
+ // A cached wallet gets its requests in MetaMask's popup window, which headless Chrome doesn't open
26
+ const usesCache = (browser) => options.cache && !browser.isHeadless;
27
+ // Build the wallet profile before the run, out of the time Cypress allows a browser to come up
28
+ on('before:run', async ({ browser }) => {
29
+ if (browser && usesCache(browser))
30
+ await (0, profile_1.prepareProfile)({ browserPath: browser.path, extensionDir: await (0, download_1.prepareExtension)(options), options });
31
+ });
32
+ on('before:browser:launch', async (browser, launchOptions) => {
33
+ if (browser.family !== 'chromium') {
34
+ throw new Error(`[dappress] MetaMask is a Chromium extension; ${browser.name} is not supported`);
35
+ }
36
+ const extensionDir = await (0, download_1.prepareExtension)(options);
37
+ launchOptions.extensions.push(extensionDir);
38
+ if (browser.isHeadless) {
39
+ // Cypress leaves extensions out of a headless launch, so ask Chrome directly
40
+ launchOptions.args.push(`--load-extension=${extensionDir}`);
41
+ if (options.cache)
42
+ console.warn('[dappress] The wallet cache needs a headed browser: importing the wallet in this run instead');
43
+ }
44
+ if (usesCache(browser)) {
45
+ const profileDir = await (0, profile_1.prepareProfile)({ browserPath: browser.path, extensionDir, options });
46
+ await (0, profile_1.installProfile)(profileDir, browser, config.isTextTerminal);
47
+ }
48
+ return launchOptions;
49
+ });
50
+ (0, browser_1.captureDebuggerUrl)(on);
51
+ on('task', (0, actions_1.createTasks)(options));
52
+ // Non-sensitive settings the browser side reads with Cypress.expose()
53
+ config.expose = { ...config.expose, dappress: (0, config_1.publicOptions)(options) };
54
+ return config;
55
+ }
@@ -0,0 +1,17 @@
1
+ import type { Browser, Page } from 'puppeteer-core';
2
+ /**
3
+ * MetaMask's extension id. Cypress loads its own extension in headed
4
+ * Chromium browsers, so the id is the one whose home page is titled "MetaMask".
5
+ */
6
+ export declare function findExtensionId(browser: Browser, timeout?: number): Promise<string>;
7
+ /** The full-screen MetaMask page, opened if it isn't already. */
8
+ export declare function getHomePage(browser: Browser, extensionId: string): Promise<Page>;
9
+ /** The pages open where dapp requests show up: the side panel, a popup. */
10
+ export declare function getRequestPages(browser: Browser, extensionId: string): Promise<Page[]>;
11
+ /**
12
+ * The page a dapp request is shown on. Waits for the popup to open, or takes
13
+ * the side panel. A popup still on its home route is one closing after the
14
+ * previous request, or one that hasn't routed to the request yet: skipped.
15
+ */
16
+ export declare function getConfirmationPage(browser: Browser, extensionId: string, timeout: number): Promise<Page>;
17
+ export declare function withUsableWindow(page: Page): Promise<Page>;