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 +91 -0
- package/README.md +59 -13
- 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 -67
- 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 -121
- package/src/metamask.js +0 -427
- package/src/page-helpers.js +0 -194
- package/src/profile.js +0 -125
- package/src/support.js +0 -90
- package/types/index.d.ts +0 -55
- package/types/support.d.ts +0 -42
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.
|
|
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.
|
|
206
|
-
src/config.
|
|
207
|
-
src/download.
|
|
208
|
-
src/browser.
|
|
209
|
-
src/profile.
|
|
210
|
-
src/metamask-pages.
|
|
211
|
-
src/metamask.
|
|
212
|
-
src/page-helpers.
|
|
213
|
-
src/actions.
|
|
214
|
-
src/support.
|
|
215
|
-
types
|
|
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
|
|
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
|
+
}
|
|
@@ -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>;
|
package/dist/browser.js
ADDED
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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>;
|
package/dist/download.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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>;
|