chainwright 0.8.10 → 0.8.12
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 +250 -56
- package/dist/cli/index.js +8 -8
- package/dist/core/index.d.ts +1 -1
- package/dist/{types-B56pinWs.d.ts → types-DK8rutb5.d.ts} +1 -6
- package/dist/wallets/keplr/index.d.ts +5 -5
- package/dist/wallets/keplr/index.js +3 -3
- package/dist/wallets/metamask/index.d.ts +5 -5
- package/dist/wallets/metamask/index.js +3 -3
- package/dist/wallets/meteor/index.d.ts +5 -5
- package/dist/wallets/meteor/index.js +7 -7
- package/dist/wallets/petra/index.d.ts +5 -5
- package/dist/wallets/petra/index.js +3 -3
- package/dist/wallets/phantom/index.d.ts +5 -5
- package/dist/wallets/phantom/index.js +2 -2
- package/dist/wallets/solflare/index.d.ts +5 -5
- package/dist/wallets/solflare/index.js +4 -2
- package/dist/{worker-scope-context-DiN3_Sig.d.ts → worker-scope-context-CNAfiliw.d.ts} +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Test, automate, and verify every wallet interaction, with the precision your use
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
Chainwright is an end-to-end testing toolkit for Web3 dapps built on
|
|
19
|
+
Chainwright is an end-to-end testing toolkit for Web3 dapps built on Playwright. It helps you build and cache browser wallet extension state, then reuse it in your end-to-end tests through ready-made fixtures.
|
|
20
20
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
@@ -24,45 +24,52 @@ Chainwright is an end-to-end testing toolkit for Web3 dapps built on top of Play
|
|
|
24
24
|
- Playwright fixtures for wallet + Dapp testing
|
|
25
25
|
- Support for multiple wallet profiles per wallet
|
|
26
26
|
- Wallet action APIs for onboarding, account switching, transaction confirmation, and more
|
|
27
|
-
- Multiple wallet profile caching
|
|
28
27
|
|
|
29
28
|
## Supported Wallets
|
|
30
29
|
|
|
30
|
+
- Keplr
|
|
31
31
|
- MetaMask
|
|
32
|
-
-
|
|
32
|
+
- Meteor
|
|
33
33
|
- Petra
|
|
34
34
|
- Phantom
|
|
35
|
-
-
|
|
36
|
-
- Keplr
|
|
35
|
+
- Solflare
|
|
37
36
|
|
|
38
37
|
## Requirements
|
|
39
38
|
|
|
40
39
|
- Node.js `>=22`
|
|
41
40
|
- `@playwright/test@1.60.0` (peer dependency)
|
|
42
41
|
|
|
42
|
+
## Operating Systems
|
|
43
|
+
Supports the following operating systems:
|
|
44
|
+
- MacOS
|
|
45
|
+
- Linux
|
|
46
|
+
- Windows
|
|
47
|
+
|
|
43
48
|
## Installation
|
|
44
49
|
|
|
50
|
+
Before installing Chainwright, ensure to install Playwright's browser using the command below.
|
|
51
|
+
|
|
45
52
|
```bash
|
|
46
|
-
|
|
53
|
+
npx playwright install chromium
|
|
47
54
|
```
|
|
55
|
+
|
|
48
56
|
```bash
|
|
49
|
-
|
|
57
|
+
bunx playwright install chromium
|
|
50
58
|
```
|
|
59
|
+
|
|
60
|
+
After Installing Playwright's browser, install `Chainwright` and `@playwright/test`
|
|
61
|
+
|
|
51
62
|
```bash
|
|
52
|
-
|
|
63
|
+
pnpm add -D chainwright @playwright/test
|
|
53
64
|
```
|
|
54
65
|
```bash
|
|
55
|
-
|
|
66
|
+
bun add -D chainwright @playwright/test
|
|
56
67
|
```
|
|
57
|
-
|
|
58
|
-
After installation, make sure Playwright browsers are installed in your machine.
|
|
59
|
-
|
|
60
68
|
```bash
|
|
61
|
-
|
|
69
|
+
npm install --save-dev chainwright @playwright/test
|
|
62
70
|
```
|
|
63
|
-
|
|
64
71
|
```bash
|
|
65
|
-
|
|
72
|
+
yarn add -D chainwright @playwright/test
|
|
66
73
|
```
|
|
67
74
|
|
|
68
75
|
## Quick Start
|
|
@@ -71,19 +78,20 @@ bunx playwright install --chromium #Optional
|
|
|
71
78
|
|
|
72
79
|
Create a setup directory (default: `tests/wallet-setup`) and add `*.setup.ts` files with a wallet name in the filename, for example:
|
|
73
80
|
|
|
74
|
-
- `
|
|
81
|
+
- `base.setup.ts`
|
|
82
|
+
- `base-two.setup.ts`
|
|
75
83
|
- `petra.setup.ts`
|
|
76
84
|
- `phantom-team-a.setup.ts`
|
|
77
85
|
|
|
78
|
-
Each file
|
|
86
|
+
Each file must export `default defineWalletSetup(...)`.
|
|
79
87
|
|
|
80
88
|
```ts
|
|
81
89
|
// tests/wallet-setup/metamask.setup.ts
|
|
82
90
|
import { defineWalletSetup } from "chainwright/core";
|
|
83
91
|
import { Metamask } from "chainwright/metamask";
|
|
84
92
|
|
|
85
|
-
const PASSWORD = "test1234";
|
|
86
|
-
const SEED_PHRASE = "
|
|
93
|
+
const PASSWORD = "test1234"; // For Petra wallet, you have to use a strong password. e.g. PlayerPetra45!!
|
|
94
|
+
const SEED_PHRASE = "test test test test test test test test test test test test test";
|
|
87
95
|
|
|
88
96
|
export default defineWalletSetup(
|
|
89
97
|
PASSWORD,
|
|
@@ -97,27 +105,109 @@ export default defineWalletSetup(
|
|
|
97
105
|
});
|
|
98
106
|
},
|
|
99
107
|
{
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
...//Optional prarmeters here
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**For Wallets with additional accounts**
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// tests/wallet-setup/metamask.setup.ts
|
|
117
|
+
import { defineWalletSetup } from "chainwright/core";
|
|
118
|
+
import { Petra } from "chainwright/petra";
|
|
119
|
+
|
|
120
|
+
const PASSWORD = "PlayerPetra45!!";
|
|
121
|
+
|
|
122
|
+
export default defineWalletSetup(
|
|
123
|
+
PASSWORD,
|
|
124
|
+
async ({ walletPage }) => {
|
|
125
|
+
const petra = new Petra(walletPage);
|
|
126
|
+
|
|
127
|
+
await petra.onboard({
|
|
128
|
+
mode: "importMnemonic",
|
|
129
|
+
accountName: "default",
|
|
130
|
+
secretRecoveryPhrase: "test test test...", // Seed phrase for the main account
|
|
131
|
+
additionalAccounts: [
|
|
132
|
+
{
|
|
133
|
+
accountName: "nw-account",
|
|
134
|
+
mode: "mnemonic",
|
|
135
|
+
mnemonicPhrase: "test test test..." // Seed Phrase for this account
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
...//Optional prarmeters here
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
To support multiple profiles in a single wallet (for example, MetaMask), only setup files from the second profile onward need an explicit, distinct profile name.
|
|
147
|
+
|
|
148
|
+
`main.setup.ts` can use the default profile, while `main-two.setup.ts` (and any additional setup files) should declare a unique profile name. Then, in any fixture that should use that profile, pass the exact same profileName value.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
- `main.setup.ts`: uses the default profile
|
|
152
|
+
- `main-two.setup.ts`: defines `profileName: "profile two"`
|
|
153
|
+
- Fixture usage: `metamaskFixture({ profileName: "profile two" })`
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// tests/wallet-setup/main-two.setup.ts
|
|
157
|
+
import { defineWalletSetup } from "chainwright/core";
|
|
158
|
+
import { Metamask } from "chainwright/metamask";
|
|
159
|
+
|
|
160
|
+
const PASSWORD = "test1234"; // For Petra wallet, you have to use a strong password. e.g. PlayerPetra45!!
|
|
161
|
+
const SEED_PHRASE = "test test test test test test test test test test test test test";
|
|
162
|
+
|
|
163
|
+
export default defineWalletSetup(
|
|
164
|
+
PASSWORD,
|
|
165
|
+
async ({ walletPage }) => {
|
|
166
|
+
const metamask = new Metamask(walletPage);
|
|
167
|
+
|
|
168
|
+
await metamask.onboard({
|
|
169
|
+
mode: "import",
|
|
170
|
+
secretRecoveryPhrase: SEED_PHRASE,
|
|
171
|
+
mainAccountName: "Main",
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
profileName: "profile two"
|
|
102
176
|
},
|
|
103
177
|
);
|
|
104
178
|
```
|
|
105
179
|
|
|
106
180
|
### 2. Build wallet cache
|
|
107
181
|
|
|
108
|
-
Run setup with the CLI:
|
|
182
|
+
Run setup with the CLI (Supports **npx**, **bun**, **pnpm**, and **yarn**):
|
|
183
|
+
|
|
184
|
+
>[!NOTE]
|
|
185
|
+
> By default, Chainwright looks for `tests/wallet-setup` in your base directory. However, you can specify the directory you want Chainwright to get your setup files from.
|
|
109
186
|
|
|
110
187
|
```bash
|
|
111
|
-
chainwright
|
|
188
|
+
bun chainwright --wallets <Wallets you want to support>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
To specify a directory:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
bun chainwright <directory path> <wallet> -f #Optional flag
|
|
112
195
|
```
|
|
113
196
|
|
|
114
197
|
Useful flags:
|
|
115
198
|
|
|
199
|
+
- `-h, --help` shows you all the commands
|
|
116
200
|
- `-f, --force` overwrite existing cache
|
|
117
|
-
- `--wallets <wallets...>` select wallets (`metamask`, `solflare`, `petra`, `phantom`, `meteor`, `keplr`)
|
|
201
|
+
- `--wallets <wallets...>` select wallets (`metamask`, `solflare`, `petra`, `phantom`, `meteor`, `keplr`). Setup multiple wallets at the same time.
|
|
118
202
|
- `-a, --all` setup all wallets
|
|
203
|
+
- `--kp, --keplr` setup keplr wallet
|
|
204
|
+
- `-m, --metamask` setup metamask wallet
|
|
205
|
+
- `--mt, --meteor` setup the meteor wallet
|
|
206
|
+
- `--pt, --petra` setup petra wallet
|
|
207
|
+
- `--ph, --phantom` setup phantom wallet
|
|
208
|
+
- `-s, --solflare` setup solflare wallet
|
|
119
209
|
|
|
120
|
-
|
|
210
|
+
Wallet profile cache is stored under:
|
|
121
211
|
|
|
122
212
|
- `.wallet-cache/<wallet>/wallet-data` (default profile)
|
|
123
213
|
- `.wallet-cache/<wallet>/<profileName>` (custom profile)
|
|
@@ -125,48 +215,158 @@ Cache is stored under:
|
|
|
125
215
|
### 3. Use wallet fixtures in Playwright tests
|
|
126
216
|
|
|
127
217
|
```ts
|
|
128
|
-
import { expect } from "@playwright/test";
|
|
218
|
+
import { expect, type Page } from "@playwright/test";
|
|
129
219
|
import { testWithChainwright } from "chainwright/core";
|
|
130
220
|
import { metamaskFixture } from "chainwright/metamask";
|
|
131
221
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
profileName: "default",
|
|
135
|
-
}),
|
|
136
|
-
);
|
|
222
|
+
// Chainwright's Fixture
|
|
223
|
+
export const testWithMetamask = testWithChainwright(metamaskFixture());
|
|
137
224
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
225
|
+
// Extend Chainwright's metamaskFixture to suit your need
|
|
226
|
+
export const testDappFixture = testWithMetamask.extend<{dAppPage: Page}>({
|
|
227
|
+
dappPage: async ({ page, baseURL }, use) => {
|
|
228
|
+
await page.goto(`https://your-dapp.example`);
|
|
229
|
+
await use(page);
|
|
230
|
+
},
|
|
142
231
|
});
|
|
232
|
+
|
|
233
|
+
// Then in your tests do:
|
|
234
|
+
const test = testDappFixture;
|
|
235
|
+
test.describe("Example tests", () => {
|
|
236
|
+
test("connect wallet to dapp", async ({ dappPage, metamask }) => {
|
|
237
|
+
const connectButton = dappPage.getByRole("button", { name: /Connect/i})
|
|
238
|
+
await connectButton.click();
|
|
239
|
+
await metamask.connectToApp("Account 1");
|
|
240
|
+
await expect(dappPage.getByText("Connected")).toBeVisible();
|
|
241
|
+
});
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
> [!NOTE]
|
|
245
|
+
> The wallet fixture will make use of the `default` wallet profile. If you specified a `profile-name` at the point of setting up, make sure to include it in the fixture.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// No profile name is specified at setup time
|
|
249
|
+
const testWithFixture = testWithChainwright(fixture())
|
|
250
|
+
|
|
251
|
+
// If a profile name is specified at setup time.
|
|
252
|
+
const testWithFixture = testWithChainwright(fixture({ profileName: "profile name" }))
|
|
143
253
|
```
|
|
144
254
|
|
|
255
|
+
`Wallet fixture parameters`:
|
|
256
|
+
- `profileName`?: string,
|
|
257
|
+
- `slowMo`?: number
|
|
258
|
+
|
|
145
259
|
## Worker-Scoped Fixture
|
|
146
260
|
|
|
147
|
-
Use worker-scoped fixtures when
|
|
261
|
+
Use worker-scoped fixtures when tests in a `test.describe()` block can safely share a wallet context. Setup and teardown run once per worker instead of per test, which speeds up CI runs and reduces flakiness caused by repeated wallet initialization.
|
|
148
262
|
|
|
149
263
|
```ts
|
|
264
|
+
import { type Page } from "@playwright/test";
|
|
265
|
+
import { testWithChainwright } from "chainwright/core";
|
|
150
266
|
import { metamaskWorkerScopeFixture } from "chainwright/metamask";
|
|
151
267
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
268
|
+
// Chainwright's worker scoped fixture
|
|
269
|
+
export const testWithFixture = testWithChainwright(metamaskWorkerScopeFixture());
|
|
270
|
+
|
|
271
|
+
// Your worker scoped fixture that extends Chainwright's worker scoped fixture
|
|
272
|
+
export const workerScopedFixture = testWithFixture.extend<{dAppPage: Page}>({
|
|
273
|
+
dappPage: [
|
|
274
|
+
async ({ workerScopeContents }, use) => {
|
|
275
|
+
const { context, wallet, walletPage } = workerScopeContents;
|
|
276
|
+
/** N.B:
|
|
277
|
+
* wallet represents -> metamask, phantom, keplr, etc...
|
|
278
|
+
* walletPage represents -> metamask wallet page, phantom wallet page, keplr wallet page, etc...
|
|
279
|
+
*/
|
|
280
|
+
const _dappPage = await context.newPage();
|
|
281
|
+
await _dappPage.goto(`http://example-site.com`);
|
|
282
|
+
await use(_dappPage);
|
|
283
|
+
},
|
|
284
|
+
{ scope: "worker" },
|
|
285
|
+
],
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// Then in your tests do:
|
|
289
|
+
const test = workerScopedFixture;
|
|
290
|
+
|
|
291
|
+
test.describe("Example test", () => {
|
|
292
|
+
test("Should confirm transaction", ({ dappPage, workerScopeContents}) => {
|
|
293
|
+
const { wallet: metamask } = workerScopeContents
|
|
294
|
+
await dappPage.getByRole("button", { name: "Send Tx" }).click();
|
|
295
|
+
await metamask.confirmTransaction();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("Should reject transaction", async ({ dappPage, workerScopeContents })=> {
|
|
299
|
+
const { wallet: metamask } = workerScopeContents
|
|
300
|
+
await dappPage.getByRole("button", { name: "Send Tx" }).click();
|
|
301
|
+
await metamask.rejectTransaction();
|
|
302
|
+
});
|
|
303
|
+
})
|
|
156
304
|
|
|
157
|
-
test("confirm transaction", async ({ dappPage, metamask }) => {
|
|
158
|
-
await dappPage.getByRole("button", { name: "Send Tx" }).click();
|
|
159
|
-
await metamask.confirmTransaction();
|
|
160
|
-
});
|
|
161
305
|
```
|
|
162
306
|
|
|
163
|
-
`
|
|
307
|
+
`Worker scoped fixture parameters`:
|
|
164
308
|
|
|
165
309
|
- `profileName?: string`
|
|
166
310
|
- `slowMo?: number`
|
|
167
|
-
- `dappUrl?: string`
|
|
168
311
|
|
|
169
|
-
|
|
312
|
+
### 4. Using in CI (GitHub Actions)
|
|
313
|
+
Running Chainwright in CI is very similar to running Playwright in CI. The only additional requirement is a cache-build step before executing tests, as shown below:
|
|
314
|
+
|
|
315
|
+
Why we make use of **xvfb**:
|
|
316
|
+
> [!IMPORTANT]
|
|
317
|
+
> Browser extensions don't load in headless Chromium, so the tests have to run in headed mode. CI machines have no display, so launching a headed browser fails. xvfb provides a fake virtual display, letting Chromium run headed in CI as if a screen were attached.
|
|
318
|
+
|
|
319
|
+
```yml
|
|
320
|
+
name: CI
|
|
321
|
+
|
|
322
|
+
on:
|
|
323
|
+
workflow_dispatch:
|
|
324
|
+
pull_request:
|
|
325
|
+
branches: ["main"]
|
|
326
|
+
|
|
327
|
+
jobs:
|
|
328
|
+
test:
|
|
329
|
+
runs-on: ubuntu-22.04
|
|
330
|
+
timeout-minutes: 60
|
|
331
|
+
|
|
332
|
+
steps:
|
|
333
|
+
- name: Checkout code
|
|
334
|
+
uses: actions/checkout@v5
|
|
335
|
+
with:
|
|
336
|
+
submodules: "recursive"
|
|
337
|
+
fetch-depth: 0
|
|
338
|
+
|
|
339
|
+
- name: Install pnpm
|
|
340
|
+
uses: pnpm/action-setup@v5
|
|
341
|
+
with:
|
|
342
|
+
version: 10
|
|
343
|
+
|
|
344
|
+
- name: Use Node LTS
|
|
345
|
+
uses: actions/setup-node@v6
|
|
346
|
+
with:
|
|
347
|
+
node-version: 24.x
|
|
348
|
+
cache: "pnpm"
|
|
349
|
+
|
|
350
|
+
- name: Install dependencies
|
|
351
|
+
run: pnpm install
|
|
352
|
+
|
|
353
|
+
- name: Install XVFB
|
|
354
|
+
run: sudo apt-get install -y xvfb
|
|
355
|
+
|
|
356
|
+
- name: Install Playwright browsers
|
|
357
|
+
run: pnpx playwright install chromium
|
|
358
|
+
|
|
359
|
+
- name: Install Foundry
|
|
360
|
+
uses: foundry-rs/foundry-toolchain@v1
|
|
361
|
+
|
|
362
|
+
- name: Build cache
|
|
363
|
+
run: xvfb-run pnpm run setup-wallets
|
|
364
|
+
|
|
365
|
+
- name: Run end-to-end tests (Headful)
|
|
366
|
+
run: xvfb-run pnpm playwright test --config=tests/playwright.config.ts
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Wallets By Module
|
|
170
370
|
|
|
171
371
|
Each wallet module exports:
|
|
172
372
|
|
|
@@ -200,9 +400,9 @@ Extra Phantom/Solflare fixtures:
|
|
|
200
400
|
defineWalletSetup(password, setupFn, config?)
|
|
201
401
|
```
|
|
202
402
|
|
|
203
|
-
- `password: string` wallet unlock password saved in cache metadata
|
|
204
|
-
- `setupFn: ({ context, walletPage }) => Promise<void>` runs onboarding/import flow
|
|
205
|
-
- `config?: { profileName?: string; slowMo?: number }`
|
|
403
|
+
- `password: string` - wallet unlock password saved in cache metadata
|
|
404
|
+
- `setupFn: ({ context, walletPage }) => Promise<void>` - runs onboarding/import flow
|
|
405
|
+
- `config?: { profileName?: string; slowMo?: number }` - useful for setting up multiple wallet profiles and running the setup in slow motion `slowMo`.
|
|
206
406
|
|
|
207
407
|
### `testWithChainwright`
|
|
208
408
|
|
|
@@ -234,12 +434,6 @@ Additional wallet-specific actions are available, for example:
|
|
|
234
434
|
- Petra/Solflare/Meteor: `switchNetwork`
|
|
235
435
|
- Meteor: `openSettings`
|
|
236
436
|
|
|
237
|
-
## Troubleshooting
|
|
238
|
-
|
|
239
|
-
- `Cache for <wallet> ... not found`: run `chainwright` setup first.
|
|
240
|
-
- Setup file not detected: ensure file matches `*.setup.ts` or `*.setup.js` and includes a wallet name.
|
|
241
|
-
- Existing profile conflict: use `--force` or a unique `profileName`.
|
|
242
|
-
|
|
243
437
|
## License
|
|
244
438
|
|
|
245
439
|
MIT
|
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --import tsx
|
|
2
|
-
import
|
|
3
|
-
${a}`,{encoding:"utf-8"})}catch(l){console.error("Error appending new profile name type: ",l)}}else try{
|
|
4
|
-
`));return await Promise.all(s.map(async({filePath:i,walletName:p})=>{let f=await(await
|
|
5
|
-
`));return await t.close(),n.id}import v from"fs";import
|
|
2
|
+
import z from"path";import{styleText as g}from"util";import Fe from"@inquirer/checkbox";import{Command as Te}from"commander";import $ from"fs";import H from"path";import{fileURLToPath as te}from"url";var oe=te(import.meta.url),re=H.dirname(oe),P=H.resolve(re,"..","..","generated-profile-name.types.ts"),ae=e=>e.replace(/(^\w)/g,o=>o.toUpperCase());async function G({walletName:e,profileName:o}){let t=ae(e),a=`export type ${t}Profiles = "${o}";`;if($.existsSync(P)){let r=$.readFileSync(P,"utf-8"),n=r.match(new RegExp(`export type ${t}Profiles = ("[^"]+"(?:\\s*\\|\\s*"[^"]+")*)`));if(n){let l=n[0];if(!l.includes(`"${o}"`)){let s=l.concat(` | "${o}"`),c=r.replace(l,s);try{$.writeFileSync(P,c)}catch(i){console.error("Error updating existing profile name type: ",i)}}}else try{$.appendFileSync(P,`
|
|
3
|
+
${a}`,{encoding:"utf-8"})}catch(l){console.error("Error appending new profile name type: ",l)}}else try{$.writeFileSync(P,a)}catch(r){console.error("Error writing new profile name type: ",r)}}import se from"path";import{pathToFileURL as ie}from"url";import{styleText as le}from"util";import{glob as pe}from"glob";import ne from"path";function I(e){let o=ne.basename(e),t=o.match(/^([a-z0-9_]+)(?:-[a-z0-9_]+)*\.setup\.(?:ts|js|mjs)$/i);if(!t)throw new Error(`Invalid wallet setup filename: ${o} (expected "<name>[ -variant].setup.{ts,js,mjs}")`);return t[1]}var ce=e=>e.replace(/\\/g,"/"),me=e=>`${ce(se.resolve(e))}/**/*.setup.{ts,js,}`,fe=e=>import(new URL(ie(e)).href);async function N({walletSetupDir:e,selectedWallets:o}){let t=me(e),a=(await pe(t,{dot:!0,absolute:!0,nodir:!0,windowsPathsNoEscape:!0})).sort(),r=o.length===1?o[0]:o,n=["metamask","solflare","petra","meteor","keplr","phantom"];Array.isArray(r)&&r.forEach(i=>{n.includes(i)||console.warn(le("magenta",`Unsupported wallet: "${i}". Supported wallets are: ${n.join(", ")}`,{validateStream:!1}))});let s=(r==="all"?a:Array.isArray(r)?a.filter(i=>r.some(p=>i.includes(p))):a.filter(i=>i.includes(r))).map(i=>({filePath:i,walletName:I(i)}));if(!s.length||s.length===0)throw new Error([`No wallet setup file found at ${e} for wallet: "${o}".`,'Setup files must use a ".setup.{ts,js,mjs}" extension and include a valid wallet name.','Examples: "metamask.setup.ts", "solflare.setup.ts", "phantom.setup.ts", "metamask-connected.setup.ts"'].join(`
|
|
4
|
+
`));return await Promise.all(s.map(async({filePath:i,walletName:p})=>{let f=await(await fe(i)).default,{fn:E,config:h,password:w}=f;return{walletName:p,fileList:s,config:h,walletPassword:w,setupFunction:E}}))}import x from"fs";import R from"path";import{styleText as S}from"util";import{chromium as be}from"@playwright/test";import xe from"path";var V=".wallet-cache";var X="wallet-setup",K="13.22.0",C="https://github.com/amaify/chainwright/releases/download/v0.1.0/",ue=`https://github.com/MetaMask/metamask-extension/releases/download/v${K}/metamask-chrome-${K}.zip`,de=`${C}solflare-wallet-extension-v2.19.1.zip`,we=`${C}petra-wallet-extension-v2.4.8.zip`,ge=`${C}phantom-wallet-extension-v26.10.0.zip`,he=`${C}meteor-wallet-extension-v0.7.0.zip`,ye=`${C}keplr-wallet-extension-v0.13.3.zip`,T={metamask:{downloadUrl:ue,extensionName:"MetaMask"},solflare:{downloadUrl:de,extensionName:"Solflare Wallet"},petra:{downloadUrl:we,extensionName:"Petra Aptos Wallet"},phantom:{downloadUrl:ge,extensionName:"Phantom"},meteor:{downloadUrl:he,extensionName:"Meteor Wallet"},keplr:{downloadUrl:ye,extensionName:"Keplr"}};function W(e){return xe.resolve(process.cwd(),V,e)}import{z as D}from"zod";var Se=D.object({id:D.string(),name:D.string()}),Ee=D.array(Se);async function Y(e,o){let t=await e.newPage();await t.goto("chrome://extensions");let a=await t.evaluate("chrome.management.getAll()"),r=Ee.parse(a),n=r.find(l=>l.name.toLowerCase()===o.toLowerCase());if(!n)throw new Error([`[GetExtensionId] Extension with name ${o} not found.`,`Available extensions: ${r.map(l=>l.name).join(", ")}`].join(`
|
|
5
|
+
`));return await t.close(),n.id}import v from"fs";import U from"path";import{styleText as _}from"util";import Pe from"adm-zip";import{createWriteStream as Ae}from"fs";import{Readable as ve}from"stream";import{styleText as k}from"util";import Le from"cli-progress";var $e=12e4;async function q({url:e,destination:o}){let t=new AbortController,a=setTimeout(()=>t.abort(),$e),r=await fetch(e,{redirect:"follow",signal:t.signal});r.ok||(console.error(k("redBright",`\u274C Download failed: HTTP ${r.status}`,{validateStream:!1})),t.abort(),process.exit(1));let n=parseInt(r.headers.get("content-length")||"0",10),l=0,s=ve.fromWeb(r.body);try{let c=new Le.SingleBar({format:`Downloading ${k("cyan","{bar}",{validateStream:!1})} {percentage}%`,clearOnComplete:!0,barCompleteChar:"\u2588",barIncompleteChar:"\u2591",hideCursor:!0});c.start(n,0,{speed:"N/A"}),await new Promise((i,p)=>{let m=Ae(o);s.pipe(m),s.on("data",f=>{l+=f.length,c.update(l)}),m.on("error",p),m.on("finish",()=>{c.stop(),i(void 0)})})}catch(c){console.error(k("redBright",`\u274C Download failed: ${c}`,{validateStream:!1})),process.exit(1)}finally{clearTimeout(a)}}var L=!1,M="";async function Z({downloadUrl:e,name:o,force:t}){let a=W(o),n=T[o].extensionName,l=U.join(a,`${o}-extension.zip`),s=U.join(a,`${o}-extension`);if(L?M!==n&&L&&(L=!1,M=n):M=n,t&&v.existsSync(a)&&!L&&(v.rmSync(a,{recursive:!0}),console.info(_("magenta",`\u{1F9F9} Removed ${n} because of the force flag`,{validateStream:!1}))),v.existsSync(a)||v.mkdir(a,{recursive:!0},i=>{if(i)throw Error("Failed to create cache directory");console.info(`\u2705 ${n} Cache directory created successfully.`)}),v.existsSync(s)?console.info(`\u2705 ${n} Version is downloaded already.`):(console.info(_("cyanBright",`\u{1F4E5} Downloading ${n} extension...`,{validateStream:!1})),await q({url:e,destination:l}),console.info(_("green",`\u2705 ${o.toUpperCase()} Extension downloaded successfully.`,{validateStream:!1}))),!v.existsSync(s))console.info("\u{1F4E6} Extracting extension..."),new Pe(l).extractAllTo(s,!0),console.info(`\u2705 ${n} Extension extracted successfully.`);else{if(L)return console.info(_("magentaBright",`Using the cached ${n} extension for profile creation.`,{validateStream:!1})),s;console.info(_("yellow",`\u26A0\uFE0F Skipping ${n} cache creation: Cache already exists at ${s}. Use --force to overwrite.`,{validateStream:!1}))}let c=U.join(s,"manifest.json");if(!v.existsSync(c))throw new Error(`\u274C (${n}) Invalid extension: manifest.json not found`);return L=!0,s}import{styleText as Ce}from"util";function B(e){return new Promise(o=>setTimeout(o,e))}var O=20,We=6e3,J=4e3;async function _e(e){return e.pages().find(a=>{try{return a.url().startsWith("chrome-extension://")}catch(r){return console.error("[WaitForExtensionOnLoadPage] Error checking page URL:",r),!1}})}async function Q(e,o){let t=0,a=null;if(o==="meteor")return await e.newPage();for(console.info(`Waiting ${J}ms for browser to initialize...`),await B(J);t<=O;)try{if(console.info(`Looking for extension page (attempt ${t+1}/${O})...`),t===O)throw new Error("Extension page not found after maximum retries");let r=await _e(e);if(r){console.info(`Found extension page after ${t+1} polling attempts`),a=r;break}r||(t++,console.info(`Extension page not found, retrying (${t}/${O})...`),await B(We))}catch(r){throw console.error("Error waiting for extension page:",r instanceof Error?r.message:"Unknown error"),new Error(`Extension failed to load properly after ${t} attempts!`)}return console.info(Ce("greenBright","\u2705 Extension page is properly loaded and ready",{validateStream:!1})),a}async function j({walletName:e,force:o,config:t,fileList:a,walletPassword:r,setupFunction:n}){let{downloadUrl:l,extensionName:s}=T[e],c=W(e),i=t?.profileName,p=i?`${i}`:"wallet-data",m=R.resolve(c,"extension-id.txt"),f=R.resolve(c,"extension-path.txt"),E=R.resolve(c,"password.txt"),h=R.resolve(c,p),w=await Z({downloadUrl:l,name:e,force:o}),y=[`--disable-extensions-except=${w}`,`--load-extension=${w}`];if(x.existsSync(h)&&a.length>1)throw Error([S("yellowBright",[`\u274C ${p} directory already exists for ${s}.`,`
|
|
6
6
|
To setup another wallet profile, add a profile name to the wallet setup function.`,S(["blueBright","italic"],'Example: defineWalletSetup(async ({ context, walletPage }) => { ... }, { profileName: "profile-name" });'),S("italic","You can also use the --force flag to overwrite the existing cache.")].join(`
|
|
7
7
|
`),{validateStream:!1})].join(`
|
|
8
|
-
`));if(x.existsSync(h))return;let A=await
|
|
9
|
-
Setting up cache for ${p}...`,{validateStream:!1})),await
|
|
10
|
-
\u274C Failed to setup cache for ${p}: ${w.message}`,{validateStream:!1}));let y=0;for(;
|
|
11
|
-
\u274C Attempt ${y+1} failed! Retrying...`,{validateStream:!1})),y++,y===
|
|
8
|
+
`));if(x.existsSync(h))return;let A=await be.launchPersistentContext(h,{headless:!1,args:y,slowMo:t?.slowMo??0});console.info(S("magentaBright",`\u{1F9E9}\u{1F680} Starting Chrome extension for ${e.toUpperCase()}`,{validateStream:!1}));let ee=await Q(A,e);if(!x.existsSync(m)&&!x.existsSync(f)){let F=await Y(A,s);console.info(S("magentaBright",`\u{1F194} ${s} extension ID: ${F}`,{validateStream:!1})),x.writeFileSync(m,F,"utf-8"),console.info(S("cyanBright",`\u{1F4BE} Saved extension ID to: ${m}`,{validateStream:!1})),x.writeFileSync(f,w,"utf-8"),console.info(S("blueBright",`\u{1F4C1} Saved extension Path to: ${f}`,{validateStream:!1})),x.writeFileSync(E,r,"utf-8"),console.info(S("yellowBright",`\u{1F511} Saved ${e} password to: ${E}`,{validateStream:!1}))}try{await n({context:A,walletPage:ee})}catch(F){await A.close(),x.rmSync(c,{force:!0,recursive:!0}),console.error("Error setting up wallet: ",F.message)}await A.close()}var De=z.resolve(process.cwd(),"tests",X),b=2;async function Oe(){let e=new Te;e.name(g("yellow","Chainwright")).description(g("green","A CLI tool for setting up wallet cache for E2E testing of web3 applications")).version(g("blue","0.0.0")),e.argument("[dir]","Directory containing the wallet setup functions",z.resolve(De)).option("--headless","Build cache in the headless browser mode. Alternatively, set the `HEADLESS` env variable to `true`",!1).option("-f, --force","Force the creation of cache even if it already exists",!1).option("-a, --all","Setup all wallets","all").option("--kp, --keplr","Setup Keplr","keplr").option("-m, --metamask","Setup MetaMask","metamask").option("--mt, --meteor","Setup Meteor","meteor").option("--pt, --petra","Setup Petra","petra").option("--ph, --phantom","Setup Phantom","phantom").option("-s, --solflare","Setup Solflare","solflare").option("--wls, --wallets <wallets...>","Specify wallets to setup (e.g., --wallets keplr metamask)").action(async(o,t)=>{let a=["all","metamask","solflare","petra","meteor","keplr","phantom"],r=Object.keys(t).filter(p=>a.includes(p)?t[p]===!0:!1),n=r.length>0,l=t.wallets,s=l||(n?r:await Fe({message:"Select the wallet you want to setup",choices:[{name:"All",value:"all"},{name:"Keplr",value:"keplr"},{name:"MetaMask",value:"metamask"},{name:"Meteor",value:"meteor"},{name:"Petra",value:"petra"},{name:"Phantom",value:"phantom"},{name:"Solflare",value:"solflare"}],validate:p=>{let m=p.map(f=>f.value);return m.includes("all")&&m.length>1?'Select either "All" or specific wallets, not both.':!0},pageSize:10})),c=z.resolve(process.cwd(),o);t.headless&&(process.env.HEADLESS=!0);let i=await N({walletSetupDir:c,selectedWallets:s});for(let{walletName:p,config:m,walletPassword:f,setupFunction:E,fileList:h}of i)try{console.info(g("cyanBright",`
|
|
9
|
+
Setting up cache for ${p}...`,{validateStream:!1})),await j({walletName:p,config:m,setupFunction:E,fileList:h,force:t.force,walletPassword:f}),m.profileName&&await G({walletName:p,profileName:m.profileName})}catch(w){if(w.message.includes("directory already exists")&&console.warn(w.message),!w.message.includes("directory already exists")){console.error(g("redBright",`
|
|
10
|
+
\u274C Failed to setup cache for ${p}: ${w.message}`,{validateStream:!1}));let y=0;for(;b>y;){console.info(`${g("yellow",`Retry Attempt ${y+1} of ${b} for ${p}...`,{validateStream:!1})}`),console.info(g("yellow","Retrying wallet setup...",{validateStream:!1}));try{await j({walletName:p,config:m,setupFunction:E,fileList:h,force:t.force,walletPassword:f});break}catch(A){y+1<b&&console.error(g("redBright",`
|
|
11
|
+
\u274C Attempt ${y+1} failed! Retrying...`,{validateStream:!1})),y++,y===b&&console.error(g("redBright",`\u274C Failed to setup cache after ${b} attempts for ${p}: ${A.message}`,{validateStream:!1}))}}}}}),await e.parseAsync(process.argv)}Oe().catch(e=>console.error(g("redBright",`Failed to run the CLI: ${e.message})`,{validateStream:!1})));export{Oe as clientEntry};
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as WalletSetupFunction, a as WalletSetupConfig } from '../types-
|
|
1
|
+
import { b as WalletSetupFunction, a as WalletSetupConfig } from '../types-DK8rutb5.js';
|
|
2
2
|
import { Fixtures, TestType } from '@playwright/test';
|
|
3
3
|
|
|
4
4
|
declare function defineWalletSetup(password: string, fn: WalletSetupFunction, config?: WalletSetupConfig): Promise<{
|
|
@@ -11,14 +11,9 @@ type WalletSetupConfig = {
|
|
|
11
11
|
profileName?: string;
|
|
12
12
|
slowMo?: number;
|
|
13
13
|
};
|
|
14
|
-
type WorkerScopeFixtureArgs = {
|
|
15
|
-
slowMo?: number;
|
|
16
|
-
profileName?: string;
|
|
17
|
-
dappUrl?: string;
|
|
18
|
-
};
|
|
19
14
|
type WalletProfileFixtureArgs = {
|
|
20
15
|
slowMo?: number;
|
|
21
16
|
profileName?: string;
|
|
22
17
|
};
|
|
23
18
|
|
|
24
|
-
export type { SupportedWallets as S, WalletProfileFixtureArgs as W, WalletSetupConfig as a, WalletSetupFunction as b
|
|
19
|
+
export type { SupportedWallets as S, WalletProfileFixtureArgs as W, WalletSetupConfig as a, WalletSetupFunction as b };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { W as WalletProfileFixtureArgs
|
|
2
|
-
import { W as WorkerScopeFixture } from '../../worker-scope-context-
|
|
3
|
-
export { w as workerScopeContext } from '../../worker-scope-context-
|
|
1
|
+
import { W as WalletProfileFixtureArgs } from '../../types-DK8rutb5.js';
|
|
2
|
+
import { W as WorkerScopeFixture } from '../../worker-scope-context-CNAfiliw.js';
|
|
3
|
+
export { w as workerScopeContext } from '../../worker-scope-context-CNAfiliw.js';
|
|
4
4
|
import * as _playwright_test from '@playwright/test';
|
|
5
5
|
import { BrowserContext, Page } from '@playwright/test';
|
|
6
6
|
import z from 'zod';
|
|
@@ -148,6 +148,6 @@ declare class Keplr extends KeplrProfile {
|
|
|
148
148
|
|
|
149
149
|
declare const keplrFixture: ({ slowMo, profileName }?: WalletProfileFixtureArgs) => _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & KeplrFixture, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
|
|
150
150
|
|
|
151
|
-
declare const keplrWorkerScopeFixture: ({ slowMo, profileName
|
|
151
|
+
declare const keplrWorkerScopeFixture: ({ slowMo, profileName }?: WalletProfileFixtureArgs) => _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & KeplrFixture, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions & WorkerScopeFixture<Keplr>>;
|
|
152
152
|
|
|
153
|
-
export { Keplr, WalletProfileFixtureArgs, WorkerScopeFixture,
|
|
153
|
+
export { Keplr, WalletProfileFixtureArgs, WorkerScopeFixture, keplrFixture, keplrWorkerScopeFixture };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import Z from"fs";import vt from"path";import{chromium as bt}from"@playwright/test";import yt from"path";var Y=".wallet-cache",q=".wallet-context";var J="13.22.0",_="https://github.com/amaify/chainwright/releases/download/v0.1.0/",Kt=`https://github.com/MetaMask/metamask-extension/releases/download/v${J}/metamask-chrome-${J}.zip`,Ut=`${_}solflare-wallet-extension-v2.19.1.zip`,Mt=`${_}petra-wallet-extension-v2.4.8.zip`,Vt=`${_}phantom-wallet-extension-v26.10.0.zip`,Ht=`${_}meteor-wallet-extension-v0.7.0.zip`,jt=`${_}keplr-wallet-extension-v0.13.3.zip`;async function N(t){return yt.resolve(process.cwd(),q,t)}import At from"path";function w(t){return At.resolve(process.cwd(),Y,t)}import Q from"fs";import Pt from"path";async function R(t){try{let e=w(t),o=Pt.resolve(e,"extension-path.txt");if(!Q.existsSync(o))throw new Error("\u274C extension-path.txt not found. Run setup script first.");let a=Q.readFileSync(o,"utf-8").trim();if(!a)throw new Error("\u274C extension-path.txt is empty. Run setup script first.");return a}catch(e){throw new Error(`\u274C Failed to get ${t} extension path: ${e.message}`)}}async function tt({wallet:t,workerInfo:e,profileName:o,slowMo:a}){let r=await N(e.workerIndex.toString()),c=w(t.name),s=vt.resolve(c,o??"wallet-data");if(!Z.existsSync(s))throw new Error(`Cache for ${t.name} does not exist. Create it first!`);Z.cpSync(s,r,{recursive:!0,force:!0});let p=await R(t.name),m=await bt.launchPersistentContext(r,{headless:!1,args:[`--disable-extensions-except=${p}`],slowMo:process.env.HEADLESS?0:a}),u=await t.indexUrl(),l=m.pages()[0];return l||(l=await m.newPage()),await l.goto(u),{context:m,walletPage:l,contextPath:r}}import{expect as nt}from"@playwright/test";function W(t){return new Promise(e=>setTimeout(e,t))}async function C(t){await t.waitForLoadState("load",{timeout:15e3}),await t.waitForLoadState("domcontentloaded",{timeout:15e3})}import et from"fs";import kt from"path";async function L(t){let e=w(t),o=kt.resolve(e,"password.txt");try{if(!et.existsSync(o))throw new Error("\u274C password.txt not found. Run setup script first.");return et.readFileSync(o,"utf-8")}catch(a){throw new Error(`\u274C Failed to get ${t} password from cache: ${a.message}`)}}import{expect as Ct}from"@playwright/test";async function ot({context:t,path:e,locator:o}){let a;try{await Ct.poll(async()=>(a=t.pages().filter(r=>r.url().startsWith("chrome-extension://")).find(r=>r.url().match(e)),!!a),{timeout:3e4}).toBe(!0)}catch{let r=t.pages().filter(c=>c.url().startsWith("chrome-extension://")).map(c=>c.url());throw new Error(`Popup page with path "${e}" not found in context after 30s. Pages in context: ${JSON.stringify(r)}`)}if(!a)throw new Error(`Popup page with path ${e} not found in context.`);return await St(a,o),await a.setViewportSize({width:360,height:592}),a}async function St(t,e){await t.waitForLoadState("load",{timeout:4e4}),await t.waitForLoadState("domcontentloaded",{timeout:4e4}),await t.locator(e).first().waitFor({state:"attached",timeout:4e4})}import at from"fs";import Bt from"path";async function rt(t){let e=w(t),o=Bt.resolve(e,"extension-id.txt");try{if(!at.existsSync(o))throw new Error("\u274C extension-id.txt not found. Run setup script first.");return at.readFileSync(o,"utf-8")}catch(a){throw new Error(`\u274C Failed to get ${t} extension ID from cache: ${a.message}`)}}var g=class{name="keplr";onboardingPath="register.html";async indexUrl(){return`chrome-extension://${await this.extensionId()}/sidePanel.html`}async onboardingUrl(){return`chrome-extension://${await this.extensionId()}/${this.onboardingPath}`}async promptUrl(){return`chrome-extension://${await this.extensionId()}/popup.html`}async extensionId(){return await rt(this.name)}async promptPage(e){let o=await this.promptUrl();return await ot({context:e,path:o,locator:"div[data-simplebar='init']"})}};var h={importExistingWalletButton:"button:has-text('Import an existing wallet')",usePrivateKeyButton:"button:has-text('Use recovery phrase or private key')",privateKeyTabButton:"button:has-text('Private key')",privateKeyInput:"input[type='password']",importButton:"button:has-text('Import')",walletNameInput:"input[name='name']",walletPasswordInput:"input[name='password']",confirmWalletPasswordInput:"input[name='confirmPassword']",nextButton:"button:has-text('Next')",searchNetworkInput:"input[placeholder='Search networks']",saveButton:"button:has-text('Save')",finishButton:"button:has-text('Finish')"};function Tt(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}async function E({page:t,privateKey:e,walletName:o,chains:a,mode:r="onboard"}){let c=await L("keplr"),s=new g;if(await t.locator(h.importExistingWalletButton).click(),await t.locator(h.usePrivateKeyButton).click(),await t.getByRole("button",{name:"Private key",exact:!0}).click(),await t.locator(h.privateKeyInput).fill(e),await t.getByRole("button",{name:"Import",exact:!0}).click(),await t.locator(h.walletNameInput).fill(o),r==="onboard"){let A=t.locator(h.walletPasswordInput),F=t.locator(h.confirmWalletPasswordInput);await A.fill(c),await F.fill(c)}await t.locator(h.nextButton).click();let y=t.locator("div:has-text('All Native Chains')").nth(-4),P=t.locator("div[cursor='pointer']:has-text('Cosmos Hub')"),b=await y.locator("input[type='checkbox']").getAttribute("checked"),k=await P.locator("input[type='checkbox']").getAttribute("checked");b!==null&&await y.click(),k!==null&&await P.click();let D=t.locator(h.searchNetworkInput);for(let A of a){await D.fill(A);let T=t.locator("div[class='simplebar-content']").locator("div[cursor] > div").first().locator("div").filter({hasText:new RegExp(`^${Tt(A)}$`,"i")}).nth(2).locator("../../../../..");await T.waitFor({state:"visible",timeout:2e4}),await T.locator("input[type='checkbox']").getAttribute("checked")===null&&await T.click()}let B=t.locator(h.saveButton);if(await B.scrollIntoViewIfNeeded(),await B.click(),await W(2e3),r==="onboard"){await t.goto(await s.indexUrl());return}if(r==="add-account-single"){let A=t.locator(h.finishButton);await A.waitFor({state:"visible",timeout:2e4}),await nt(A).toBeEnabled({timeout:2e4}),await A.click()}}async function $(t){let o=await new g().onboardingUrl();await t.getByRole("link",{name:"Settings",exact:!0}).click(),await t.locator("div[cursor='pointer']").first().click(),await t.getByRole("button",{name:"Add Wallet",exact:!0}).click();let s;if(await nt.poll(async()=>(s=t.context().pages().find(p=>p.url().match(o)),!!s),{timeout:3e4}).toBe(!0).catch(p=>{console.error(`Failed to find onboarding page with URL matching ${o}. Original error: ${p}`)}),!s)throw new Error(`Onboarding page not found. Expected URL: ${o}`);return await C(s),await s.bringToFront(),s}async function it({page:t,privateKey:e,chains:o,walletName:a,mode:r}){let c=await $(t);await E({page:c,privateKey:e,walletName:a,chains:o,mode:r}),await t.locator("div:has(div:has-text('Select Wallet'))").nth(-4).locator("div:has(> div > svg)").first().click(),await t.getByRole("link",{name:"Home",exact:!0}).click()}import{expect as Wt}from"@playwright/test";var K={approveButton:"button:has-text('Approve')",rejectButton:"button[color='secondary']"};async function ct(t){let e=t.locator(K.approveButton);await Wt(e).toBeEnabled({timeout:2e4}),await e.click()}async function st(t){t.getByRole("button",{name:"Approve",exact:!0}).click(),await W(1e3)}import{expect as Et}from"@playwright/test";import f from"zod";var lt=f.discriminatedUnion("chain",[f.object({chain:f.literal(["Injective","Injective (Testnet)","Polygon"]),walletName:f.string().min(1,"Wallet name cannot be an empty string")}),f.object({chain:f.literal(["Bitcoin","Bitcoin Signet","Bitcoin Testnet"]),chainTag:f.literal(["Taproot","Native Segwit"]),walletName:f.string().min(1,"Wallet name cannot be an empty string")})]),pt=f.object({currentAccountName:f.string().min(1,"Current account name cannot be an empty string"),newAccountName:f.string().min(1,"New account name cannot be an empty string")});async function mt({page:t,...e}){let o=lt.parse({...e});await t.getByRole("textbox",{name:"Search for asset or chain (i.e. ATOM, Cosmos)",exact:!0}).fill(o.chain);let r=t.locator(`div:has-text("${o.chain}")`).nth(-2).filter({hasNot:t.locator("span")});if(await r.waitFor({state:"attached",timeout:2e4}),!await r.isVisible().catch(()=>!1))throw Error(`Make sure "${o.chain}" is activated.`);let s=await r.locator("div").all();Et(s.length).toBeGreaterThan(0),await t.locator(`div:has(div:has-text('${o.walletName}'))`).nth(-3).locator("div:has(> div > svg)").click();let u=t.locator("div:has(> div[data-simplebar='init'])").last(),d=u.locator("div:has(> div > input)").locator("input");await d.fill(o.chain);let v=await u.locator("div[cursor='pointer']",{hasText:o.chain}).all(),y;for(let b of v){let k;"chainTag"in e&&(k=e.chainTag);let D=b.locator("div",{hasText:o.chain}).last(),B=k?b.locator("div",{hasText:k}).last():null,F=(B?await B?.isVisible().catch(()=>!1):!1)?await B?.textContent():null,T=await D.textContent(),X=F?`${T} ${F}`:T,xt=k?`${o.chain} ${k}`:o.chain;if(X===xt){y=D.locator("xpath=../../../.."),await d.clear();break}}if(!y)throw Error(`Address for ${o.walletName} account on "${o.chain}" chain not found.`);return await y.hover(),await y.scrollIntoViewIfNeeded(),await y.click(),await t.evaluate(async()=>await navigator.clipboard.readText())}var O={openSidebarMenuButton:"div[cursor='pointer']:has(> div[cursor='pointer'])",menuPopupContent:"div[id='modal-root-3']",lockWalletButton:"div:has(> div:has-text('Lock Wallet'))",settingsButton:"div:has(a[href='#/settings'])"},H={unlockButton:"button[type='submit']:has-text('Unlock')",passwordInput:"input[placeholder='Type Your Password']"};async function ut(t){await t.locator(O.openSidebarMenuButton).click(),await t.locator(O.lockWalletButton).nth(-1).click(),await t.getByText("Welcome Back").waitFor({state:"visible",timeout:3e4})}import{styleText as dt}from"util";import{expect as It}from"@playwright/test";async function U(t,e){let o=t.locator("div[color]").nth(1);if(await o.textContent()===e){console.info(`
|
|
2
|
-
Already on ${e} account. No need to switch.`);return}await o.click();let r=t.locator("div[class='simplebar-content'] > div").locator("> div",{hasText:e});if(!await r.isVisible().catch(()=>!1))throw new Error(`Account "${e}" not found. Make sure the account is onboarded or verify the account name.`);let
|
|
3
|
-
Keplr onboarding started...`,{validateStream:!1})),e.length===1)for(let{privateKey:o,walletName:a,chains:r}of e)await
|
|
1
|
+
import Z from"fs";import vt from"path";import{chromium as bt}from"@playwright/test";import yt from"path";var Y=".wallet-cache",q=".wallet-context";var J="13.22.0",_="https://github.com/amaify/chainwright/releases/download/v0.1.0/",Kt=`https://github.com/MetaMask/metamask-extension/releases/download/v${J}/metamask-chrome-${J}.zip`,Ut=`${_}solflare-wallet-extension-v2.19.1.zip`,Mt=`${_}petra-wallet-extension-v2.4.8.zip`,Vt=`${_}phantom-wallet-extension-v26.10.0.zip`,Ht=`${_}meteor-wallet-extension-v0.7.0.zip`,jt=`${_}keplr-wallet-extension-v0.13.3.zip`;async function N(t){return yt.resolve(process.cwd(),q,t)}import At from"path";function w(t){return At.resolve(process.cwd(),Y,t)}import Q from"fs";import Pt from"path";async function R(t){try{let e=w(t),o=Pt.resolve(e,"extension-path.txt");if(!Q.existsSync(o))throw new Error("\u274C extension-path.txt not found. Run setup script first.");let a=Q.readFileSync(o,"utf-8").trim();if(!a)throw new Error("\u274C extension-path.txt is empty. Run setup script first.");return a}catch(e){throw new Error(`\u274C Failed to get ${t} extension path: ${e.message}`)}}async function tt({wallet:t,workerInfo:e,profileName:o,slowMo:a}){let r=await N(e.workerIndex.toString()),s=w(t.name),c=vt.resolve(s,o??"wallet-data");if(!Z.existsSync(c))throw new Error(`Cache for ${t.name} does not exist. Create it first!`);Z.cpSync(c,r,{recursive:!0,force:!0});let p=await R(t.name),m=await bt.launchPersistentContext(r,{headless:!1,args:[`--disable-extensions-except=${p}`],slowMo:process.env.HEADLESS?0:a}),u=await t.indexUrl(),l=m.pages()[0];return l||(l=await m.newPage()),await l.goto(u),{context:m,walletPage:l,contextPath:r}}import{expect as nt}from"@playwright/test";function T(t){return new Promise(e=>setTimeout(e,t))}async function C(t){await t.waitForLoadState("load",{timeout:15e3}),await t.waitForLoadState("domcontentloaded",{timeout:15e3})}import et from"fs";import kt from"path";async function L(t){let e=w(t),o=kt.resolve(e,"password.txt");try{if(!et.existsSync(o))throw new Error("\u274C password.txt not found. Run setup script first.");return et.readFileSync(o,"utf-8")}catch(a){throw new Error(`\u274C Failed to get ${t} password from cache: ${a.message}`)}}import{expect as Ct}from"@playwright/test";async function ot({context:t,path:e,locator:o}){let a;try{await Ct.poll(async()=>(a=t.pages().filter(r=>r.url().startsWith("chrome-extension://")).find(r=>r.url().match(e)),!!a),{timeout:3e4}).toBe(!0)}catch{let r=t.pages().filter(s=>s.url().startsWith("chrome-extension://")).map(s=>s.url());throw new Error(`Popup page with path "${e}" not found in context after 30s. Pages in context: ${JSON.stringify(r)}`)}if(!a)throw new Error(`Popup page with path ${e} not found in context.`);return await St(a,o),await a.setViewportSize({width:360,height:592}),a}async function St(t,e){await t.waitForLoadState("load",{timeout:4e4}),await t.waitForLoadState("domcontentloaded",{timeout:4e4}),await t.locator(e).first().waitFor({state:"attached",timeout:4e4})}import at from"fs";import Bt from"path";async function rt(t){let e=w(t),o=Bt.resolve(e,"extension-id.txt");try{if(!at.existsSync(o))throw new Error("\u274C extension-id.txt not found. Run setup script first.");return at.readFileSync(o,"utf-8")}catch(a){throw new Error(`\u274C Failed to get ${t} extension ID from cache: ${a.message}`)}}var h=class{name="keplr";onboardingPath="register.html";async indexUrl(){return`chrome-extension://${await this.extensionId()}/sidePanel.html`}async onboardingUrl(){return`chrome-extension://${await this.extensionId()}/${this.onboardingPath}`}async promptUrl(){return`chrome-extension://${await this.extensionId()}/popup.html`}async extensionId(){return await rt(this.name)}async promptPage(e){let o=await this.promptUrl();return await ot({context:e,path:o,locator:"div[data-simplebar='init']"})}};var g={importExistingWalletButton:"button:has-text('Import an existing wallet')",usePrivateKeyButton:"button:has-text('Use recovery phrase or private key')",privateKeyTabButton:"button:has-text('Private key')",privateKeyInput:"input[type='password']",importButton:"button:has-text('Import')",walletNameInput:"input[name='name']",walletPasswordInput:"input[name='password']",confirmWalletPasswordInput:"input[name='confirmPassword']",nextButton:"button:has-text('Next')",searchNetworkInput:"input[placeholder='Search networks']",saveButton:"button:has-text('Save')",finishButton:"button:has-text('Finish')"};function Tt(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}async function W({page:t,privateKey:e,walletName:o,chains:a,mode:r="onboard"}){let s=await L("keplr"),c=new h;if(await t.locator(g.importExistingWalletButton).click(),await t.locator(g.usePrivateKeyButton).click(),await t.getByRole("button",{name:"Private key",exact:!0}).click(),await t.locator(g.privateKeyInput).fill(e),await t.getByRole("button",{name:"Import",exact:!0}).click(),await t.locator(g.walletNameInput).fill(o),r==="onboard"){let A=t.locator(g.walletPasswordInput),F=t.locator(g.confirmWalletPasswordInput);await A.fill(s),await F.fill(s)}await t.locator(g.nextButton).click();let y=t.locator("div:has-text('All Native Chains')").nth(-4),P=t.locator("div[cursor='pointer']:has-text('Cosmos Hub')"),b=await y.locator("input[type='checkbox']").getAttribute("checked"),k=await P.locator("input[type='checkbox']").getAttribute("checked");b!==null&&await y.click(),k!==null&&await P.click();let D=t.locator(g.searchNetworkInput);for(let A of a){await D.fill(A);let B=t.locator("div[class='simplebar-content']").locator("div[cursor] > div").first().locator("div").filter({hasText:new RegExp(`^${Tt(A)}$`,"i")}).nth(2).locator("../../../../..");await B.waitFor({state:"visible",timeout:2e4}),await B.locator("input[type='checkbox']").getAttribute("checked")===null&&await B.click()}let S=t.locator(g.saveButton);if(await S.scrollIntoViewIfNeeded(),await S.click(),await T(2e3),r==="onboard"){await t.goto(await c.indexUrl());return}if(r==="add-account-single"){let A=t.locator(g.finishButton);await A.waitFor({state:"visible",timeout:2e4}),await nt(A).toBeEnabled({timeout:2e4}),await A.click()}}async function $(t){let o=await new h().onboardingUrl();await t.getByRole("link",{name:"Settings",exact:!0}).click(),await t.locator("div[cursor='pointer']").first().click(),await t.getByRole("button",{name:"Add Wallet",exact:!0}).click();let c;if(await nt.poll(async()=>(c=t.context().pages().find(p=>p.url().match(o)),!!c),{timeout:3e4}).toBe(!0).catch(p=>{console.error(`Failed to find onboarding page with URL matching ${o}. Original error: ${p}`)}),!c)throw new Error(`Onboarding page not found. Expected URL: ${o}`);return await C(c),await c.bringToFront(),c}async function it({page:t,privateKey:e,chains:o,walletName:a,mode:r}){let s=await $(t);await W({page:s,privateKey:e,walletName:a,chains:o,mode:r}),await t.locator("div:has(div:has-text('Select Wallet'))").nth(-4).locator("div:has(> div > svg)").first().click(),await t.getByRole("link",{name:"Home",exact:!0}).click()}import{expect as Wt}from"@playwright/test";var K={approveButton:"button:has-text('Approve')",rejectButton:"button[color='secondary']"};async function ct(t){let e=t.locator(K.approveButton);await Wt(e).toBeEnabled({timeout:2e4}),await e.click()}async function st(t){t.getByRole("button",{name:"Approve",exact:!0}).click(),await T(1e3)}import{expect as Et}from"@playwright/test";import f from"zod";var lt=f.discriminatedUnion("chain",[f.object({chain:f.literal(["Injective","Injective (Testnet)","Polygon"]),walletName:f.string().min(1,"Wallet name cannot be an empty string")}),f.object({chain:f.literal(["Bitcoin","Bitcoin Signet","Bitcoin Testnet"]),chainTag:f.literal(["Taproot","Native Segwit"]),walletName:f.string().min(1,"Wallet name cannot be an empty string")})]),pt=f.object({currentAccountName:f.string().min(1,"Current account name cannot be an empty string"),newAccountName:f.string().min(1,"New account name cannot be an empty string")});async function mt({page:t,...e}){let o=lt.parse({...e});await t.getByRole("textbox",{name:"Search for asset or chain (i.e. ATOM, Cosmos)",exact:!0}).fill(o.chain);let r=t.locator(`div:has-text("${o.chain}")`).nth(-2).filter({hasNot:t.locator("span")});if(await r.waitFor({state:"attached",timeout:2e4}),!await r.isVisible().catch(()=>!1))throw Error(`Make sure "${o.chain}" is activated.`);let c=await r.locator("div").all();Et(c.length).toBeGreaterThan(0),await t.locator(`div:has(div:has-text('${o.walletName}'))`).nth(-3).locator("div:has(> div > svg)").click();let u=t.locator("div:has(> div[data-simplebar='init'])").last(),d=u.locator("div:has(> div > input)").locator("input");await d.fill(o.chain);let v=await u.locator("div[cursor='pointer']",{hasText:o.chain}).all(),y;for(let b of v){let k;"chainTag"in e&&(k=e.chainTag);let D=b.locator("div",{hasText:o.chain}).last(),S=k?b.locator("div",{hasText:k}).last():null,F=(S?await S?.isVisible().catch(()=>!1):!1)?await S?.textContent():null,B=await D.textContent(),X=F?`${B} ${F}`:B,xt=k?`${o.chain} ${k}`:o.chain;if(X===xt){y=D.locator("xpath=../../../.."),await d.clear();break}}if(!y)throw Error(`Address for ${o.walletName} account on "${o.chain}" chain not found.`);return await y.hover(),await y.scrollIntoViewIfNeeded(),await y.click(),await t.evaluate(async()=>await navigator.clipboard.readText())}var O={openSidebarMenuButton:"div[cursor='pointer']:has(> div[cursor='pointer'])",menuPopupContent:"div[id='modal-root-3']",lockWalletButton:"div:has(> div:has-text('Lock Wallet'))",settingsButton:"div:has(a[href='#/settings'])"},H={unlockButton:"button[type='submit']:has-text('Unlock')",passwordInput:"input[placeholder='Type Your Password']"};async function ut(t){await t.locator(O.openSidebarMenuButton).click(),await t.locator(O.lockWalletButton).nth(-1).click(),await t.getByText("Welcome Back").waitFor({state:"visible",timeout:3e4})}import{styleText as dt}from"util";import{expect as It}from"@playwright/test";async function U(t,e){let o=t.locator("div[color]").nth(1);if(await o.textContent()===e){console.info(`
|
|
2
|
+
Already on ${e} account. No need to switch.`);return}await o.click();let r=t.locator("div[class='simplebar-content'] > div").locator("> div",{hasText:e});if(!await r.isVisible().catch(()=>!1))throw new Error(`Account "${e}" not found. Make sure the account is onboarded or verify the account name.`);let c=t.locator("div:has-text('Select Wallet')").last();await r.click(),await c.waitFor({state:"detached",timeout:3e4})}async function j({page:t,onboard:e}){if(console.info(dt("yellowBright",`
|
|
3
|
+
Keplr onboarding started...`,{validateStream:!1})),e.length===1)for(let{privateKey:o,walletName:a,chains:r}of e)await W({page:t,privateKey:o,walletName:a,chains:r,mode:"onboard"});if(e.length>1){let o=e[0];if(o){let{privateKey:l,walletName:d,chains:x}=o;await W({page:t,privateKey:l,walletName:d,chains:x,mode:"onboard"})}let a=e.slice(1);for(let{privateKey:l,walletName:d,chains:x}of a){let v=await $(t);await W({page:v,privateKey:l,walletName:d,chains:x,mode:"add-account-single"})}await t.locator("div",{hasText:"Select Wallet"}).last().locator("../../..").locator("div > svg").click(),await t.getByRole("link",{name:"Home",exact:!0}).click();let p=t.locator(O.openSidebarMenuButton);await It(p).toBeVisible({timeout:3e4});let m=e.at(-1)?.walletName,u=e[0]?.walletName;m&&u&&await U(t,u)}await T(3e3),console.info(dt("greenBright","\u2728 Keplr onboarding completed successfully",{validateStream:!1}))}async function wt(t){let e=t.locator(K.rejectButton);await e.waitFor({state:"visible",timeout:2e4}),await e.click()}async function ht({page:t,currentAccountName:e,newAccountName:o}){let a=pt.parse({currentAccountName:e,newAccountName:o});await t.getByRole("link",{name:"Settings",exact:!0}).click(),await t.locator("div[cursor='pointer']").first().click();let c=t.locator("div",{hasText:a.currentAccountName}).nth(-4);if(!await c.isVisible().catch(()=>!1))throw Error(`Account with name "${a.currentAccountName}" not found`);await c.locator("div[cursor='pointer'] svg").click(),await t.locator("div > div[cursor='pointer'] > div:has-text('Change Wallet Name')").last().click(),await t.locator("input[name='name']").fill(a.newAccountName),await t.locator("button:has-text('Save')").click()}async function M(t){let e=await L("keplr");await t.locator(H.passwordInput).fill(e),await t.locator(H.unlockButton).click(),await t.locator("div:has-text('Deposit')").last().waitFor({state:"visible",timeout:3e4})}var E=class extends h{page;constructor(e){super(),this.page=e}async onboard(e){await j({page:this.page,onboard:e})}async unlock(){await M(this.page)}async lock(){await ut(this.page)}async renameAccount({currentAccountName:e,newAccountName:o}){await ht({page:this.page,currentAccountName:e,newAccountName:o})}async switchAccount(e){await U(this.page,e)}async getAccountAddress({...e}){return await mt({page:this.page,...e})}async addAccount({chains:e,privateKey:o,walletName:a,mode:r="add-account-multiple"}){await it({page:this.page,privateKey:o,walletName:a,chains:e,mode:r})}async connectToApp(){await st(await this.promptPage(this.page.context()))}async confirmTransaction(){await ct(await this.promptPage(this.page.context()))}async rejectTransaction(){await wt(await this.promptPage(this.page.context()))}};import ft from"fs";import Ot from"path";import{test as Dt,chromium as Rt}from"@playwright/test";import{expect as Ft}from"@playwright/test";async function z(t,e){let o=await t.newPage();return await Ft(async()=>{await o.goto(e),await C(o)}).toPass(),o}async function G(t,e){let o=await e.newPage();for(let{origin:a,localStorage:r}of t){let s=o.mainFrame();await s.goto(a),await s.evaluate(c=>{c.forEach(({name:p,value:m})=>{window.localStorage.setItem(p,m)})},r)}await o.close()}import _t from"fs/promises";async function gt(t){await _t.rm(t,{maxRetries:50,retryDelay:500,recursive:!0,force:!0})}var Nt=35e3;async function V(t,e){try{await Promise.race([t.close(),new Promise((o,a)=>setTimeout(()=>a(new Error("Context close timed out")),Nt))])}catch(o){console.warn(`Browser context close did not complete cleanly: ${o.message}`)}try{await gt(e)}catch(o){console.error(`Failed to remove temporary context directory at ${e}. Error:`,o)}}var I,Pa=({slowMo:t=0,profileName:e}={})=>Dt.extend({contextPath:async({browserName:o},a,r)=>{let s=await N(`${o}-${r.testId}`);await a(s)},context:async({context:o,contextPath:a},r)=>{let s=new h,c=w(s.name),p=await R(s.name),m=Ot.resolve(c,e??"wallet-data");if(!ft.existsSync(m))throw new Error("\u274C Cache for Keplr wallet data not found. Create it first");ft.cpSync(m,a,{recursive:!0,force:!0});let u=[`--disable-extensions-except=${p}`,`--load-extension=${p}`];process.env.HEADLESS&&(u.push("--headless=new"),t>0&&console.warn("\u26A0\uFE0F Slow motion makes no sense in headless mode. It will be ignored!"));let l=await Rt.launchPersistentContext(a,{headless:!1,args:u,slowMo:process.env.HEADLESS?0:t});await l.grantPermissions(["clipboard-read"]);let{cookies:d,origins:x}=await o.storageState();d&&await l.addCookies(d),x&&x.length>0&&G(x,l);let v=await s.indexUrl();I=l.pages().find(P=>P.url().startsWith(v))||await z(l,v),await C(I);for(let P of l.pages()){let b=P.url();(b.includes("about:blank")||b.includes(s.onboardingPath))&&await P.close()}await I.bringToFront(),await M(I),await r(l),await V(l,a)},keplrPage:async({context:o},a)=>{await a(I)},keplr:async({context:o},a)=>{let r=new E(I);await a(r)}});import{test as Lt}from"@playwright/test";var Wa=({slowMo:t,profileName:e}={})=>Lt.extend({workerScopeContents:[async({browser:o},a,r)=>{let s=new h,{context:c,contextPath:p,walletPage:m}=await tt({wallet:s,workerInfo:r,profileName:e,slowMo:t});await c.grantPermissions(["clipboard-read"]);for(let l of c.pages())l.url().includes("about:blank")&&await l.close();let u=new E(m);await u.unlock(),await a({wallet:u,walletPage:m,context:c}),await V(c,p)},{scope:"worker"}]});export{E as Keplr,Pa as keplrFixture,Wa as keplrWorkerScopeFixture,tt as workerScopeContext};
|