dappress 0.4.0 → 0.5.1

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,98 @@
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.1] - 2026-10-05
8
+
9
+ ### Changed
10
+
11
+ - The README is shorter and keeps to what using Dappress needs. What contributors need moves to `CONTRIBUTING.md`.
12
+
13
+ ## [0.5.0] - 2026-10-03
14
+
15
+ ### Changed
16
+
17
+ - 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.
18
+ - `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.
19
+
20
+ ### Fixed
21
+
22
+ - 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.
23
+
24
+ ## [0.4.0] - 2026-10-03
25
+
26
+ ### Added
27
+
28
+ - `cy.lockWallet()`, `cy.unlockWallet()` and `cy.disconnectFromDapp()`.
29
+ - `cy.connectToDapp({ accounts })` chooses the accounts the dapp is connected with.
30
+ - `cy.confirmTransaction({ spendingCap, gas })` sets the spending cap of an ERC-20 approval and the network fee before confirming.
31
+
32
+ ## [0.3.3] - 2026-10-03
33
+
34
+ ### Fixed
35
+
36
+ - MetaMask's extension is looked for again when its page is read while it loads.
37
+
38
+ ## [0.3.2] - 2026-10-03
39
+
40
+ ### Added
41
+
42
+ - A failed command says what the MetaMask page shows, and what may keep a button disabled.
43
+
44
+ ### Fixed
45
+
46
+ - A confirmation is scrolled to its end while its button stays disabled.
47
+ - An element is taken again before each check and click: MetaMask re-renders its screens.
48
+ - MetaMask's popup is given its size back when the window it gets is too small to lay out.
49
+ - 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.
50
+ - The browser that builds the cached profile starts without the sandbox on Linux, and keeps MetaMask's page in front.
51
+
52
+ ## [0.3.1] - 2026-10-02
53
+
54
+ ### Added
55
+
56
+ - Headless runs: Dappress passes MetaMask to Chrome itself, since Cypress leaves extensions out of a headless launch.
57
+
58
+ ## [0.3.0] - 2026-10-02
59
+
60
+ ### Changed
61
+
62
+ - Without a seed phrase, a new wallet is made for each run.
63
+
64
+ ## [0.2.0] - 2026-10-02
65
+
66
+ ### Added
67
+
68
+ - `cy.addAccount()`, `cy.importAccount()` and `cy.switchAccount()`.
69
+
70
+ ### Changed
71
+
72
+ - MetaMask's backup and sync is turned off while the wallet is imported, unless `backupAndSync` is set.
73
+
74
+ ## [0.1.1] - 2026-10-02
75
+
76
+ ### Changed
77
+
78
+ - The error of a confirmation that stays after its button was pressed says whether it is the same request, and what MetaMask logged.
79
+
80
+ ## [0.1.0] - 2026-10-02
81
+
82
+ ### Added
83
+
84
+ - 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.
85
+ - The wallet setup file, `cypress/wallet.setup.ts`, and the opt-in profile cache.
86
+ - TypeScript declarations.
87
+
88
+ [Unreleased]: https://github.com/blassaut/dappress/compare/v0.5.1...HEAD
89
+ [0.5.1]: https://github.com/blassaut/dappress/compare/v0.5.0...v0.5.1
90
+ [0.5.0]: https://github.com/blassaut/dappress/compare/v0.4.0...v0.5.0
91
+ [0.4.0]: https://github.com/blassaut/dappress/compare/v0.3.3...v0.4.0
92
+ [0.3.3]: https://github.com/blassaut/dappress/compare/v0.3.2...v0.3.3
93
+ [0.3.2]: https://github.com/blassaut/dappress/compare/v0.3.1...v0.3.2
94
+ [0.3.1]: https://github.com/blassaut/dappress/compare/v0.3.0...v0.3.1
95
+ [0.3.0]: https://github.com/blassaut/dappress/compare/v0.2.0...v0.3.0
96
+ [0.2.0]: https://github.com/blassaut/dappress/compare/v0.1.1...v0.2.0
97
+ [0.1.1]: https://github.com/blassaut/dappress/compare/v0.1.0...v0.1.1
98
+ [0.1.0]: https://github.com/blassaut/dappress/releases/tag/v0.1.0
package/README.md CHANGED
@@ -4,27 +4,41 @@
4
4
 
5
5
  [![MetaMask conformance](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/blassaut/dappress/conformance-reports/badge.json)](https://github.com/blassaut/dappress/blob/conformance-reports/MATRIX.md)
6
6
 
7
- Dappress loads the MetaMask browser extension into the browser Cypress launches, imports a test wallet, and exposes `cy.*` commands that answer the requests a dapp sends to the wallet: connection, signatures, transactions, network changes.
7
+ Your dapp asks MetaMask to connect, sign or send a transaction, and a real user clicks a button in the wallet. Dappress clicks it for you. It loads the real MetaMask extension into the browser Cypress launches, imports a test wallet, and gives you `cy.*` commands such as `cy.connectToDapp()` or `cy.confirmTransaction()`.
8
8
 
9
- - **Current MetaMask.** The extension version is a configuration value. Each release is verified by a conformance suite, with one test per command.
10
- - **Cypress native.** No second browser, no proxy. Dappress drives MetaMask through Puppeteer, connected to the browser Cypress already runs. Typed commands for TypeScript projects.
11
- - **Resilient selectors.** Every selector comes from MetaMask's own end-to-end test suite and carries a fallback.
9
+ - **The latest MetaMask.** You choose the extension version. Every MetaMask release is tested daily, one test per command: see the [conformance matrix](https://github.com/blassaut/dappress/blob/conformance-reports/MATRIX.md).
10
+ - **Plain Cypress.** No second browser, no proxy. Commands are typed for TypeScript.
11
+ - **Stable selectors.** They come from MetaMask's own end-to-end tests, each with a fallback.
12
+
13
+ ## Contents
14
+
15
+ - [Requirements](#requirements)
16
+ - [Quick start](#quick-start)
17
+ - [Commands](#commands)
18
+ - [Configuration](#configuration)
19
+ - [Security](#security)
20
+ - [Troubleshooting](#troubleshooting)
21
+ - [Contributing](#contributing)
12
22
 
13
23
  ## Requirements
14
24
 
15
- | Dependency | Version |
16
- |---|---|
17
- | Cypress | 13.6 or later |
18
- | Node.js | 20 or later |
19
- | Browser | Chrome for Testing, Chromium or Electron. Google Chrome 137 and later cannot load extensions. |
25
+ | Dependency | Version |
26
+ | ---------- | ----------------------------------------------------------------------------- |
27
+ | Cypress | 15.10 or later |
28
+ | Node.js | 20 or later |
29
+ | Browser | Chrome for Testing, Chromium or Electron. Not Google Chrome: since version 137 it no longer loads extensions. |
30
+
31
+ Tested on macOS and on GitHub's Linux runners.
20
32
 
21
- ## Installation
33
+ ## Quick start
22
34
 
23
- ```bash
35
+ ### 1. Install
36
+
37
+ ```sh
24
38
  npm install --save-dev dappress
25
39
  ```
26
40
 
27
- Register the plugin in the Cypress configuration:
41
+ ### 2. Register the plugin
28
42
 
29
43
  ```ts
30
44
  // cypress.config.ts
@@ -34,7 +48,7 @@ import { configureDappress } from 'dappress';
34
48
  export default defineConfig({
35
49
  e2e: {
36
50
  baseUrl: 'http://localhost:3000',
37
- testIsolation: false, // the wallet state is shared across tests
51
+ testIsolation: false,
38
52
  setupNodeEvents(on, config) {
39
53
  return configureDappress(on, config);
40
54
  },
@@ -42,21 +56,36 @@ export default defineConfig({
42
56
  });
43
57
  ```
44
58
 
45
- Load the commands in the support file:
59
+ `testIsolation: false` is required. The tests of a spec share one wallet, and Cypress must not reset the page between them. Each test starts where the previous one ended, so write the tests of a spec to run in order.
60
+
61
+ ### 3. Load the commands
46
62
 
47
63
  ```ts
48
64
  // cypress/support/e2e.ts
49
65
  import 'dappress/support';
50
66
  ```
51
67
 
52
- Describe the test wallet:
68
+ ```json
69
+ // tsconfig.json
70
+ {
71
+ "compilerOptions": {
72
+ "types": ["cypress", "dappress/support"]
73
+ }
74
+ }
75
+ ```
76
+
77
+ JavaScript projects use the same files with a `.js` extension and `module.exports`, and skip `tsconfig.json`.
78
+
79
+ ### 4. Choose the network
80
+
81
+ Dappress reads `cypress/wallet.setup.ts` (or `.js`) by itself. It holds no secret, so commit it.
53
82
 
54
83
  ```ts
55
84
  // cypress/wallet.setup.ts
56
85
  import type { WalletSetup } from 'dappress';
57
86
 
58
87
  const wallet: WalletSetup = {
59
- // The network the dapp is moved onto when it connects
88
+ // The network cy.connectToDapp() moves the dapp onto
60
89
  network: {
61
90
  chainId: '0x88bb0',
62
91
  chainName: 'Hoodi',
@@ -68,46 +97,20 @@ const wallet: WalletSetup = {
68
97
  export default wallet;
69
98
  ```
70
99
 
71
- Without a seed phrase, Dappress makes a new wallet for each run: enough to connect and sign. To test with a wallet of yours, one that holds test funds for instance, give its seed phrase and keep it out of the repository, in `cypress.env.json` (git-ignored):
72
-
73
- ```json
74
- {
75
- "DAPPRESS_SEED_PHRASE": "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12"
76
- }
77
- ```
78
-
79
- In CI, provide it as an encrypted secret of the repository, exposed to the job as an environment variable:
80
-
81
- ```yaml
82
- # GitHub Actions
83
- env:
84
- DAPPRESS_SEED_PHRASE: ${{ secrets.DAPPRESS_SEED_PHRASE }}
85
- ```
86
-
87
-
88
- Add the command types to `tsconfig.json`:
89
-
90
- ```json
91
- {
92
- "compilerOptions": {
93
- "types": ["cypress", "dappress/support"]
94
- }
95
- }
96
- ```
97
-
98
- JavaScript projects use the same files with a `.js` extension and `module.exports`.
100
+ This step is optional. Without a network, the dapp stays on Ethereum mainnet, where MetaMask starts.
99
101
 
100
- Run the tests in a browser that supports extensions:
102
+ ### 5. Get a browser that loads extensions
101
103
 
102
- ```bash
103
- npx cypress run --browser chrome-for-testing --headed
104
+ ```sh
105
+ npx @puppeteer/browsers install chrome@stable
104
106
  ```
105
107
 
106
- Without `--headed`, the run is headless and works the same, except for the wallet cache, which needs a headed browser.
108
+ The command downloads Chrome for Testing and prints the path of its executable. `npx cypress info` lists the browsers Cypress finds by itself.
107
109
 
108
- ## Usage
110
+ ### 6. Write a test and run it
109
111
 
110
112
  ```ts
113
+ // cypress/e2e/wallet.cy.ts
111
114
  it('connects the wallet and signs in', () => {
112
115
  cy.visit('/');
113
116
  cy.contains('button', 'Connect wallet').click();
@@ -119,142 +122,147 @@ it('connects the wallet and signs in', () => {
119
122
  });
120
123
  ```
121
124
 
122
- The wallet is imported once, before the first test of each spec. Each command waits for MetaMask to display the request, answers it, and waits for the request to be dismissed.
123
-
124
- ### Commands
125
-
126
- | Command | What it does |
127
- |---|---|
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 |
130
- | `cy.rejectConnection()` | Rejects the connection request |
131
- | `cy.disconnectFromDapp()` | Disconnects the dapp from MetaMask's permissions screen. The dapp receives an empty `accountsChanged`. |
132
- | `cy.confirmSignature()` | Signs the message (`personal_sign`, `eth_signTypedData_*`) |
133
- | `cy.rejectSignature()` | Rejects the signature request |
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). |
135
- | `cy.rejectTransaction()` | Rejects the transaction |
136
- | `cy.approveNewNetwork()` | Adds the network requested by `wallet_addEthereumChain` |
137
- | `cy.rejectNewNetwork()` | Rejects the network |
138
- | `cy.approveSwitchNetwork()` | Grants the permission asked by `wallet_switchEthereumChain` for a network the dapp is not yet allowed on |
139
- | `cy.rejectSwitchNetwork()` | Denies that permission |
140
- | `cy.approveAddToken()` | Adds the token requested by `wallet_watchAsset` |
141
- | `cy.rejectAddToken()` | Rejects the token |
142
- | `cy.addAccount()` | Adds an account to the wallet and selects it. Yields its name, such as `Account 2`. |
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. |
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. |
147
- | `cy.useNetwork(network?)` | Moves the dapp onto a network, adding it to MetaMask when needed. Defaults to the network of the wallet setup. |
148
- | `cy.getAccountAddress()` | Yields the address the dapp is connected with |
149
- | `cy.setupMetaMask()` | Imports the wallet, or unlocks it. Called automatically before each spec. |
125
+ ```sh
126
+ npx cypress run --browser /path/to/chrome-for-testing
127
+ ```
150
128
 
151
- ### Transaction options
129
+ Pass `--browser chrome-for-testing` instead when `npx cypress info` lists it. Add `--headed` to watch MetaMask at work.
152
130
 
153
- `cy.confirmTransaction()` takes what a user may change on the confirmation before confirming it:
131
+ Before the first test of each spec, Dappress imports the wallet. Each command then waits for MetaMask to show the request, answers it, and waits for the request to close.
154
132
 
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' });
133
+ ### Testing with funds
158
134
 
159
- // The network fee, from the advanced form of MetaMask's fee editor
160
- cy.confirmTransaction({ gas: { maxBaseFee: 30, priorityFee: 2, gasLimit: 100000 } });
135
+ By default, Dappress creates a new, empty wallet for each run. That is enough to connect and sign. To send transactions, use a wallet of yours that holds test funds, and give its seed phrase outside the repository.
161
136
 
162
- // Or one of the estimates the fee editor lists
163
- cy.confirmTransaction({ gas: 'aggressive' });
137
+ On your machine, in `cypress.env.json` (add it to `.gitignore`):
138
+
139
+ ```json
140
+ {
141
+ "DAPPRESS_SEED_PHRASE": "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12"
142
+ }
164
143
  ```
165
144
 
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. |
145
+ In CI, as an encrypted secret:
171
146
 
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.
147
+ ```yaml
148
+ # GitHub Actions
149
+ env:
150
+ DAPPRESS_SEED_PHRASE: ${{ secrets.DAPPRESS_SEED_PHRASE }}
151
+ ```
173
152
 
174
- ### Configuration
153
+ Read [Security](#security) before you pick that seed phrase.
175
154
 
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:
155
+ ## Commands
177
156
 
178
- 1. Environment variables, for secrets in CI.
179
- 2. `cypress.env.json`, for secrets on a development machine, such as the seed phrase.
180
- 3. `cypress/wallet.setup.ts`, for everything else.
181
- 4. The third argument of `configureDappress(on, config, options)`.
157
+ ### Connection
182
158
 
183
- | Option | Environment variable | Default |
184
- |---|---|---|
185
- | `metamaskVersion` | `DAPPRESS_METAMASK_VERSION` | `13.50.0`. The build is downloaded from MetaMask's GitHub releases on first run and cached in `~/.cache/dappress`. |
186
- | `seedPhrase` | `DAPPRESS_SEED_PHRASE` | None. Dappress makes a new wallet for each run, which nobody else knows and which is never written anywhere. |
187
- | `password` | `DAPPRESS_PASSWORD` | `Tester@1234`. It only protects the throwaway browser profile Cypress creates for each run. |
188
- | `network` | | None. The dapp stays on the network MetaMask starts on, Ethereum mainnet. |
189
- | `backupAndSync` | | `false`. Dappress turns off MetaMask's backup and sync while importing the wallet, so that an account a test adds is not restored by the next import of the same seed phrase. With `true`, MetaMask keeps saving and restoring the accounts and contacts of that phrase. |
190
- | `autoSetup` | | `true`. Dappress imports or unlocks the wallet before the first test of each spec. Set to `false` to call `cy.setupMetaMask()` yourself. |
191
- | `cache` | | `false`. The wallet is imported in every run, about fifteen seconds. With `true`, and a seed phrase of your own, it is imported once, in a browser Dappress opens before the run, and the resulting profile is reused by later runs, which then start by unlocking the wallet. Headed runs only: a headless run imports the wallet as usual. |
192
- | `timeout` | | `20000` ms. The time allowed for MetaMask to display a request before a command fails. |
159
+ | Command | What it does |
160
+ | -------------------------------- | -------------------------------------------------------------------------------------------- |
161
+ | `cy.connectToDapp()` | Accepts the connection request, then switches the dapp to the network of your wallet setup. |
162
+ | `cy.connectToDapp({ accounts })` | Same, but connects only the listed accounts, for example `['Account 1', 'Account 2']`. |
163
+ | `cy.rejectConnection()` | Rejects the connection request. |
164
+ | `cy.disconnectFromDapp()` | Disconnects the dapp in MetaMask. The dapp receives an empty `accountsChanged`. |
165
+ | `cy.getAccountAddress()` | Yields the address the dapp is connected with. |
193
166
 
194
- ### Security
167
+ ### Signatures and transactions
195
168
 
196
- When you give a seed phrase, make it one dedicated to testing, funded on test networks only. Avoid a phrase other people know, such as the Hardhat or Anvil development mnemonic: MetaMask restores the accounts others saved for it, so the wallet is not the one you expect. The seed phrase and password remain on the Node.js side: they are never exposed to the browser nor written to the Cypress command log. The wallet setup file contains no secret and can be committed.
169
+ | Command | What it does |
170
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
171
+ | `cy.confirmSignature()` | Signs the message (`personal_sign`, `eth_signTypedData_*`). |
172
+ | `cy.rejectSignature()` | Rejects the signature request. |
173
+ | `cy.confirmTransaction(options?)` | Confirms the transaction, ERC-20 approvals included. Options set the spending cap or the gas fee first: see below. |
174
+ | `cy.rejectTransaction()` | Rejects the transaction. |
197
175
 
198
- With `cache: true`, the profile under `~/.cache/dappress/profiles` holds the wallet's vault, encrypted by MetaMask with the password. Treat that directory like the seed phrase: keep it on the machine, and do not store it in a CI cache that other people or workflows can restore.
176
+ ### Networks and tokens
199
177
 
200
- ## Conformance suite
178
+ | Command | What it does |
179
+ | --------------------------- | -------------------------------------------------------------------------------------------------- |
180
+ | `cy.useNetwork(network?)` | Switches the dapp to a network, and adds it to MetaMask if needed. Defaults to your wallet setup. |
181
+ | `cy.approveNewNetwork()` | Accepts a `wallet_addEthereumChain` request. |
182
+ | `cy.rejectNewNetwork()` | Rejects it. |
183
+ | `cy.approveSwitchNetwork()` | Accepts a `wallet_switchEthereumChain` request to a network the dapp has no permission on yet. |
184
+ | `cy.rejectSwitchNetwork()` | Rejects it. |
185
+ | `cy.approveAddToken()` | Accepts a `wallet_watchAsset` request. |
186
+ | `cy.rejectAddToken()` | Rejects it. |
201
187
 
202
- The suite runs one test per command against [MetaMask's test dapp](https://metamask.github.io/test-dapp/). Each run makes a wallet nobody has used, with Foundry's `cast`, and starts a local [Anvil](https://getfoundry.sh) node that funds it. It requires Chrome for Testing and Foundry.
188
+ ### Accounts and wallet
203
189
 
204
- ```bash
205
- npm run conformance # default MetaMask version, side panel
206
- npm run conformance -- 13.51.0 # a specific release
207
- DAPPRESS_MODE=headless npm run conformance # without a browser window
208
- DAPPRESS_MODE=popup npm run conformance # wallet from the profile cache: requests in the popup
209
- ```
190
+ | Command | What it does |
191
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
192
+ | `cy.addAccount()` | Creates an account and selects it. Yields its name, for example `Account 2`. |
193
+ | `cy.importAccount(privateKey)` | Imports an account from a private key and selects it. Yields its name. The key stays out of the log. |
194
+ | `cy.switchAccount(name)` | Selects an account by its name. |
195
+ | `cy.lockWallet()` | Locks the wallet. Requests then wait until it is unlocked. |
196
+ | `cy.unlockWallet()` | Unlocks the wallet. Yields `locked` if it was locked, `unlocked` if there was nothing to do. |
197
+ | `cy.setupMetaMask()` | Imports or unlocks the wallet. Runs by itself before each spec, unless `autoSetup` is `false`. |
210
198
 
211
- A mode is where MetaMask shows the dapp's requests: `sidepanel` (the default), `headless`, or `popup`. Each run writes `reports/metamask-<version>-<mode>.json`, and `npm run matrix -- reports .` turns the reports into `MATRIX.md` and `badge.json`, a [shields.io endpoint](https://shields.io/badges/endpoint-badge) for the latest version.
199
+ ### Transaction options
212
200
 
213
- The GitHub workflow runs the suite daily, in the three modes, against the latest MetaMask release that has no report yet, and publishes the reports, the matrix and the badge on the [`conformance-reports`](https://github.com/blassaut/dappress/tree/conformance-reports) branch: [MATRIX.md](https://github.com/blassaut/dappress/blob/conformance-reports/MATRIX.md).
201
+ ```ts
202
+ // ERC-20 approval: allow the spender 2.5 tokens, whatever the dapp asked for
203
+ cy.confirmTransaction({ spendingCap: '2.5' });
214
204
 
215
- ### Updating to a new MetaMask release
205
+ // One of the fee levels MetaMask offers
206
+ cy.confirmTransaction({ gas: 'aggressive' });
216
207
 
217
- 1. Run the suite. Each failure names a screenshot of the MetaMask screen at that moment.
218
- 2. Locate the new selector in MetaMask's page objects: `github.com/MetaMask/metamask-extension/tree/v<version>/test/e2e/page-objects/pages`.
219
- 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.
208
+ // Custom fees, in GWEI, as in MetaMask's advanced fee form
209
+ cy.confirmTransaction({ gas: { maxBaseFee: 30, priorityFee: 2, gasLimit: 100000 } });
210
+ ```
220
211
 
221
- ## Architecture
212
+ | Option | Value |
213
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
214
+ | `spendingCap` | The amount the spender may use, in tokens: `5` or `'2.5'`. Use a string to avoid rounding. Fails if the transaction is not an ERC-20 approval. |
215
+ | `gas` | A fee level: `'low'`, `'market'` or `'aggressive'`. On a network with no fee estimates, such as a local node, use `'networkSuggested'`. Fails if MetaMask does not offer that level. |
216
+ | `gas` | Or custom values: `maxBaseFee` and `priorityFee` in GWEI, and `gasLimit`. A missing field keeps MetaMask's value. EIP-1559 transactions only. |
222
217
 
223
- Cypress executes tests inside the dapp's tab and has no access to the extension. For each command, Dappress connects Puppeteer to the browser Cypress launched, through the debugging URL Cypress provides, locates the MetaMask page displaying the request and interacts with it.
218
+ Both options can be combined. MetaMask remembers the fee chosen for an account on a network and starts the next transaction from it.
224
219
 
225
- MetaMask displays requests in its side panel when the panel is open, and in its popup window otherwise. Without the cache, the import of the wallet ends by opening the side panel, so requests appear there. With the cache, the wallet was imported in another browser, the panel is closed, and requests appear in the popup. Dappress handles both, and the conformance suite runs in both: its `popup` mode turns the cache on.
220
+ ## Configuration
226
221
 
227
- Cypress leaves extensions out of a headless launch, so Dappress passes MetaMask to Chrome itself. Headless Chrome displays the side panel but does not open the popup window, which is why the cache, whose requests appear in the popup, is limited to headed runs.
222
+ Settings come from three places:
228
223
 
229
- 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.
224
+ - **`cypress/wallet.setup.ts`** for the network. It accepts `network` and `seedPhrase` only, and any other key is an error. Keep the seed phrase out of it, since this file is committed.
225
+ - **Environment variables or `cypress.env.json`** for secrets and the MetaMask version.
226
+ - **The third argument of `configureDappress()`** for everything else:
230
227
 
228
+ ```ts
229
+ setupNodeEvents(on, config) {
230
+ return configureDappress(on, config, { cache: true, timeout: 30000 });
231
+ }
231
232
  ```
232
- src/index.js configureDappress(): plugin entry point
233
- src/config.js options and wallet setup file
234
- src/download.js download and cache of MetaMask builds
235
- src/browser.js Puppeteer connection to the Cypress browser
236
- src/profile.js wallet profile built once and reused across runs
237
- src/metamask-pages.js discovery of MetaMask's pages
238
- src/metamask.js MetaMask screens: selectors and flows
239
- src/page-helpers.js wait, click and fill primitives with fallback selectors
240
- src/actions.js Cypress tasks behind the commands
241
- src/support.js cy.* commands
242
- types/ TypeScript declarations
243
- conformance/ conformance suite
244
- ```
245
233
 
246
- Selectors are MetaMask's `data-testid` attributes, each with the button's English label as a fallback. MetaMask's pages run under LavaMoat, which rejects injected scripts; the helpers therefore rely only on what Puppeteer can do from outside the page.
234
+ When a setting appears in several places, the first one found wins: environment variable, then `cypress.env.json`, then `wallet.setup.ts`, then `configureDappress()`.
235
+
236
+ | Option | Environment variable | Default and meaning |
237
+ | ----------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
238
+ | `network` | | None. The network `cy.connectToDapp()` and `cy.useNetwork()` switch to. |
239
+ | `seedPhrase` | `DAPPRESS_SEED_PHRASE` | None. A new wallet is created for each run. It is never written to disk. |
240
+ | `password` | `DAPPRESS_PASSWORD` | `Tester@1234`. It only protects the throwaway browser profile of the run. |
241
+ | `metamaskVersion` | `DAPPRESS_METAMASK_VERSION` | `13.50.0`. Downloaded from MetaMask's GitHub releases on first use, then cached. |
242
+ | `timeout` | | `20000` ms. How long a command waits for MetaMask to show the request. |
243
+ | `autoSetup` | | `true`. Set to `false` to call `cy.setupMetaMask()` yourself. |
244
+ | `cache` | | `false`. Importing the wallet takes about fifteen seconds per run. With `true`, Dappress imports it once and reuses the browser profile. Needs your own seed phrase and a headed run. |
245
+ | `backupAndSync` | | `false`. Dappress turns off MetaMask's backup and sync, so accounts added by a test do not come back on the next run. |
246
+ | `cacheDir` | | `~/.cache/dappress`. Where MetaMask builds and cached profiles are kept. |
247
+
248
+ ## Security
249
+
250
+ - **Use a seed phrase made for testing**, funded on test networks only.
251
+ - **Avoid well-known phrases**, such as the Hardhat or Anvil development mnemonic. MetaMask restores the accounts other people saved for them, and your wallet will not be the one you expect.
252
+ - **Secrets stay in Node.js.** The seed phrase and the password never reach the browser or the Cypress log.
253
+ - **With `cache: true`**, `~/.cache/dappress/profiles` holds the wallet's encrypted vault. Treat it like the seed phrase: keep it on the machine, and keep it out of any CI cache that other people or workflows can restore.
247
254
 
248
255
  ## Troubleshooting
249
256
 
250
- - **Cypress exits immediately with `MODULE_NOT_FOUND`.** The terminal sets `ELECTRON_RUN_AS_NODE=1`, as some IDEs do. Run `env -u ELECTRON_RUN_AS_NODE npx cypress run …`.
251
- - **"MetaMask showed no confirmation".** The dapp sent no request, or sent it on a network whose RPC endpoint is unreachable.
252
- - **Adding or importing an account never finishes.** `chromeWebSecurity: false` is set in the Cypress config. MetaMask then cannot start its snaps, which its account screens wait for. Leave Chrome's web security on, the Cypress default.
253
- - **The wallet shows accounts you did not create.** The seed phrase is used elsewhere, and MetaMask restored what its cloud holds for it. This happens with well-known development mnemonics. Use a phrase made for your tests, or none.
257
+ - **Cypress exits immediately with `MODULE_NOT_FOUND`.** Your terminal sets `ELECTRON_RUN_AS_NODE=1`, as some IDEs do. Run `env -u ELECTRON_RUN_AS_NODE npx cypress run …`.
258
+ - **"MetaMask showed no confirmation".** The dapp sent no request, or the network's RPC endpoint is unreachable.
259
+ - **Adding or importing an account never finishes.** `chromeWebSecurity: false` is set in your Cypress config. MetaMask then cannot start the snaps its account screens wait for. Remove that setting.
260
+ - **The wallet shows accounts you did not create.** The seed phrase is used elsewhere, and MetaMask restored what it saved for it. Use a phrase made for your tests, or none.
261
+ - **"MetaMask extension not found in the browser".** The browser did not load the extension. Most often it is Google Chrome 137 or later, which no longer can. Use Chrome for Testing: see [step 5](#5-get-a-browser-that-loads-extensions).
254
262
 
255
- ## Status
263
+ ## Contributing
256
264
 
257
- Verified with MetaMask 13.49.0 and 13.50.0, Cypress 16 and Chrome for Testing 154: in headed mode on macOS and on GitHub's Linux runners, and in headless mode on macOS.
265
+ How Dappress works, how to run the conformance suite, update to a new MetaMask release and publish a version: see [CONTRIBUTING.md](CONTRIBUTING.md).
258
266
 
259
267
  ## License
260
268
 
@@ -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
+ }