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 +98 -0
- package/README.md +158 -150
- package/dist/actions.d.ts +2 -0
- package/dist/actions.js +114 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +45 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +82 -0
- package/dist/download.d.ts +7 -0
- package/dist/download.js +48 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +55 -0
- package/dist/metamask-pages.d.ts +17 -0
- package/dist/metamask-pages.js +143 -0
- package/dist/metamask.d.ts +53 -0
- package/dist/metamask.js +830 -0
- package/dist/page-helpers.d.ts +33 -0
- package/dist/page-helpers.js +199 -0
- package/dist/profile.d.ts +15 -0
- package/dist/profile.js +165 -0
- package/dist/support.d.ts +45 -0
- package/dist/support.js +113 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.js +4 -0
- package/package.json +27 -15
- package/src/actions.js +0 -84
- package/src/browser.js +0 -42
- package/src/config.js +0 -78
- package/src/download.js +0 -50
- package/src/index.js +0 -56
- package/src/metamask-pages.js +0 -133
- package/src/metamask.js +0 -808
- package/src/page-helpers.js +0 -194
- package/src/profile.js +0 -125
- package/src/support.js +0 -102
- package/types/index.d.ts +0 -55
- package/types/support.d.ts +0 -88
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
|
[](https://github.com/blassaut/dappress/blob/conformance-reports/MATRIX.md)
|
|
6
6
|
|
|
7
|
-
Dappress loads the MetaMask
|
|
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
|
-
- **
|
|
10
|
-
- **Cypress
|
|
11
|
-
- **
|
|
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
|
|
18
|
-
| Node.js
|
|
19
|
-
| Browser
|
|
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
|
-
##
|
|
33
|
+
## Quick start
|
|
22
34
|
|
|
23
|
-
|
|
35
|
+
### 1. Install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
24
38
|
npm install --save-dev dappress
|
|
25
39
|
```
|
|
26
40
|
|
|
27
|
-
Register the plugin
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
102
|
+
### 5. Get a browser that loads extensions
|
|
101
103
|
|
|
102
|
-
```
|
|
103
|
-
npx
|
|
104
|
+
```sh
|
|
105
|
+
npx @puppeteer/browsers install chrome@stable
|
|
104
106
|
```
|
|
105
107
|
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
129
|
+
Pass `--browser chrome-for-testing` instead when `npx cypress info` lists it. Add `--headed` to watch MetaMask at work.
|
|
152
130
|
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
```yaml
|
|
148
|
+
# GitHub Actions
|
|
149
|
+
env:
|
|
150
|
+
DAPPRESS_SEED_PHRASE: ${{ secrets.DAPPRESS_SEED_PHRASE }}
|
|
151
|
+
```
|
|
173
152
|
|
|
174
|
-
|
|
153
|
+
Read [Security](#security) before you pick that seed phrase.
|
|
175
154
|
|
|
176
|
-
|
|
155
|
+
## Commands
|
|
177
156
|
|
|
178
|
-
|
|
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
|
-
|
|
|
184
|
-
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
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
|
-
###
|
|
167
|
+
### Signatures and transactions
|
|
195
168
|
|
|
196
|
-
|
|
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
|
-
|
|
176
|
+
### Networks and tokens
|
|
199
177
|
|
|
200
|
-
|
|
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
|
-
|
|
188
|
+
### Accounts and wallet
|
|
203
189
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
199
|
+
### Transaction options
|
|
212
200
|
|
|
213
|
-
|
|
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
|
-
|
|
205
|
+
// One of the fee levels MetaMask offers
|
|
206
|
+
cy.confirmTransaction({ gas: 'aggressive' });
|
|
216
207
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
+
## Configuration
|
|
226
221
|
|
|
227
|
-
|
|
222
|
+
Settings come from three places:
|
|
228
223
|
|
|
229
|
-
|
|
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
|
-
|
|
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`.**
|
|
251
|
-
- **"MetaMask showed no confirmation".** The dapp sent no request, or
|
|
252
|
-
- **Adding or importing an account never finishes.** `chromeWebSecurity: false` is set in
|
|
253
|
-
- **The wallet shows accounts you did not create.** The seed phrase is used elsewhere, and MetaMask restored what
|
|
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
|
-
##
|
|
263
|
+
## Contributing
|
|
256
264
|
|
|
257
|
-
|
|
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
|
|
package/dist/actions.js
ADDED
|
@@ -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
|
+
}
|