dappress 0.2.0 → 0.3.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/README.md +6 -7
- package/package.json +2 -1
- package/src/config.js +16 -2
- package/src/metamask.js +23 -6
- package/types/index.d.ts +4 -1
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ const wallet: WalletSetup = {
|
|
|
66
66
|
export default wallet;
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
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):
|
|
70
70
|
|
|
71
71
|
```json
|
|
72
72
|
{
|
|
@@ -82,7 +82,6 @@ env:
|
|
|
82
82
|
DAPPRESS_SEED_PHRASE: ${{ secrets.DAPPRESS_SEED_PHRASE }}
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
When neither is provided, Dappress uses the public Hardhat / Anvil development wallet. Give your test suite a seed phrase of its own: that one is shared with everybody.
|
|
86
85
|
|
|
87
86
|
Add the command types to `tsconfig.json`:
|
|
88
87
|
|
|
@@ -109,7 +108,7 @@ it('connects the wallet and signs in', () => {
|
|
|
109
108
|
cy.visit('/');
|
|
110
109
|
cy.contains('button', 'Connect wallet').click();
|
|
111
110
|
cy.connectToDapp();
|
|
112
|
-
cy.getAccountAddress().should('
|
|
111
|
+
cy.getAccountAddress().should('match', /^0x[0-9a-f]{40}$/);
|
|
113
112
|
|
|
114
113
|
cy.contains('button', 'Sign in').click();
|
|
115
114
|
cy.confirmSignature();
|
|
@@ -153,17 +152,17 @@ Settings normally live in `cypress/wallet.setup.ts`. Secrets go in `cypress.env.
|
|
|
153
152
|
| Option | Environment variable | Default |
|
|
154
153
|
|---|---|---|
|
|
155
154
|
| `metamaskVersion` | `DAPPRESS_METAMASK_VERSION` | `13.50.0`. The build is downloaded from MetaMask's GitHub releases on first run and cached in `~/.cache/dappress`. |
|
|
156
|
-
| `seedPhrase` | `DAPPRESS_SEED_PHRASE` |
|
|
155
|
+
| `seedPhrase` | `DAPPRESS_SEED_PHRASE` | None. Dappress makes a new wallet for each run, which nobody else knows and which is never written anywhere. |
|
|
157
156
|
| `password` | `DAPPRESS_PASSWORD` | `Tester@1234`. It only protects the throwaway browser profile Cypress creates for each run. |
|
|
158
157
|
| `network` | | None. The dapp stays on the network MetaMask starts on, Ethereum mainnet. |
|
|
159
158
|
| `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. |
|
|
160
159
|
| `autoSetup` | | `true`. Dappress imports or unlocks the wallet before the first test of each spec. Set to `false` to call `cy.setupMetaMask()` yourself. |
|
|
161
|
-
| `cache` | | `false`. The wallet is imported in every run, about fifteen seconds. With `true`, 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. |
|
|
160
|
+
| `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. |
|
|
162
161
|
| `timeout` | | `20000` ms. The time allowed for MetaMask to display a request before a command fails. |
|
|
163
162
|
|
|
164
163
|
### Security
|
|
165
164
|
|
|
166
|
-
|
|
165
|
+
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.
|
|
167
166
|
|
|
168
167
|
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.
|
|
169
168
|
|
|
@@ -214,7 +213,7 @@ Selectors are MetaMask's `data-testid` attributes, each with the button's Englis
|
|
|
214
213
|
- **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 …`.
|
|
215
214
|
- **"MetaMask showed no confirmation".** The dapp sent no request, or sent it on a network whose RPC endpoint is unreachable.
|
|
216
215
|
- **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.
|
|
217
|
-
- **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
|
|
216
|
+
- **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.
|
|
218
217
|
|
|
219
218
|
## Status
|
|
220
219
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dappress",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MetaMask automation for Cypress: load the real extension, import a test wallet, answer dapp requests with cy.* commands. Tracks the latest MetaMask release.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Benjamin Lassaut",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"cypress": ">=13.6.0"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"@scure/bip39": "^1.6.0",
|
|
60
61
|
"extract-zip": "^2.0.1",
|
|
61
62
|
"puppeteer-core": "^24.43.1"
|
|
62
63
|
},
|
package/src/config.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const os = require('node:os');
|
|
3
3
|
const path = require('node:path');
|
|
4
|
+
const { generateMnemonic } = require('@scure/bip39');
|
|
5
|
+
const { wordlist } = require('@scure/bip39/wordlists/english');
|
|
4
6
|
|
|
5
7
|
const DEFAULTS = {
|
|
6
8
|
metamaskVersion: '13.50.0',
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
+
// None: a new wallet is made for each run. A phrase known to others is not
|
|
10
|
+
// blank, since MetaMask restores the accounts saved for it elsewhere.
|
|
11
|
+
seedPhrase: null,
|
|
9
12
|
password: 'Tester@1234',
|
|
10
13
|
// The network cy.connectToDapp() moves the dapp onto, as a wallet_addEthereumChain parameter; none by default
|
|
11
14
|
network: null,
|
|
@@ -42,9 +45,20 @@ function resolveOptions(userOptions = {}, cypressConfig = {}) {
|
|
|
42
45
|
for (const [key, value] of Object.entries(fromEnv)) {
|
|
43
46
|
if (value) options[key] = value;
|
|
44
47
|
}
|
|
48
|
+
if (!options.seedPhrase) useNewWallet(options);
|
|
45
49
|
return options;
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/** No seed phrase was given: make one for this run. It is never written anywhere. */
|
|
53
|
+
function useNewWallet(options) {
|
|
54
|
+
options.seedPhrase = generateMnemonic(wordlist);
|
|
55
|
+
console.log('[dappress] No seed phrase configured: using a new wallet for this run');
|
|
56
|
+
if (options.cache) {
|
|
57
|
+
console.warn('[dappress] The profile cache needs a seed phrase of your own: importing the wallet in this run instead');
|
|
58
|
+
options.cache = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
/** The project's wallet setup file: { seedPhrase?, network? }. */
|
|
49
63
|
function loadWalletSetup(projectRoot = process.cwd()) {
|
|
50
64
|
const file = WALLET_SETUP_FILES.map((name) => path.join(projectRoot, name)).find((candidate) => fs.existsSync(candidate));
|
package/src/metamask.js
CHANGED
|
@@ -214,23 +214,40 @@ async function reloadHome(page) {
|
|
|
214
214
|
await page.goto(homeUrl);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* Add an account to the wallet and select it. Yields its name, "Account N".
|
|
219
|
+
* A click that lands while the list is still settling is lost, so the button
|
|
220
|
+
* is pressed again when no account shows up.
|
|
221
|
+
*/
|
|
218
222
|
async function addAccount(page) {
|
|
219
223
|
await click(page, selectors.home.accountMenu);
|
|
220
224
|
const before = await accountNames(page);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
while (Date.now() < deadline) {
|
|
224
|
-
const [added] = (await accountNames(page)).filter((name) => !before.includes(name));
|
|
225
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
226
|
+
const added = (await newAccount(page, before, 0)) || (await pressAddAccount(page, before));
|
|
225
227
|
if (added) {
|
|
226
228
|
await selectAccount(page, added);
|
|
227
229
|
return added;
|
|
228
230
|
}
|
|
229
|
-
await sleep(250);
|
|
230
231
|
}
|
|
231
232
|
throw await failure(page, 'MetaMask added no account');
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
async function pressAddAccount(page, before) {
|
|
236
|
+
await clickWhenEnabled(page, selectors.accounts.add, { timeout: 30000 });
|
|
237
|
+
return newAccount(page, before, 15000);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The name of an account that wasn't in `before`, within `timeout` ms. */
|
|
241
|
+
async function newAccount(page, before, timeout) {
|
|
242
|
+
const deadline = Date.now() + timeout;
|
|
243
|
+
do {
|
|
244
|
+
const [added] = (await accountNames(page)).filter((name) => !before.includes(name));
|
|
245
|
+
if (added) return added;
|
|
246
|
+
await sleep(250);
|
|
247
|
+
} while (Date.now() < deadline);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
234
251
|
async function accountNames(page) {
|
|
235
252
|
await waitFor(page, selectors.accounts.name);
|
|
236
253
|
return page.$$eval(selectors.accounts.name, (cells) => cells.map((cell) => cell.textContent.trim()));
|
package/types/index.d.ts
CHANGED
|
@@ -12,7 +12,10 @@ export interface Network {
|
|
|
12
12
|
|
|
13
13
|
/** What cypress/wallet.setup.ts exports. */
|
|
14
14
|
export interface WalletSetup {
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* The wallet to import. Without one, Dappress makes a new wallet for each run.
|
|
17
|
+
* Prefer DAPPRESS_SEED_PHRASE in cypress.env.json or the environment: this file is usually committed.
|
|
18
|
+
*/
|
|
16
19
|
seedPhrase?: string;
|
|
17
20
|
/** The network the dapp is moved onto by cy.connectToDapp(). */
|
|
18
21
|
network?: Network;
|