@walletwright/core 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +19 -15
- package/dist/{cache-BaWu5vyM.mjs → cache-gBvk86TW.mjs} +334 -401
- package/dist/cache-gBvk86TW.mjs.map +1 -0
- package/dist/chain.d.mts +3 -3
- package/dist/cli.d.mts +4 -4
- package/dist/cli.mjs +52 -39
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +17 -9
- package/dist/index.mjs +226 -75
- package/dist/index.mjs.map +1 -1
- package/dist/mock-standard.d.mts +5 -0
- package/dist/mock-standard.mjs +9 -7
- package/dist/mock-standard.mjs.map +1 -1
- package/dist/mock.d.mts +2 -2
- package/dist/mock.mjs +5 -3
- package/dist/mock.mjs.map +1 -1
- package/dist/{types-BXDYOEaq.d.mts → types-CjNm7Tik.d.mts} +63 -19
- package/package.json +10 -9
- package/dist/cache-BaWu5vyM.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,65 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { cp, mkdtemp, rm
|
|
1
|
+
import { a as walletKinds, c as locateProfile, d as gotoWithRetry, f as waitUntil, i as isWalletKind, l as pathExists, n as openWalletContext, o as walletKindsByEcosystem, r as prepareExtension, s as wallets, t as buildCache, u as formatTimeout } from "./cache-gBvk86TW.mjs";
|
|
2
|
+
import { cp, mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { test } from "@playwright/test";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
|
|
7
|
+
//#region src/internal/popup.ts
|
|
8
|
+
/** MetaMask/Phantom approval-popup URL token; Slush overrides it with `notificationMatch: "isPopup=1"`. */
|
|
9
|
+
const DEFAULT_NOTIFICATION_MATCH = "notification.html";
|
|
10
|
+
/**
|
|
11
|
+
* Approval popups open as `about:blank` and then navigate, so `waitForEvent('page', { predicate })`
|
|
12
|
+
* misses them, poll the open pages instead. `match` is the URL token identifying the popup
|
|
13
|
+
* (`notification.html` for MetaMask/Phantom; `isPopup=1` for Slush's single-page `index.html`).
|
|
14
|
+
*/
|
|
15
|
+
const isApprovalPopup = (page, extensionId, match) => page.url().startsWith(`chrome-extension://${extensionId}`) && page.url().includes(match) && !page.isClosed();
|
|
16
|
+
const isVisible = async (locator) => {
|
|
17
|
+
try {
|
|
18
|
+
return await locator.isVisible();
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const hasVisibleButton = (page) => isVisible(page.locator("button").first());
|
|
24
|
+
const findNotificationPopup = ({ approvalControls, context, extensionId, match, timeoutMs }) => waitUntil(async () => {
|
|
25
|
+
const popup = context.pages().find((page) => isApprovalPopup(page, extensionId, match));
|
|
26
|
+
if (!popup) return;
|
|
27
|
+
await popup.waitForLoadState("domcontentloaded").catch(() => {});
|
|
28
|
+
return (approvalControls === void 0 ? await hasVisibleButton(popup) : await isVisible(approvalControls(popup).first())) ? popup : void 0;
|
|
29
|
+
}, { timeoutMs });
|
|
30
|
+
/** MetaMask's own popup dimensions, which its layout is built for. */
|
|
31
|
+
const APPROVAL_WINDOW = {
|
|
32
|
+
height: 592,
|
|
33
|
+
width: 360
|
|
34
|
+
};
|
|
35
|
+
/** Far enough from the edge that a popup this size lands fully on any usable display. */
|
|
36
|
+
const APPROVAL_WINDOW_OFFSET = 50;
|
|
37
|
+
/** Move approval popups on-screen; headless and unsupported CDP targets are ignored. */
|
|
38
|
+
const placeApprovalWindow = async (page) => {
|
|
39
|
+
await page.setViewportSize(APPROVAL_WINDOW).catch(() => {});
|
|
40
|
+
try {
|
|
41
|
+
const session = await page.context().newCDPSession(page);
|
|
42
|
+
const { targetInfo } = await session.send("Target.getTargetInfo");
|
|
43
|
+
const { windowId } = await session.send("Browser.getWindowForTarget", { targetId: targetInfo.targetId });
|
|
44
|
+
await session.send("Browser.setWindowBounds", {
|
|
45
|
+
bounds: {
|
|
46
|
+
left: APPROVAL_WINDOW_OFFSET,
|
|
47
|
+
top: APPROVAL_WINDOW_OFFSET
|
|
48
|
+
},
|
|
49
|
+
windowId
|
|
50
|
+
});
|
|
51
|
+
await session.detach();
|
|
52
|
+
} catch {}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
7
56
|
//#region src/internal/controller.ts
|
|
8
57
|
const POPUP_CLOSE_TIMEOUT_MS = 15e3;
|
|
9
58
|
/** Optional approvals keep a short wait: "no popup" is a normal outcome, and the wait is latency. */
|
|
10
59
|
const OPTIONAL_POPUP_TIMEOUT_MS = 1e4;
|
|
11
60
|
/** A popup that has to appear gets the full wait: the MV3 service worker can be slow to spawn it. */
|
|
12
61
|
const REQUIRED_POPUP_TIMEOUT_MS = 3e4;
|
|
62
|
+
const runDirectly = (_title, body) => body();
|
|
13
63
|
/**
|
|
14
64
|
* Drive `popup` with `settle` and return the page that actually took the approval. A popup that
|
|
15
65
|
* closes mid-settle is the wallet respawning the request, so re-acquire once and drive the new one.
|
|
@@ -27,13 +77,14 @@ const settleApproval = async (settle, popup, acquire) => {
|
|
|
27
77
|
}
|
|
28
78
|
};
|
|
29
79
|
/** Build the runtime controller that drives an unlocked wallet against the dapp under test. */
|
|
30
|
-
const createWallet = ({ context, definition, extensionId, home, password }) => {
|
|
80
|
+
const createWallet = ({ context, definition, extensionId, home, password, step, unlock }) => {
|
|
31
81
|
const match = definition.notificationMatch ?? "notification.html";
|
|
32
82
|
const ctx = {
|
|
33
83
|
context,
|
|
34
84
|
extensionId,
|
|
35
85
|
home,
|
|
36
|
-
password
|
|
86
|
+
password,
|
|
87
|
+
unlock
|
|
37
88
|
};
|
|
38
89
|
/** MetaMask opens requests inline while its extension page owns focus. */
|
|
39
90
|
const frontDapp = async () => {
|
|
@@ -64,58 +115,131 @@ const createWallet = ({ context, definition, extensionId, home, password }) => {
|
|
|
64
115
|
const driven = await settleApproval(settle, popup, acquire);
|
|
65
116
|
if (await waitUntil(() => driven.isClosed() || void 0, { timeoutMs: POPUP_CLOSE_TIMEOUT_MS }) === void 0) throw new Error(`[walletwright] approval popup did not close after ${formatTimeout(POPUP_CLOSE_TIMEOUT_MS)} (the approval may not have registered)`);
|
|
66
117
|
};
|
|
67
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Every public call goes through here, so each one reaches `step` under its own name. It awaits
|
|
120
|
+
* `step` itself because Playwright reports a boxed step at the caller of the function awaiting it,
|
|
121
|
+
* which makes that the spec line; another async layer in between would move it into this package.
|
|
122
|
+
*/
|
|
123
|
+
const named = (name, fn) => async (...args) => {
|
|
124
|
+
await step(`wallet.${name}`, () => fn(...args));
|
|
125
|
+
};
|
|
68
126
|
const approve = (options = {}) => resolvePopup((popup) => definition.approve(popup, password), options);
|
|
69
127
|
const reject = (options = {}) => resolvePopup((popup) => definition.reject(popup), options);
|
|
70
128
|
/** Bind a verified wallet action and restore focus to the dapp when it finishes. */
|
|
71
|
-
const action = (
|
|
72
|
-
if (!fn) throw
|
|
129
|
+
const action = (name, fn) => named(name, async (...args) => {
|
|
130
|
+
if (!fn) throw new Error(`[walletwright] ${definition.extensionName} does not support ${name}()`);
|
|
73
131
|
if (home.isClosed()) throw new Error(`[walletwright] wallet home page is closed; cannot run ${name}()`);
|
|
74
132
|
await home.bringToFront().catch(() => {});
|
|
75
133
|
await fn(ctx, ...args);
|
|
76
134
|
await frontDapp();
|
|
77
|
-
};
|
|
135
|
+
});
|
|
136
|
+
const { accounts, network, settings } = definition.actions ?? {};
|
|
78
137
|
return {
|
|
79
138
|
accounts: {
|
|
80
|
-
add: action(
|
|
81
|
-
importPrivateKey: action(
|
|
82
|
-
rename: action(
|
|
83
|
-
switch: action(
|
|
139
|
+
add: action("accounts.add", accounts?.add),
|
|
140
|
+
importPrivateKey: action("accounts.importPrivateKey", accounts?.importPrivateKey),
|
|
141
|
+
rename: action("accounts.rename", accounts?.rename),
|
|
142
|
+
switch: action("accounts.switch", accounts?.switch)
|
|
84
143
|
},
|
|
85
|
-
approve,
|
|
86
|
-
confirmSignature: () => approve(),
|
|
87
|
-
confirmTransaction: () => approve(),
|
|
88
|
-
connectToDapp: (
|
|
144
|
+
approve: named("approve", approve),
|
|
145
|
+
confirmSignature: named("confirmSignature", () => approve()),
|
|
146
|
+
confirmTransaction: named("confirmTransaction", () => approve()),
|
|
147
|
+
connectToDapp: named("connectToDapp", approve),
|
|
89
148
|
extensionId,
|
|
90
149
|
home,
|
|
91
|
-
network: {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
reject,
|
|
96
|
-
rejectConnection: () => reject(),
|
|
97
|
-
rejectSignature: () => reject(),
|
|
98
|
-
rejectTransaction: () => reject(),
|
|
150
|
+
network: { add: action("network.add", network?.add) },
|
|
151
|
+
reject: named("reject", reject),
|
|
152
|
+
rejectConnection: named("rejectConnection", () => reject()),
|
|
153
|
+
rejectSignature: named("rejectSignature", () => reject()),
|
|
154
|
+
rejectTransaction: named("rejectTransaction", () => reject()),
|
|
99
155
|
settings: {
|
|
100
|
-
lock: action(
|
|
101
|
-
unlock: action(
|
|
156
|
+
lock: action("settings.lock", settings?.lock),
|
|
157
|
+
unlock: action("settings.unlock", settings?.unlock)
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/internal/unlock-screen.ts
|
|
164
|
+
const PASSWORD_FIELD = "input[type=\"password\"]";
|
|
165
|
+
const NAVIGATION_TIMEOUT_MS = 15e3;
|
|
166
|
+
const FIRST_SETTLE_TIMEOUT_MS = 2e4;
|
|
167
|
+
const RELOAD_ATTEMPTS = 5;
|
|
168
|
+
const RELOAD_SETTLE_TIMEOUT_MS = 5e3;
|
|
169
|
+
const PROBE_INTERVAL_MS = 500;
|
|
170
|
+
const PASSWORD_CLEARED_TIMEOUT_MS = 15e3;
|
|
171
|
+
/**
|
|
172
|
+
* Short on purpose. `unlock` is normally handed a page `reachUnlockScreen` already settled, so this
|
|
173
|
+
* budget only matters when it is called on its own (the `settings.unlock` path), where a wallet with a
|
|
174
|
+
* late-mounting UI needs a poll rather than a single read but a dead page should still fail fast.
|
|
175
|
+
*/
|
|
176
|
+
const UNLOCK_SETTLE_TIMEOUT_MS = 5e3;
|
|
177
|
+
/**
|
|
178
|
+
* The shared unlock flow: open the wallet's own page, wait for it to settle into a state we can
|
|
179
|
+
* name, and type the password. Every wallet differs only in its `unlockScreen`: the entry page, how
|
|
180
|
+
* it submits, and whether it can come back already unlocked.
|
|
181
|
+
*/
|
|
182
|
+
const createUnlockScreen = ({ extensionName: wallet, unlockScreen: { entry, isUnlocked, submit } }) => {
|
|
183
|
+
const settled = async (page) => {
|
|
184
|
+
if (await page.locator(PASSWORD_FIELD).isVisible().catch(() => false)) return "locked";
|
|
185
|
+
return await isUnlocked?.(page).catch(() => false) === true ? "unlocked" : void 0;
|
|
186
|
+
};
|
|
187
|
+
const settleWithin = (page, timeoutMs) => waitUntil(() => settled(page), {
|
|
188
|
+
intervalMs: PROBE_INTERVAL_MS,
|
|
189
|
+
timeoutMs
|
|
190
|
+
});
|
|
191
|
+
const reachUnlockScreen = async (context, extensionId) => {
|
|
192
|
+
const page = await context.newPage();
|
|
193
|
+
await gotoWithRetry(page, `chrome-extension://${extensionId}/${entry}`, {
|
|
194
|
+
label: `${wallet} ${entry}`,
|
|
195
|
+
timeoutMs: NAVIGATION_TIMEOUT_MS
|
|
196
|
+
});
|
|
197
|
+
let state = await settleWithin(page, FIRST_SETTLE_TIMEOUT_MS);
|
|
198
|
+
for (let attempt = 0; attempt < RELOAD_ATTEMPTS && state === void 0; attempt++) {
|
|
199
|
+
await page.reload().catch(() => {});
|
|
200
|
+
state = await settleWithin(page, RELOAD_SETTLE_TIMEOUT_MS);
|
|
102
201
|
}
|
|
202
|
+
if (state === void 0) throw new Error(`[walletwright] ${wallet} never reached its unlock screen (gave up after ${formatTimeout(45e3)})`);
|
|
203
|
+
return page;
|
|
204
|
+
};
|
|
205
|
+
const unlock = async (page, password) => {
|
|
206
|
+
const state = await settleWithin(page, UNLOCK_SETTLE_TIMEOUT_MS);
|
|
207
|
+
if (state === "unlocked") return;
|
|
208
|
+
if (state === void 0) throw new Error(`[walletwright] ${wallet} showed neither an unlock screen nor an unlocked wallet`);
|
|
209
|
+
const field = page.locator(PASSWORD_FIELD);
|
|
210
|
+
await field.fill(password);
|
|
211
|
+
await (submit === void 0 ? field.press("Enter") : submit(page, field));
|
|
212
|
+
if (!await field.waitFor({
|
|
213
|
+
state: "hidden",
|
|
214
|
+
timeout: PASSWORD_CLEARED_TIMEOUT_MS
|
|
215
|
+
}).then(() => true).catch(() => false)) throw new Error(`[walletwright] ${wallet} unlock failed (password screen still visible after ${formatTimeout(PASSWORD_CLEARED_TIMEOUT_MS)})`);
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
reachUnlockScreen,
|
|
219
|
+
unlock
|
|
103
220
|
};
|
|
104
221
|
};
|
|
105
222
|
|
|
106
223
|
//#endregion
|
|
107
224
|
//#region src/internal/launch.ts
|
|
225
|
+
/** Close the browser, then remove the profile copy, even when closing fails; report every failure. */
|
|
108
226
|
const closeLaunch = async (context, runDir) => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
227
|
+
const errors = [];
|
|
228
|
+
try {
|
|
229
|
+
await context.close();
|
|
230
|
+
} catch (error) {
|
|
231
|
+
errors.push(error);
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
await rm(runDir, {
|
|
235
|
+
force: true,
|
|
236
|
+
recursive: true
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
errors.push(error);
|
|
240
|
+
}
|
|
241
|
+
if (errors.length > 1) throw new AggregateError(errors, `[walletwright] failed to close the browser and remove ${runDir}`);
|
|
242
|
+
if (errors.length === 1) throw errors[0];
|
|
119
243
|
};
|
|
120
244
|
/** Close Chromium's blank tab and the extension's auto-opened tab. */
|
|
121
245
|
const closeStrayPages = async (context, home) => {
|
|
@@ -124,20 +248,13 @@ const closeStrayPages = async (context, home) => {
|
|
|
124
248
|
for (const page of context.pages()) if (isStray(page)) closing.push(page.close());
|
|
125
249
|
await Promise.allSettled(closing);
|
|
126
250
|
};
|
|
127
|
-
/**
|
|
128
|
-
const
|
|
251
|
+
/** `launchWallet`, with the step runner the Playwright fixtures report wallet calls through. */
|
|
252
|
+
const launch = async (setup, { headless, step }) => {
|
|
129
253
|
const definition = wallets[setup.wallet];
|
|
130
254
|
if (headless && definition.headlessApprovals !== true) throw new Error(`[walletwright] ${definition.extensionName} has no verified headless approval flow; run this suite headed (\`use: { headless: false }\`, or \`--headed\`).`);
|
|
131
|
-
const cacheDir =
|
|
132
|
-
|
|
133
|
-
await
|
|
134
|
-
try {
|
|
135
|
-
await stat(profileDir);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
throw new Error(`[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \`walletwright cache\`.`, { cause: error });
|
|
138
|
-
}
|
|
139
|
-
const extensionPath = await definition.prepareExtension(cacheDir, setup.version);
|
|
140
|
-
const extensionId = await extensionIdFromPath(extensionPath);
|
|
255
|
+
const { cacheDir, profileDir } = await locateProfile(setup);
|
|
256
|
+
if (!await pathExists(profileDir)) throw new Error(`[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \`walletwright cache\`.`);
|
|
257
|
+
const extensionPath = await prepareExtension(setup, cacheDir);
|
|
141
258
|
/** Cleanup must cover copy and launch failures that occur before a context exists. */
|
|
142
259
|
const runDir = await mkdtemp(path.join(os.tmpdir(), "walletwright-"));
|
|
143
260
|
const removeRunDir = () => rm(runDir, {
|
|
@@ -152,25 +269,36 @@ const launchWallet = async (setup, { headless = false } = {}) => {
|
|
|
152
269
|
let context;
|
|
153
270
|
try {
|
|
154
271
|
await cp(profileDir, runDir, { recursive: true });
|
|
155
|
-
|
|
156
|
-
|
|
272
|
+
const opened = await openWalletContext({
|
|
273
|
+
definition,
|
|
274
|
+
extensionPath,
|
|
275
|
+
headless,
|
|
276
|
+
userDataDir: runDir
|
|
277
|
+
});
|
|
278
|
+
const launched = opened.context;
|
|
279
|
+
context = launched;
|
|
157
280
|
launched.on("close", handleContextClose);
|
|
158
|
-
|
|
159
|
-
const home = await
|
|
160
|
-
|
|
281
|
+
const screen = createUnlockScreen(definition);
|
|
282
|
+
const home = await screen.reachUnlockScreen(launched, opened.extensionId);
|
|
283
|
+
const unlock = () => screen.unlock(home, setup.password);
|
|
284
|
+
await unlock();
|
|
161
285
|
await closeStrayPages(launched, home);
|
|
286
|
+
const close = async () => {
|
|
287
|
+
launched.off("close", handleContextClose);
|
|
288
|
+
await closeLaunch(launched, runDir);
|
|
289
|
+
};
|
|
162
290
|
return {
|
|
163
|
-
close
|
|
164
|
-
launched.off("close", handleContextClose);
|
|
165
|
-
await closeLaunch(launched, runDir);
|
|
166
|
-
},
|
|
291
|
+
close,
|
|
167
292
|
context: launched,
|
|
293
|
+
[Symbol.asyncDispose]: close,
|
|
168
294
|
wallet: createWallet({
|
|
169
295
|
context: launched,
|
|
170
296
|
definition,
|
|
171
|
-
extensionId,
|
|
297
|
+
extensionId: opened.extensionId,
|
|
172
298
|
home,
|
|
173
|
-
password: setup.password
|
|
299
|
+
password: setup.password,
|
|
300
|
+
step,
|
|
301
|
+
unlock
|
|
174
302
|
})
|
|
175
303
|
};
|
|
176
304
|
} catch (error) {
|
|
@@ -180,24 +308,47 @@ const launchWallet = async (setup, { headless = false } = {}) => {
|
|
|
180
308
|
throw error;
|
|
181
309
|
}
|
|
182
310
|
};
|
|
311
|
+
/** Launch and unlock a disposable profile copy. Headless mode requires verified wallet support. */
|
|
312
|
+
const launchWallet = (setup, { headless = false } = {}) => launch(setup, {
|
|
313
|
+
headless,
|
|
314
|
+
step: runDirectly
|
|
315
|
+
});
|
|
183
316
|
|
|
184
317
|
//#endregion
|
|
185
318
|
//#region src/fixtures.ts
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
})
|
|
319
|
+
/**
|
|
320
|
+
* Report each wallet call as a boxed step, named after it (`wallet.confirmSignature`) and failing at
|
|
321
|
+
* the spec line that made the call. Not `async`: an extra awaiting frame would become that location.
|
|
322
|
+
*/
|
|
323
|
+
const reportAsStep = (title, body) => test.step(title, body, { box: true });
|
|
324
|
+
/**
|
|
325
|
+
* Create Playwright fixtures backed by one unlocked wallet launch per test. `headless` applies only
|
|
326
|
+
* to wallets that can be driven that way (Phantom, Rabby); the rest launch headed, so a suite runs
|
|
327
|
+
* under Playwright's defaults. `launchWallet` is the strict form that refuses them headless instead.
|
|
328
|
+
*/
|
|
329
|
+
const createWalletFixtures = (setup) => {
|
|
330
|
+
const definition = wallets[setup.wallet];
|
|
331
|
+
return test.extend({
|
|
332
|
+
context: [async ({ walletLaunch }, use) => {
|
|
333
|
+
await use(walletLaunch.context);
|
|
334
|
+
}, {
|
|
335
|
+
box: true,
|
|
336
|
+
scope: "test"
|
|
337
|
+
}],
|
|
338
|
+
wallet: [async ({ walletLaunch }, use) => {
|
|
339
|
+
await use(walletLaunch.wallet);
|
|
340
|
+
}, { box: true }],
|
|
341
|
+
walletLaunch: [async ({ headless }, use) => {
|
|
342
|
+
const launched = await launch(setup, {
|
|
343
|
+
headless: headless && definition.headlessApprovals === true,
|
|
344
|
+
step: reportAsStep
|
|
345
|
+
});
|
|
346
|
+
await use(launched);
|
|
347
|
+
await launched.close();
|
|
348
|
+
}, { title: `launch ${definition.extensionName}` }]
|
|
349
|
+
});
|
|
350
|
+
};
|
|
200
351
|
|
|
201
352
|
//#endregion
|
|
202
|
-
export { buildCache, createWalletFixtures, isWalletKind, launchWallet, walletKindsByEcosystem, wallets };
|
|
353
|
+
export { buildCache, createWalletFixtures, isWalletKind, launchWallet, walletKinds, walletKindsByEcosystem, wallets };
|
|
203
354
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["base"],"sources":["../src/internal/controller.ts","../src/internal/launch.ts","../src/fixtures.ts"],"sourcesContent":["import type { BrowserContext, Page } from \"@playwright/test\";\n\nimport type { Wallet, WalletActionContext, WalletDefinition } from \"../types\";\n\nimport { DEFAULT_NOTIFICATION_MATCH, findNotificationPopup, placeApprovalWindow } from \"./utils\";\nimport { formatTimeout, waitUntil } from \"./wait\";\n\nconst POPUP_CLOSE_TIMEOUT_MS = 15_000;\n/** Optional approvals keep a short wait: \"no popup\" is a normal outcome, and the wait is latency. */\nconst OPTIONAL_POPUP_TIMEOUT_MS = 10_000;\n/** A popup that has to appear gets the full wait: the MV3 service worker can be slow to spawn it. */\nconst REQUIRED_POPUP_TIMEOUT_MS = 30_000;\ntype ResolveOptions = { optional?: boolean };\n\n/**\n * Drive `popup` with `settle` and return the page that actually took the approval. A popup that\n * closes mid-settle is the wallet respawning the request, so re-acquire once and drive the new one.\n */\nconst settleApproval = async (\n settle: (popup: Page) => Promise<void>,\n popup: Page,\n acquire: () => Promise<Page | undefined>,\n): Promise<Page> => {\n try {\n await settle(popup);\n return popup;\n } catch (error) {\n if (!popup.isClosed()) {\n throw error;\n }\n const fresh = await acquire();\n if (!fresh) {\n throw error;\n }\n await settle(fresh);\n return fresh;\n }\n};\n\ntype CreateWalletOptions = {\n context: BrowserContext;\n definition: WalletDefinition;\n extensionId: string;\n /** The wallet's own page, left open after unlock, that the actions drive. */\n home: Page;\n password: string;\n};\n\n/** Build the runtime controller that drives an unlocked wallet against the dapp under test. */\nexport const createWallet = ({\n context,\n definition,\n extensionId,\n home,\n password,\n}: CreateWalletOptions): Wallet => {\n const match = definition.notificationMatch ?? DEFAULT_NOTIFICATION_MATCH;\n const ctx: WalletActionContext = { context, extensionId, home, password };\n\n /** MetaMask opens requests inline while its extension page owns focus. */\n const frontDapp = async (): Promise<void> => {\n const dapp = context.pages().find((page) => /^https?:/v.test(page.url()) && !page.isClosed());\n await dapp?.bringToFront().catch(() => {});\n };\n\n /** Find and place the approval window before its controls are driven. */\n const acquireApproval = async (optional: boolean): Promise<Page | undefined> => {\n const popup = await findNotificationPopup({\n approvalControls: definition.approvalControls,\n context,\n extensionId,\n match,\n timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS,\n });\n if (popup) {\n await placeApprovalWindow(popup);\n }\n return popup;\n };\n\n /** Drive the pending approval popup with `settle`, then wait for that popup to close. */\n const resolvePopup = async (\n settle: (popup: Page) => Promise<void>,\n { optional = false }: ResolveOptions,\n ): Promise<void> => {\n await frontDapp();\n const acquire = () => acquireApproval(optional);\n const popup = await acquire();\n if (!popup) {\n if (optional) {\n return; // e.g. Phantom auto-approves an already-trusted site (no popup)\n }\n const open = context.pages().map((page) => page.url());\n throw new Error(\n `[walletwright] approval popup did not appear. Open pages: ${open.join(\" | \") || \"none\"}`,\n );\n }\n\n const driven = await settleApproval(settle, popup, acquire);\n\n /** Wait for the exact popup we drove; chained popups may reuse the same URL. */\n const closed = await waitUntil(() => driven.isClosed() || undefined, {\n timeoutMs: POPUP_CLOSE_TIMEOUT_MS,\n });\n if (closed === undefined) {\n throw new Error(\n `[walletwright] approval popup did not close after ${formatTimeout(POPUP_CLOSE_TIMEOUT_MS)} (the approval may not have registered)`,\n );\n }\n };\n\n const unsupported = (name: string): Error =>\n new Error(`[walletwright] ${definition.extensionName} does not support ${name}()`);\n\n const approve = (options: ResolveOptions = {}) =>\n resolvePopup((popup) => definition.approve(popup, password), options);\n\n const reject = (options: ResolveOptions = {}) =>\n resolvePopup((popup) => definition.reject(popup), options);\n\n /** Bind a verified wallet action and restore focus to the dapp when it finishes. */\n const action =\n <A extends ReadonlyArray<unknown>>(\n fn: ((ctx: WalletActionContext, ...args: A) => Promise<void>) | undefined,\n name: string,\n ) =>\n async (...args: A): Promise<void> => {\n if (!fn) {\n throw unsupported(name);\n }\n if (home.isClosed()) {\n throw new Error(`[walletwright] wallet home page is closed; cannot run ${name}()`);\n }\n await home.bringToFront().catch(() => {});\n await fn(ctx, ...args);\n await frontDapp();\n };\n\n return {\n accounts: {\n add: action(definition.actions?.accounts?.add, \"accounts.add\"),\n importPrivateKey: action(\n definition.actions?.accounts?.importPrivateKey,\n \"accounts.importPrivateKey\",\n ),\n rename: action(definition.actions?.accounts?.rename, \"accounts.rename\"),\n switch: action(definition.actions?.accounts?.switch, \"accounts.switch\"),\n },\n approve,\n confirmSignature: () => approve(),\n confirmTransaction: () => approve(),\n connectToDapp: (options: ResolveOptions = {}) => approve(options),\n extensionId,\n home,\n network: {\n add: action(definition.actions?.network?.add, \"network.add\"),\n switch: action(definition.actions?.network?.switch, \"network.switch\"),\n },\n reject,\n rejectConnection: () => reject(),\n rejectSignature: () => reject(),\n rejectTransaction: () => reject(),\n settings: {\n lock: action(definition.actions?.settings?.lock, \"settings.lock\"),\n unlock: action(definition.actions?.settings?.unlock, \"settings.unlock\"),\n },\n };\n};\n","import { cp, mkdtemp, rm, stat } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport { type BrowserContext, chromium, type Page } from \"@playwright/test\";\n\nimport type { Wallet, WalletSetup } from \"../types\";\nimport { wallets } from \"../wallets/index\";\n\nimport { restorePreviousProfile } from \"./cache\";\nimport { extensionContextOptions } from \"./chromium\";\nimport { createWallet } from \"./controller\";\nimport { DEFAULT_CACHE_DIR, extensionIdFromPath, profileKey } from \"./utils\";\n\nexport type LaunchedWallet = {\n /**\n * Close the browser and remove the throwaway profile copy, in that order. Prefer this over\n * `context.close()`: the copy is a full onboarded profile (tens of MB), and awaiting its removal is\n * the only way to know it is gone before the process exits.\n */\n close: () => Promise<void>;\n context: BrowserContext;\n wallet: Wallet;\n};\n\nconst closeLaunch = async (context: BrowserContext, runDir: string): Promise<void> => {\n const closed = await Promise.allSettled([context.close()]);\n const removed = await Promise.allSettled([rm(runDir, { force: true, recursive: true })]);\n const closeResult = closed[0];\n const removeResult = removed[0];\n\n if (closeResult?.status === \"rejected\" && removeResult?.status === \"rejected\") {\n throw new AggregateError(\n [closeResult.reason, removeResult.reason],\n `[walletwright] failed to close the browser and remove ${runDir}`,\n );\n }\n if (closeResult?.status === \"rejected\") {\n throw closeResult.reason;\n }\n if (removeResult?.status === \"rejected\") {\n throw removeResult.reason;\n }\n};\n\n/** Close Chromium's blank tab and the extension's auto-opened tab. */\nconst closeStrayPages = async (context: BrowserContext, home: Page): Promise<void> => {\n const isStray = (page: Page): boolean =>\n page !== home &&\n !page.isClosed() &&\n (page.url() === \"about:blank\" || page.url().startsWith(\"chrome-extension://\"));\n\n const closing: Array<Promise<void>> = [];\n for (const page of context.pages()) {\n if (isStray(page)) {\n closing.push(page.close());\n }\n }\n await Promise.allSettled(closing);\n};\n\n/** Launch and unlock a disposable profile copy. Headless mode requires verified wallet support. */\nexport const launchWallet = async (\n setup: WalletSetup,\n { headless = false }: { headless?: boolean } = {},\n): Promise<LaunchedWallet> => {\n const definition = wallets[setup.wallet];\n if (headless && definition.headlessApprovals !== true) {\n throw new Error(\n `[walletwright] ${definition.extensionName} has no verified headless approval flow; run this suite headed (\\`use: { headless: false }\\`, or \\`--headed\\`).`,\n );\n }\n const cacheDir = path.resolve(setup.cacheDir ?? DEFAULT_CACHE_DIR);\n const profileDir = path.join(cacheDir, profileKey(setup));\n await restorePreviousProfile(profileDir);\n try {\n await stat(profileDir);\n } catch (error) {\n throw new Error(\n `[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \\`walletwright cache\\`.`,\n { cause: error },\n );\n }\n\n const extensionPath = await definition.prepareExtension(cacheDir, setup.version);\n\n const extensionId = await extensionIdFromPath(extensionPath);\n\n /** Cleanup must cover copy and launch failures that occur before a context exists. */\n const runDir = await mkdtemp(path.join(os.tmpdir(), \"walletwright-\"));\n const removeRunDir = (): Promise<void> => rm(runDir, { force: true, recursive: true });\n const handleContextClose = async (): Promise<void> => {\n try {\n await removeRunDir();\n } catch {\n // Playwright does not await close-event listeners; `close()` owns strict cleanup.\n }\n };\n\n let context: BrowserContext | undefined;\n try {\n await cp(profileDir, runDir, { recursive: true });\n\n context = await chromium.launchPersistentContext(\n runDir,\n extensionContextOptions(extensionPath, headless),\n );\n const launched = context;\n launched.on(\"close\", handleContextClose);\n\n await definition.prepareContext?.(launched);\n const home = await definition.reachUnlockScreen(launched, extensionId);\n await definition.unlock(home, setup.password);\n await closeStrayPages(launched, home);\n\n return {\n close: async () => {\n launched.off(\"close\", handleContextClose);\n await closeLaunch(launched, runDir);\n },\n context: launched,\n wallet: createWallet({\n context: launched,\n definition,\n extensionId,\n home,\n password: setup.password,\n }),\n };\n } catch (error) {\n context?.off(\"close\", handleContextClose);\n await context?.close().catch(() => {});\n await removeRunDir().catch(() => {});\n throw error;\n }\n};\n\nexport { closeLaunch };\n","import { test as base } from \"@playwright/test\";\n\nimport { type LaunchedWallet, launchWallet } from \"./internal/launch\";\nimport type { Wallet, WalletSetup } from \"./types\";\n\nexport type WalletFixtures = {\n wallet: Wallet;\n};\n\n/** One launch per test, which the `context` and `wallet` fixtures then read from. */\ntype LaunchFixture = {\n walletLaunch: LaunchedWallet;\n};\n\n/** Create Playwright fixtures backed by one unlocked wallet launch per test. */\nexport const createWalletFixtures = (setup: WalletSetup) =>\n base.extend<LaunchFixture & WalletFixtures>({\n context: async ({ walletLaunch }, use) => {\n await use(walletLaunch.context);\n },\n wallet: async ({ walletLaunch }, use) => {\n await use(walletLaunch.wallet);\n },\n walletLaunch: async ({ headless }, use) => {\n const launched = await launchWallet(setup, { headless });\n await use(launched);\n await launched.close();\n },\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["base"],"sources":["../src/internal/popup.ts","../src/internal/controller.ts","../src/internal/unlock-screen.ts","../src/internal/launch.ts","../src/fixtures.ts"],"sourcesContent":["import type { BrowserContext, Locator, Page } from \"@playwright/test\";\n\nimport { waitUntil } from \"./wait\";\n\n/** MetaMask/Phantom approval-popup URL token; Slush overrides it with `notificationMatch: \"isPopup=1\"`. */\nexport const DEFAULT_NOTIFICATION_MATCH = \"notification.html\";\n\n/**\n * Approval popups open as `about:blank` and then navigate, so `waitForEvent('page', { predicate })`\n * misses them, poll the open pages instead. `match` is the URL token identifying the popup\n * (`notification.html` for MetaMask/Phantom; `isPopup=1` for Slush's single-page `index.html`).\n */\nexport const isApprovalPopup = (page: Page, extensionId: string, match: string): boolean =>\n page.url().startsWith(`chrome-extension://${extensionId}`) &&\n page.url().includes(match) &&\n !page.isClosed();\n\nconst isVisible = async (locator: Locator): Promise<boolean> => {\n try {\n return await locator.isVisible();\n } catch {\n return false; // the page can go while we ask\n }\n};\n\nconst hasVisibleButton = (page: Page): Promise<boolean> =>\n isVisible(page.locator(\"button\").first());\n\nexport const findNotificationPopup = ({\n approvalControls,\n context,\n extensionId,\n match,\n timeoutMs,\n}: {\n /** From `WalletDefinition.approvalControls`, for a wallet that can render its popup requestless. */\n approvalControls: ((page: Page) => Locator) | undefined;\n context: BrowserContext;\n extensionId: string;\n match: string;\n timeoutMs: number;\n}): Promise<Page | undefined> =>\n waitUntil(\n async () => {\n const popup = context.pages().find((page) => isApprovalPopup(page, extensionId, match));\n if (!popup) {\n return undefined;\n }\n await popup.waitForLoadState(\"domcontentloaded\").catch(() => {});\n const ready =\n approvalControls === undefined\n ? await hasVisibleButton(popup)\n : await isVisible(approvalControls(popup).first());\n return ready ? popup : undefined;\n },\n { timeoutMs },\n );\n\n/** MetaMask's own popup dimensions, which its layout is built for. */\nconst APPROVAL_WINDOW = { height: 592, width: 360 };\n/** Far enough from the edge that a popup this size lands fully on any usable display. */\nconst APPROVAL_WINDOW_OFFSET = 50;\n\n/** Move approval popups on-screen; headless and unsupported CDP targets are ignored. */\nexport const placeApprovalWindow = async (page: Page): Promise<void> => {\n await page.setViewportSize(APPROVAL_WINDOW).catch(() => {});\n try {\n const session = await page.context().newCDPSession(page);\n const { targetInfo } = await session.send(\"Target.getTargetInfo\");\n const { windowId } = await session.send(\"Browser.getWindowForTarget\", {\n targetId: targetInfo.targetId,\n });\n await session.send(\"Browser.setWindowBounds\", {\n bounds: { left: APPROVAL_WINDOW_OFFSET, top: APPROVAL_WINDOW_OFFSET },\n windowId,\n });\n await session.detach();\n } catch {\n // no window to place (headless), or the page refused a CDP session\n }\n};\n","import type { BrowserContext, Page } from \"@playwright/test\";\n\nimport type { Wallet, WalletActionContext, WalletDefinition } from \"../types\";\n\nimport { DEFAULT_NOTIFICATION_MATCH, findNotificationPopup, placeApprovalWindow } from \"./popup\";\nimport { formatTimeout, waitUntil } from \"./wait\";\n\nconst POPUP_CLOSE_TIMEOUT_MS = 15_000;\n/** Optional approvals keep a short wait: \"no popup\" is a normal outcome, and the wait is latency. */\nconst OPTIONAL_POPUP_TIMEOUT_MS = 10_000;\n/** A popup that has to appear gets the full wait: the MV3 service worker can be slow to spawn it. */\nconst REQUIRED_POPUP_TIMEOUT_MS = 30_000;\ntype ResolveOptions = { optional?: boolean };\n\n/**\n * Runs one named wallet call. The Playwright fixtures report each call as a boxed `test.step`;\n * `launchWallet` has no test to report into, so it runs the call directly.\n */\nexport type StepRunner = (title: string, body: () => Promise<void>) => Promise<void>;\n\nexport const runDirectly: StepRunner = (_title, body) => body();\n\n/**\n * Drive `popup` with `settle` and return the page that actually took the approval. A popup that\n * closes mid-settle is the wallet respawning the request, so re-acquire once and drive the new one.\n */\nconst settleApproval = async (\n settle: (popup: Page) => Promise<void>,\n popup: Page,\n acquire: () => Promise<Page | undefined>,\n): Promise<Page> => {\n try {\n await settle(popup);\n return popup;\n } catch (error) {\n if (!popup.isClosed()) {\n throw error;\n }\n const fresh = await acquire();\n if (!fresh) {\n throw error;\n }\n await settle(fresh);\n return fresh;\n }\n};\n\ntype CreateWalletOptions = {\n context: BrowserContext;\n definition: WalletDefinition;\n extensionId: string;\n /** The wallet's own page, left open after unlock, that the actions drive. */\n home: Page;\n password: string;\n step: StepRunner;\n /** Unlock `home` again through the wallet's unlock screen. */\n unlock: () => Promise<void>;\n};\n\n/** Build the runtime controller that drives an unlocked wallet against the dapp under test. */\nexport const createWallet = ({\n context,\n definition,\n extensionId,\n home,\n password,\n step,\n unlock,\n}: CreateWalletOptions): Wallet => {\n const match = definition.notificationMatch ?? DEFAULT_NOTIFICATION_MATCH;\n const ctx: WalletActionContext = { context, extensionId, home, password, unlock };\n\n /** MetaMask opens requests inline while its extension page owns focus. */\n const frontDapp = async (): Promise<void> => {\n const dapp = context.pages().find((page) => /^https?:/v.test(page.url()) && !page.isClosed());\n await dapp?.bringToFront().catch(() => {});\n };\n\n /** Find and place the approval window before its controls are driven. */\n const acquireApproval = async (optional: boolean): Promise<Page | undefined> => {\n const popup = await findNotificationPopup({\n approvalControls: definition.approvalControls,\n context,\n extensionId,\n match,\n timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS,\n });\n if (popup) {\n await placeApprovalWindow(popup);\n }\n return popup;\n };\n\n /** Drive the pending approval popup with `settle`, then wait for that popup to close. */\n const resolvePopup = async (\n settle: (popup: Page) => Promise<void>,\n { optional = false }: ResolveOptions,\n ): Promise<void> => {\n await frontDapp();\n const acquire = () => acquireApproval(optional);\n const popup = await acquire();\n if (!popup) {\n if (optional) {\n return; // e.g. Phantom auto-approves an already-trusted site (no popup)\n }\n const open = context.pages().map((page) => page.url());\n throw new Error(\n `[walletwright] approval popup did not appear. Open pages: ${open.join(\" | \") || \"none\"}`,\n );\n }\n\n const driven = await settleApproval(settle, popup, acquire);\n\n /** Wait for the exact popup we drove; chained popups may reuse the same URL. */\n const closed = await waitUntil(() => driven.isClosed() || undefined, {\n timeoutMs: POPUP_CLOSE_TIMEOUT_MS,\n });\n if (closed === undefined) {\n throw new Error(\n `[walletwright] approval popup did not close after ${formatTimeout(POPUP_CLOSE_TIMEOUT_MS)} (the approval may not have registered)`,\n );\n }\n };\n\n /**\n * Every public call goes through here, so each one reaches `step` under its own name. It awaits\n * `step` itself because Playwright reports a boxed step at the caller of the function awaiting it,\n * which makes that the spec line; another async layer in between would move it into this package.\n */\n const named =\n <Args extends ReadonlyArray<unknown>>(name: string, fn: (...args: Args) => Promise<void>) =>\n async (...args: Args): Promise<void> => {\n await step(`wallet.${name}`, () => fn(...args));\n };\n\n const approve = (options: ResolveOptions = {}) =>\n resolvePopup((popup) => definition.approve(popup, password), options);\n\n const reject = (options: ResolveOptions = {}) =>\n resolvePopup((popup) => definition.reject(popup), options);\n\n /** Bind a verified wallet action and restore focus to the dapp when it finishes. */\n const action = <Args extends ReadonlyArray<unknown>>(\n name: string,\n fn: ((ctx: WalletActionContext, ...args: Args) => Promise<void>) | undefined,\n ) =>\n named(name, async (...args: Args): Promise<void> => {\n if (!fn) {\n throw new Error(`[walletwright] ${definition.extensionName} does not support ${name}()`);\n }\n if (home.isClosed()) {\n throw new Error(`[walletwright] wallet home page is closed; cannot run ${name}()`);\n }\n await home.bringToFront().catch(() => {});\n await fn(ctx, ...args);\n await frontDapp();\n });\n\n const { accounts, network, settings } = definition.actions ?? {};\n\n return {\n accounts: {\n add: action(\"accounts.add\", accounts?.add),\n importPrivateKey: action(\"accounts.importPrivateKey\", accounts?.importPrivateKey),\n rename: action(\"accounts.rename\", accounts?.rename),\n switch: action(\"accounts.switch\", accounts?.switch),\n },\n approve: named(\"approve\", approve),\n confirmSignature: named(\"confirmSignature\", () => approve()),\n confirmTransaction: named(\"confirmTransaction\", () => approve()),\n connectToDapp: named(\"connectToDapp\", approve),\n extensionId,\n home,\n network: {\n add: action(\"network.add\", network?.add),\n },\n reject: named(\"reject\", reject),\n rejectConnection: named(\"rejectConnection\", () => reject()),\n rejectSignature: named(\"rejectSignature\", () => reject()),\n rejectTransaction: named(\"rejectTransaction\", () => reject()),\n settings: {\n lock: action(\"settings.lock\", settings?.lock),\n unlock: action(\"settings.unlock\", settings?.unlock),\n },\n };\n};\n","import type { BrowserContext, Page } from \"@playwright/test\";\n\nimport type { WalletDefinition } from \"../types\";\n\nimport { formatTimeout, gotoWithRetry, waitUntil } from \"./wait\";\n\nconst PASSWORD_FIELD = 'input[type=\"password\"]';\n\nconst NAVIGATION_TIMEOUT_MS = 15_000;\nconst FIRST_SETTLE_TIMEOUT_MS = 20_000;\nconst RELOAD_ATTEMPTS = 5;\nconst RELOAD_SETTLE_TIMEOUT_MS = 5000;\nconst PROBE_INTERVAL_MS = 500;\nconst PASSWORD_CLEARED_TIMEOUT_MS = 15_000;\n/**\n * Short on purpose. `unlock` is normally handed a page `reachUnlockScreen` already settled, so this\n * budget only matters when it is called on its own (the `settings.unlock` path), where a wallet with a\n * late-mounting UI needs a poll rather than a single read but a dead page should still fail fast.\n */\nconst UNLOCK_SETTLE_TIMEOUT_MS = 5000;\n\ntype UnlockFlow = {\n reachUnlockScreen: (context: BrowserContext, extensionId: string) => Promise<Page>;\n unlock: (page: Page, password: string) => Promise<void>;\n};\n\n/**\n * The shared unlock flow: open the wallet's own page, wait for it to settle into a state we can\n * name, and type the password. Every wallet differs only in its `unlockScreen`: the entry page, how\n * it submits, and whether it can come back already unlocked.\n */\nexport const createUnlockScreen = ({\n extensionName: wallet,\n unlockScreen: { entry, isUnlocked, submit },\n}: Pick<WalletDefinition, \"extensionName\" | \"unlockScreen\">): UnlockFlow => {\n const settled = async (page: Page): Promise<\"locked\" | \"unlocked\" | undefined> => {\n const locked = await page\n .locator(PASSWORD_FIELD)\n .isVisible()\n .catch(() => false);\n if (locked) {\n return \"locked\";\n }\n const unlocked = await isUnlocked?.(page).catch(() => false);\n return unlocked === true ? \"unlocked\" : undefined;\n };\n\n const settleWithin = (page: Page, timeoutMs: number) =>\n waitUntil(() => settled(page), { intervalMs: PROBE_INTERVAL_MS, timeoutMs });\n\n const reachUnlockScreen = async (context: BrowserContext, extensionId: string): Promise<Page> => {\n const page = await context.newPage();\n await gotoWithRetry(page, `chrome-extension://${extensionId}/${entry}`, {\n label: `${wallet} ${entry}`,\n timeoutMs: NAVIGATION_TIMEOUT_MS,\n });\n\n let state = await settleWithin(page, FIRST_SETTLE_TIMEOUT_MS);\n for (let attempt = 0; attempt < RELOAD_ATTEMPTS && state === undefined; attempt++) {\n await page.reload().catch(() => {});\n state = await settleWithin(page, RELOAD_SETTLE_TIMEOUT_MS);\n }\n if (state === undefined) {\n throw new Error(\n `[walletwright] ${wallet} never reached its unlock screen (gave up after ${formatTimeout(\n FIRST_SETTLE_TIMEOUT_MS + RELOAD_ATTEMPTS * RELOAD_SETTLE_TIMEOUT_MS,\n )})`,\n );\n }\n return page;\n };\n\n const unlock = async (page: Page, password: string): Promise<void> => {\n // Through `settleWithin`, so there is one answer to \"what state is this page in\" and it waits.\n const state = await settleWithin(page, UNLOCK_SETTLE_TIMEOUT_MS);\n if (state === \"unlocked\") {\n return;\n }\n if (state === undefined) {\n throw new Error(\n `[walletwright] ${wallet} showed neither an unlock screen nor an unlocked wallet`,\n );\n }\n\n const field = page.locator(PASSWORD_FIELD);\n await field.fill(password);\n await (submit === undefined ? field.press(\"Enter\") : submit(page, field));\n const cleared = await field\n .waitFor({ state: \"hidden\", timeout: PASSWORD_CLEARED_TIMEOUT_MS })\n .then(() => true)\n .catch(() => false);\n if (!cleared) {\n throw new Error(\n `[walletwright] ${wallet} unlock failed (password screen still visible after ${formatTimeout(\n PASSWORD_CLEARED_TIMEOUT_MS,\n )})`,\n );\n }\n };\n\n return { reachUnlockScreen, unlock };\n};\n","import { cp, mkdtemp, rm } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport type { BrowserContext, Page } from \"@playwright/test\";\n\nimport type { Wallet, WalletSetup } from \"../types\";\nimport { wallets } from \"../wallets/index\";\n\nimport { openWalletContext } from \"./browser\";\nimport { createWallet, runDirectly, type StepRunner } from \"./controller\";\nimport { prepareExtension } from \"./extension\";\nimport { pathExists } from \"./fs\";\nimport { locateProfile } from \"./profile\";\nimport { createUnlockScreen } from \"./unlock-screen\";\n\nexport type LaunchedWallet = {\n /** `close()`, so `await using launched = await launchWallet(setup)` cleans up on any exit. */\n [Symbol.asyncDispose]: () => Promise<void>;\n /**\n * Close the browser and remove the throwaway profile copy, in that order. Prefer this over\n * `context.close()`: the copy is a full onboarded profile (tens of MB), and awaiting its removal is\n * the only way to know it is gone before the process exits.\n */\n close: () => Promise<void>;\n context: BrowserContext;\n wallet: Wallet;\n};\n\n/** Close the browser, then remove the profile copy, even when closing fails; report every failure. */\nconst closeLaunch = async (context: BrowserContext, runDir: string): Promise<void> => {\n const errors: Array<unknown> = [];\n try {\n await context.close();\n } catch (error) {\n errors.push(error);\n }\n try {\n await rm(runDir, { force: true, recursive: true });\n } catch (error) {\n errors.push(error);\n }\n\n if (errors.length > 1) {\n throw new AggregateError(\n errors,\n `[walletwright] failed to close the browser and remove ${runDir}`,\n );\n }\n if (errors.length === 1) {\n throw errors[0];\n }\n};\n\n/** Close Chromium's blank tab and the extension's auto-opened tab. */\nconst closeStrayPages = async (context: BrowserContext, home: Page): Promise<void> => {\n const isStray = (page: Page): boolean =>\n page !== home &&\n !page.isClosed() &&\n (page.url() === \"about:blank\" || page.url().startsWith(\"chrome-extension://\"));\n\n const closing: Array<Promise<void>> = [];\n for (const page of context.pages()) {\n if (isStray(page)) {\n closing.push(page.close());\n }\n }\n await Promise.allSettled(closing);\n};\n\n/** `launchWallet`, with the step runner the Playwright fixtures report wallet calls through. */\nexport const launch = async (\n setup: WalletSetup,\n { headless, step }: { headless: boolean; step: StepRunner },\n): Promise<LaunchedWallet> => {\n const definition = wallets[setup.wallet];\n if (headless && definition.headlessApprovals !== true) {\n throw new Error(\n `[walletwright] ${definition.extensionName} has no verified headless approval flow; run this suite headed (\\`use: { headless: false }\\`, or \\`--headed\\`).`,\n );\n }\n const { cacheDir, profileDir } = await locateProfile(setup);\n if (!(await pathExists(profileDir))) {\n throw new Error(\n `[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \\`walletwright cache\\`.`,\n );\n }\n const extensionPath = await prepareExtension(setup, cacheDir);\n\n /** Cleanup must cover copy and launch failures that occur before a context exists. */\n const runDir = await mkdtemp(path.join(os.tmpdir(), \"walletwright-\"));\n const removeRunDir = (): Promise<void> => rm(runDir, { force: true, recursive: true });\n const handleContextClose = async (): Promise<void> => {\n try {\n await removeRunDir();\n } catch {\n // Playwright does not await close-event listeners; `close()` owns strict cleanup.\n }\n };\n\n let context: BrowserContext | undefined;\n try {\n await cp(profileDir, runDir, { recursive: true });\n\n const opened = await openWalletContext({\n definition,\n extensionPath,\n headless,\n userDataDir: runDir,\n });\n const launched = opened.context;\n context = launched;\n launched.on(\"close\", handleContextClose);\n\n const screen = createUnlockScreen(definition);\n const home = await screen.reachUnlockScreen(launched, opened.extensionId);\n const unlock = () => screen.unlock(home, setup.password);\n await unlock();\n await closeStrayPages(launched, home);\n\n const close = async (): Promise<void> => {\n launched.off(\"close\", handleContextClose);\n await closeLaunch(launched, runDir);\n };\n return {\n close,\n context: launched,\n [Symbol.asyncDispose]: close,\n wallet: createWallet({\n context: launched,\n definition,\n extensionId: opened.extensionId,\n home,\n password: setup.password,\n step,\n unlock,\n }),\n };\n } catch (error) {\n context?.off(\"close\", handleContextClose);\n await context?.close().catch(() => {});\n await removeRunDir().catch(() => {});\n throw error;\n }\n};\n\n/** Launch and unlock a disposable profile copy. Headless mode requires verified wallet support. */\nexport const launchWallet = (\n setup: WalletSetup,\n { headless = false }: { headless?: boolean } = {},\n): Promise<LaunchedWallet> => launch(setup, { headless, step: runDirectly });\n\nexport { closeLaunch };\n","import { test as base } from \"@playwright/test\";\n\nimport type { StepRunner } from \"./internal/controller\";\nimport { type LaunchedWallet, launch } from \"./internal/launch\";\nimport type { Wallet, WalletSetup } from \"./types\";\nimport { wallets } from \"./wallets/index\";\n\nexport type WalletFixtures = {\n wallet: Wallet;\n};\n\n/** One launch per test, which the `context` and `wallet` fixtures then read from. */\ntype LaunchFixture = {\n walletLaunch: LaunchedWallet;\n};\n\n/**\n * Report each wallet call as a boxed step, named after it (`wallet.confirmSignature`) and failing at\n * the spec line that made the call. Not `async`: an extra awaiting frame would become that location.\n */\nconst reportAsStep: StepRunner = (title, body) => base.step(title, body, { box: true });\n\n/**\n * Create Playwright fixtures backed by one unlocked wallet launch per test. `headless` applies only\n * to wallets that can be driven that way (Phantom, Rabby); the rest launch headed, so a suite runs\n * under Playwright's defaults. `launchWallet` is the strict form that refuses them headless instead.\n */\nexport const createWalletFixtures = (setup: WalletSetup) => {\n const definition = wallets[setup.wallet];\n return base.extend<LaunchFixture & WalletFixtures>({\n context: [\n async ({ walletLaunch }, use) => {\n await use(walletLaunch.context);\n },\n { box: true, scope: \"test\" },\n ],\n wallet: [\n async ({ walletLaunch }, use) => {\n await use(walletLaunch.wallet);\n },\n { box: true },\n ],\n walletLaunch: [\n async ({ headless }, use) => {\n const launched = await launch(setup, {\n headless: headless && definition.headlessApprovals === true,\n step: reportAsStep,\n });\n await use(launched);\n await launched.close();\n },\n { title: `launch ${definition.extensionName}` },\n ],\n });\n};\n"],"mappings":";;;;;;;;AAKA,MAAa,6BAA6B;;;;;;AAO1C,MAAa,mBAAmB,MAAY,aAAqB,UAC/D,KAAK,IAAI,CAAC,CAAC,WAAW,sBAAsB,aAAa,KACzD,KAAK,IAAI,CAAC,CAAC,SAAS,KAAK,KACzB,CAAC,KAAK,SAAS;AAEjB,MAAM,YAAY,OAAO,YAAuC;CAC9D,IAAI;EACF,OAAO,MAAM,QAAQ,UAAU;CACjC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,oBAAoB,SACxB,UAAU,KAAK,QAAQ,QAAQ,CAAC,CAAC,MAAM,CAAC;AAE1C,MAAa,yBAAyB,EACpC,kBACA,SACA,aACA,OACA,gBASA,UACE,YAAY;CACV,MAAM,QAAQ,QAAQ,MAAM,CAAC,CAAC,MAAM,SAAS,gBAAgB,MAAM,aAAa,KAAK,CAAC;CACtF,IAAI,CAAC,OACH;CAEF,MAAM,MAAM,iBAAiB,kBAAkB,CAAC,CAAC,YAAY,CAAC,CAAC;CAK/D,QAHE,qBAAqB,SACjB,MAAM,iBAAiB,KAAK,IAC5B,MAAM,UAAU,iBAAiB,KAAK,CAAC,CAAC,MAAM,CAAC,KACtC,QAAQ;AACzB,GACA,EAAE,UAAU,CACd;;AAGF,MAAM,kBAAkB;CAAE,QAAQ;CAAK,OAAO;AAAI;;AAElD,MAAM,yBAAyB;;AAG/B,MAAa,sBAAsB,OAAO,SAA8B;CACtE,MAAM,KAAK,gBAAgB,eAAe,CAAC,CAAC,YAAY,CAAC,CAAC;CAC1D,IAAI;EACF,MAAM,UAAU,MAAM,KAAK,QAAQ,CAAC,CAAC,cAAc,IAAI;EACvD,MAAM,EAAE,eAAe,MAAM,QAAQ,KAAK,sBAAsB;EAChE,MAAM,EAAE,aAAa,MAAM,QAAQ,KAAK,8BAA8B,EACpE,UAAU,WAAW,SACvB,CAAC;EACD,MAAM,QAAQ,KAAK,2BAA2B;GAC5C,QAAQ;IAAE,MAAM;IAAwB,KAAK;GAAuB;GACpE;EACF,CAAC;EACD,MAAM,QAAQ,OAAO;CACvB,QAAQ,CAER;AACF;;;;ACzEA,MAAM,yBAAyB;;AAE/B,MAAM,4BAA4B;;AAElC,MAAM,4BAA4B;AASlC,MAAa,eAA2B,QAAQ,SAAS,KAAK;;;;;AAM9D,MAAM,iBAAiB,OACrB,QACA,OACA,YACkB;CAClB,IAAI;EACF,MAAM,OAAO,KAAK;EAClB,OAAO;CACT,SAAS,OAAO;EACd,IAAI,CAAC,MAAM,SAAS,GAClB,MAAM;EAER,MAAM,QAAQ,MAAM,QAAQ;EAC5B,IAAI,CAAC,OACH,MAAM;EAER,MAAM,OAAO,KAAK;EAClB,OAAO;CACT;AACF;;AAeA,MAAa,gBAAgB,EAC3B,SACA,YACA,aACA,MACA,UACA,MACA,aACiC;CACjC,MAAM,QAAQ,WAAW;CACzB,MAAM,MAA2B;EAAE;EAAS;EAAa;EAAM;EAAU;CAAO;;CAGhF,MAAM,YAAY,YAA2B;EAE3C,MADa,QAAQ,MAAM,CAAC,CAAC,MAAM,SAAS,0BAAU,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,KAAK,SAAS,CAClF,CAAC,EAAE,aAAa,CAAC,CAAC,YAAY,CAAC,CAAC;CAC3C;;CAGA,MAAM,kBAAkB,OAAO,aAAiD;EAC9E,MAAM,QAAQ,MAAM,sBAAsB;GACxC,kBAAkB,WAAW;GAC7B;GACA;GACA;GACA,WAAW,WAAW,4BAA4B;EACpD,CAAC;EACD,IAAI,OACF,MAAM,oBAAoB,KAAK;EAEjC,OAAO;CACT;;CAGA,MAAM,eAAe,OACnB,QACA,EAAE,WAAW,YACK;EAClB,MAAM,UAAU;EAChB,MAAM,gBAAgB,gBAAgB,QAAQ;EAC9C,MAAM,QAAQ,MAAM,QAAQ;EAC5B,IAAI,CAAC,OAAO;GACV,IAAI,UACF;GAEF,MAAM,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,SAAS,KAAK,IAAI,CAAC;GACrD,MAAM,IAAI,MACR,6DAA6D,KAAK,KAAK,KAAK,KAAK,QACnF;EACF;EAEA,MAAM,SAAS,MAAM,eAAe,QAAQ,OAAO,OAAO;EAM1D,IAAI,MAHiB,gBAAgB,OAAO,SAAS,KAAK,QAAW,EACnE,WAAW,uBACb,CAAC,MACc,QACb,MAAM,IAAI,MACR,qDAAqD,cAAc,sBAAsB,EAAE,wCAC7F;CAEJ;;;;;;CAOA,MAAM,SACkC,MAAc,OACpD,OAAO,GAAG,SAA8B;EACtC,MAAM,KAAK,UAAU,cAAc,GAAG,GAAG,IAAI,CAAC;CAChD;CAEF,MAAM,WAAW,UAA0B,CAAC,MAC1C,cAAc,UAAU,WAAW,QAAQ,OAAO,QAAQ,GAAG,OAAO;CAEtE,MAAM,UAAU,UAA0B,CAAC,MACzC,cAAc,UAAU,WAAW,OAAO,KAAK,GAAG,OAAO;;CAG3D,MAAM,UACJ,MACA,OAEA,MAAM,MAAM,OAAO,GAAG,SAA8B;EAClD,IAAI,CAAC,IACH,MAAM,IAAI,MAAM,kBAAkB,WAAW,cAAc,oBAAoB,KAAK,GAAG;EAEzF,IAAI,KAAK,SAAS,GAChB,MAAM,IAAI,MAAM,yDAAyD,KAAK,GAAG;EAEnF,MAAM,KAAK,aAAa,CAAC,CAAC,YAAY,CAAC,CAAC;EACxC,MAAM,GAAG,KAAK,GAAG,IAAI;EACrB,MAAM,UAAU;CAClB,CAAC;CAEH,MAAM,EAAE,UAAU,SAAS,aAAa,WAAW,WAAW,CAAC;CAE/D,OAAO;EACL,UAAU;GACR,KAAK,OAAO,gBAAgB,UAAU,GAAG;GACzC,kBAAkB,OAAO,6BAA6B,UAAU,gBAAgB;GAChF,QAAQ,OAAO,mBAAmB,UAAU,MAAM;GAClD,QAAQ,OAAO,mBAAmB,UAAU,MAAM;EACpD;EACA,SAAS,MAAM,WAAW,OAAO;EACjC,kBAAkB,MAAM,0BAA0B,QAAQ,CAAC;EAC3D,oBAAoB,MAAM,4BAA4B,QAAQ,CAAC;EAC/D,eAAe,MAAM,iBAAiB,OAAO;EAC7C;EACA;EACA,SAAS,EACP,KAAK,OAAO,eAAe,SAAS,GAAG,EACzC;EACA,QAAQ,MAAM,UAAU,MAAM;EAC9B,kBAAkB,MAAM,0BAA0B,OAAO,CAAC;EAC1D,iBAAiB,MAAM,yBAAyB,OAAO,CAAC;EACxD,mBAAmB,MAAM,2BAA2B,OAAO,CAAC;EAC5D,UAAU;GACR,MAAM,OAAO,iBAAiB,UAAU,IAAI;GAC5C,QAAQ,OAAO,mBAAmB,UAAU,MAAM;EACpD;CACF;AACF;;;;ACnLA,MAAM,iBAAiB;AAEvB,MAAM,wBAAwB;AAC9B,MAAM,0BAA0B;AAChC,MAAM,kBAAkB;AACxB,MAAM,2BAA2B;AACjC,MAAM,oBAAoB;AAC1B,MAAM,8BAA8B;;;;;;AAMpC,MAAM,2BAA2B;;;;;;AAYjC,MAAa,sBAAsB,EACjC,eAAe,QACf,cAAc,EAAE,OAAO,YAAY,eACuC;CAC1E,MAAM,UAAU,OAAO,SAA2D;EAKhF,IAAI,MAJiB,KAClB,QAAQ,cAAc,CAAC,CACvB,UAAU,CAAC,CACX,YAAY,KAAK,GAElB,OAAO;EAGT,OAAO,MADgB,aAAa,IAAI,CAAC,CAAC,YAAY,KAAK,MACvC,OAAO,aAAa;CAC1C;CAEA,MAAM,gBAAgB,MAAY,cAChC,gBAAgB,QAAQ,IAAI,GAAG;EAAE,YAAY;EAAmB;CAAU,CAAC;CAE7E,MAAM,oBAAoB,OAAO,SAAyB,gBAAuC;EAC/F,MAAM,OAAO,MAAM,QAAQ,QAAQ;EACnC,MAAM,cAAc,MAAM,sBAAsB,YAAY,GAAG,SAAS;GACtE,OAAO,GAAG,OAAO,GAAG;GACpB,WAAW;EACb,CAAC;EAED,IAAI,QAAQ,MAAM,aAAa,MAAM,uBAAuB;EAC5D,KAAK,IAAI,UAAU,GAAG,UAAU,mBAAmB,UAAU,QAAW,WAAW;GACjF,MAAM,KAAK,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;GAClC,QAAQ,MAAM,aAAa,MAAM,wBAAwB;EAC3D;EACA,IAAI,UAAU,QACZ,MAAM,IAAI,MACR,kBAAkB,OAAO,kDAAkD,cACzE,IACF,EAAE,EACJ;EAEF,OAAO;CACT;CAEA,MAAM,SAAS,OAAO,MAAY,aAAoC;EAEpE,MAAM,QAAQ,MAAM,aAAa,MAAM,wBAAwB;EAC/D,IAAI,UAAU,YACZ;EAEF,IAAI,UAAU,QACZ,MAAM,IAAI,MACR,kBAAkB,OAAO,wDAC3B;EAGF,MAAM,QAAQ,KAAK,QAAQ,cAAc;EACzC,MAAM,MAAM,KAAK,QAAQ;EACzB,OAAO,WAAW,SAAY,MAAM,MAAM,OAAO,IAAI,OAAO,MAAM,KAAK;EAKvE,IAAI,CAAC,MAJiB,MACnB,QAAQ;GAAE,OAAO;GAAU,SAAS;EAA4B,CAAC,CAAC,CAClE,WAAW,IAAI,CAAC,CAChB,YAAY,KAAK,GAElB,MAAM,IAAI,MACR,kBAAkB,OAAO,sDAAsD,cAC7E,2BACF,EAAE,EACJ;CAEJ;CAEA,OAAO;EAAE;EAAmB;CAAO;AACrC;;;;;ACvEA,MAAM,cAAc,OAAO,SAAyB,WAAkC;CACpF,MAAM,SAAyB,CAAC;CAChC,IAAI;EACF,MAAM,QAAQ,MAAM;CACtB,SAAS,OAAO;EACd,OAAO,KAAK,KAAK;CACnB;CACA,IAAI;EACF,MAAM,GAAG,QAAQ;GAAE,OAAO;GAAM,WAAW;EAAK,CAAC;CACnD,SAAS,OAAO;EACd,OAAO,KAAK,KAAK;CACnB;CAEA,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,eACR,QACA,yDAAyD,QAC3D;CAEF,IAAI,OAAO,WAAW,GACpB,MAAM,OAAO;AAEjB;;AAGA,MAAM,kBAAkB,OAAO,SAAyB,SAA8B;CACpF,MAAM,WAAW,SACf,SAAS,QACT,CAAC,KAAK,SAAS,MACd,KAAK,IAAI,MAAM,iBAAiB,KAAK,IAAI,CAAC,CAAC,WAAW,qBAAqB;CAE9E,MAAM,UAAgC,CAAC;CACvC,KAAK,MAAM,QAAQ,QAAQ,MAAM,GAC/B,IAAI,QAAQ,IAAI,GACd,QAAQ,KAAK,KAAK,MAAM,CAAC;CAG7B,MAAM,QAAQ,WAAW,OAAO;AAClC;;AAGA,MAAa,SAAS,OACpB,OACA,EAAE,UAAU,WACgB;CAC5B,MAAM,aAAa,QAAQ,MAAM;CACjC,IAAI,YAAY,WAAW,sBAAsB,MAC/C,MAAM,IAAI,MACR,kBAAkB,WAAW,cAAc,gHAC7C;CAEF,MAAM,EAAE,UAAU,eAAe,MAAM,cAAc,KAAK;CAC1D,IAAI,CAAE,MAAM,WAAW,UAAU,GAC/B,MAAM,IAAI,MACR,6CAA6C,WAAW,8DAC1D;CAEF,MAAM,gBAAgB,MAAM,iBAAiB,OAAO,QAAQ;;CAG5D,MAAM,SAAS,MAAM,QAAQ,KAAK,KAAK,GAAG,OAAO,GAAG,eAAe,CAAC;CACpE,MAAM,qBAAoC,GAAG,QAAQ;EAAE,OAAO;EAAM,WAAW;CAAK,CAAC;CACrF,MAAM,qBAAqB,YAA2B;EACpD,IAAI;GACF,MAAM,aAAa;EACrB,QAAQ,CAER;CACF;CAEA,IAAI;CACJ,IAAI;EACF,MAAM,GAAG,YAAY,QAAQ,EAAE,WAAW,KAAK,CAAC;EAEhD,MAAM,SAAS,MAAM,kBAAkB;GACrC;GACA;GACA;GACA,aAAa;EACf,CAAC;EACD,MAAM,WAAW,OAAO;EACxB,UAAU;EACV,SAAS,GAAG,SAAS,kBAAkB;EAEvC,MAAM,SAAS,mBAAmB,UAAU;EAC5C,MAAM,OAAO,MAAM,OAAO,kBAAkB,UAAU,OAAO,WAAW;EACxE,MAAM,eAAe,OAAO,OAAO,MAAM,MAAM,QAAQ;EACvD,MAAM,OAAO;EACb,MAAM,gBAAgB,UAAU,IAAI;EAEpC,MAAM,QAAQ,YAA2B;GACvC,SAAS,IAAI,SAAS,kBAAkB;GACxC,MAAM,YAAY,UAAU,MAAM;EACpC;EACA,OAAO;GACL;GACA,SAAS;IACR,OAAO,eAAe;GACvB,QAAQ,aAAa;IACnB,SAAS;IACT;IACA,aAAa,OAAO;IACpB;IACA,UAAU,MAAM;IAChB;IACA;GACF,CAAC;EACH;CACF,SAAS,OAAO;EACd,SAAS,IAAI,SAAS,kBAAkB;EACxC,MAAM,SAAS,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;EACrC,MAAM,aAAa,CAAC,CAAC,YAAY,CAAC,CAAC;EACnC,MAAM;CACR;AACF;;AAGA,MAAa,gBACX,OACA,EAAE,WAAW,UAAkC,CAAC,MACpB,OAAO,OAAO;CAAE;CAAU,MAAM;AAAY,CAAC;;;;;;;;AClI3E,MAAM,gBAA4B,OAAO,SAASA,KAAK,KAAK,OAAO,MAAM,EAAE,KAAK,KAAK,CAAC;;;;;;AAOtF,MAAa,wBAAwB,UAAuB;CAC1D,MAAM,aAAa,QAAQ,MAAM;CACjC,OAAOA,KAAK,OAAuC;EACjD,SAAS,CACP,OAAO,EAAE,gBAAgB,QAAQ;GAC/B,MAAM,IAAI,aAAa,OAAO;EAChC,GACA;GAAE,KAAK;GAAM,OAAO;EAAO,CAC7B;EACA,QAAQ,CACN,OAAO,EAAE,gBAAgB,QAAQ;GAC/B,MAAM,IAAI,aAAa,MAAM;EAC/B,GACA,EAAE,KAAK,KAAK,CACd;EACA,cAAc,CACZ,OAAO,EAAE,YAAY,QAAQ;GAC3B,MAAM,WAAW,MAAM,OAAO,OAAO;IACnC,UAAU,YAAY,WAAW,sBAAsB;IACvD,MAAM;GACR,CAAC;GACD,MAAM,IAAI,QAAQ;GAClB,MAAM,SAAS,MAAM;EACvB,GACA,EAAE,OAAO,UAAU,WAAW,gBAAgB,CAChD;CACF,CAAC;AACH"}
|
package/dist/mock-standard.d.mts
CHANGED
|
@@ -20,6 +20,11 @@ type BridgeRequest = {
|
|
|
20
20
|
method: string;
|
|
21
21
|
};
|
|
22
22
|
declare const createStandardHandler: (seed: Buffer) => ({ message, method }: BridgeRequest) => Promise<Array<number>>;
|
|
23
|
+
/**
|
|
24
|
+
* Install the headless Wallet-Standard mock for Solana connect + sign on a context (every page) or a
|
|
25
|
+
* single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.
|
|
26
|
+
* Returns the account it announces so a test can verify signatures against the public key.
|
|
27
|
+
*/
|
|
23
28
|
declare const installMockStandardWallet: (target: BrowserContext | Page, options?: MockStandardWalletOptions) => Promise<MockStandardAccount>;
|
|
24
29
|
//#endregion
|
|
25
30
|
export { type MockStandardAccount, type MockStandardWalletOptions, createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };
|
package/dist/mock-standard.mjs
CHANGED
|
@@ -10,8 +10,8 @@ const encodeBase58 = (bytes) => {
|
|
|
10
10
|
const digits = [];
|
|
11
11
|
for (const byte of bytes) {
|
|
12
12
|
let carry = byte;
|
|
13
|
-
for (
|
|
14
|
-
carry +=
|
|
13
|
+
for (const [index, digit] of digits.entries()) {
|
|
14
|
+
carry += digit * 256;
|
|
15
15
|
digits[index] = carry % 58;
|
|
16
16
|
carry = Math.floor(carry / 58);
|
|
17
17
|
}
|
|
@@ -41,13 +41,13 @@ const createStandardHandler = (seed) => ({ message, method }) => {
|
|
|
41
41
|
default: return Promise.reject(/* @__PURE__ */ new Error(`[walletwright/mock-standard] unsupported method: ${method}`));
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
+
/** Per install, for the same reason as the EVM mock: see the note on `installCount` in `mock.ts`. */
|
|
45
|
+
let installCount = 0;
|
|
44
46
|
/**
|
|
45
47
|
* Install the headless Wallet-Standard mock for Solana connect + sign on a context (every page) or a
|
|
46
48
|
* single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.
|
|
47
49
|
* Returns the account it announces so a test can verify signatures against the public key.
|
|
48
50
|
*/
|
|
49
|
-
/** Per install, for the same reason as the EVM mock: see the note on `installCount` in `mock.ts`. */
|
|
50
|
-
let installCount = 0;
|
|
51
51
|
const installMockStandardWallet = async (target, options = {}) => {
|
|
52
52
|
const { name = "Walletwright Mock", seedHex = DEFAULT_SEED_HEX } = options;
|
|
53
53
|
const seed = Buffer.from(seedHex, "hex");
|
|
@@ -66,9 +66,11 @@ const installMockStandardWallet = async (target, options = {}) => {
|
|
|
66
66
|
publicKeyHex
|
|
67
67
|
}];
|
|
68
68
|
await target.addInitScript(([binding, info]) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const assertBinding = (value) => {
|
|
70
|
+
if (typeof value !== "function") throw new TypeError(`Missing Playwright binding: ${binding}`);
|
|
71
|
+
};
|
|
72
|
+
const call = Object.getOwnPropertyDescriptor(window, binding)?.value;
|
|
73
|
+
assertBinding(call);
|
|
72
74
|
const publicKeyBytes = Uint8Array.from((info.publicKeyHex.match(new RegExp(".{2}", "gv")) ?? []).map((byte) => Number.parseInt(byte, 16)));
|
|
73
75
|
const account = {
|
|
74
76
|
address: info.address,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-standard.mjs","names":[],"sources":["../src/mock-standard.ts"],"sourcesContent":["import { createPrivateKey, createPublicKey, sign } from \"node:crypto\";\n\nimport type { BrowserContext, Page } from \"@playwright/test\";\n\n/** Headless Wallet-Standard Solana mock that signs messages with Node's ed25519 implementation. */\ntype MockStandardWalletOptions = {\n /** Wallet name announced over Wallet Standard. */\n name?: string;\n /** 32-byte ed25519 seed as hex. Defaults to a fixed test seed so the address is stable. */\n seedHex?: string;\n};\n\ntype MockStandardAccount = {\n address: string;\n /** Raw 32-byte ed25519 public key, hex-encoded. */\n publicKeyHex: string;\n};\n\nconst DEFAULT_SEED_HEX = \"0101010101010101010101010101010101010101010101010101010101010101\";\n\nconst PKCS8_ED25519_PREFIX = Buffer.from(\"302e020100300506032b657004220420\", \"hex\");\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nconst encodeBase58 = (bytes: Uint8Array): string => {\n let leadingZeros = 0;\n while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) {\n leadingZeros += 1;\n }\n const digits: Array<number> = [];\n for (const byte of bytes) {\n let carry = byte;\n for (
|
|
1
|
+
{"version":3,"file":"mock-standard.mjs","names":[],"sources":["../src/mock-standard.ts"],"sourcesContent":["import { createPrivateKey, createPublicKey, sign } from \"node:crypto\";\n\nimport type { BrowserContext, Page } from \"@playwright/test\";\n\n/** Headless Wallet-Standard Solana mock that signs messages with Node's ed25519 implementation. */\ntype MockStandardWalletOptions = {\n /** Wallet name announced over Wallet Standard. */\n name?: string;\n /** 32-byte ed25519 seed as hex. Defaults to a fixed test seed so the address is stable. */\n seedHex?: string;\n};\n\ntype MockStandardAccount = {\n address: string;\n /** Raw 32-byte ed25519 public key, hex-encoded. */\n publicKeyHex: string;\n};\n\nconst DEFAULT_SEED_HEX = \"0101010101010101010101010101010101010101010101010101010101010101\";\n\nconst PKCS8_ED25519_PREFIX = Buffer.from(\"302e020100300506032b657004220420\", \"hex\");\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nconst encodeBase58 = (bytes: Uint8Array): string => {\n let leadingZeros = 0;\n while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) {\n leadingZeros += 1;\n }\n const digits: Array<number> = [];\n for (const byte of bytes) {\n let carry = byte;\n for (const [index, digit] of digits.entries()) {\n carry += digit * 256;\n digits[index] = carry % 58;\n carry = Math.floor(carry / 58);\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = Math.floor(carry / 58);\n }\n }\n const prefix = \"1\".repeat(leadingZeros);\n const body = digits\n .toReversed()\n .map((digit) => BASE58_ALPHABET[digit])\n .join(\"\");\n return prefix + body;\n};\n\nconst privateKeyFromSeed = (seed: Buffer) =>\n createPrivateKey({\n format: \"der\",\n key: Buffer.concat([PKCS8_ED25519_PREFIX, seed]),\n type: \"pkcs8\",\n });\n\nconst rawPublicKey = (seed: Buffer): Buffer => {\n const spki = createPublicKey(privateKeyFromSeed(seed)).export({ format: \"der\", type: \"spki\" });\n return spki.subarray(-32);\n};\n\nconst signEd25519 = (seed: Buffer, message: Uint8Array): Array<number> => [\n ...sign(null, Buffer.from(message), privateKeyFromSeed(seed)),\n];\n\nconst SVM_CHAINS = [\"solana:mainnet\", \"solana:devnet\"] as const;\n\n/** `message` is required: defaulting it would sign the empty message instead of rejecting. */\ntype BridgeRequest = { message: Array<number>; method: string };\ntype StandardInitInfo = {\n address: string;\n chains: ReadonlyArray<string>;\n icon: string;\n name: string;\n publicKeyHex: string;\n};\n\nconst createStandardHandler =\n (seed: Buffer) =>\n ({ message, method }: BridgeRequest): Promise<Array<number>> => {\n switch (method) {\n case \"solana:signMessage\": {\n return Promise.resolve(signEd25519(seed, Uint8Array.from(message)));\n }\n default: {\n return Promise.reject(\n new Error(`[walletwright/mock-standard] unsupported method: ${method}`),\n );\n }\n }\n };\n\n/** Per install, for the same reason as the EVM mock: see the note on `installCount` in `mock.ts`. */\nlet installCount = 0;\n\n/**\n * Install the headless Wallet-Standard mock for Solana connect + sign on a context (every page) or a\n * single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.\n * Returns the account it announces so a test can verify signatures against the public key.\n */\nconst installMockStandardWallet = async (\n target: BrowserContext | Page,\n options: MockStandardWalletOptions = {},\n): Promise<MockStandardAccount> => {\n const { name = \"Walletwright Mock\", seedHex = DEFAULT_SEED_HEX } = options;\n const seed = Buffer.from(seedHex, \"hex\");\n if (seed.length !== 32) {\n throw new Error(\"[walletwright/mock-standard] seedHex must be 32 bytes\");\n }\n const publicKey = rawPublicKey(seed);\n const address = encodeBase58(publicKey);\n const publicKeyHex = publicKey.toString(\"hex\");\n const handle = createStandardHandler(seed);\n\n const bindingName = `__walletwrightMockStandardSign_${installCount++}`;\n await target.exposeFunction(bindingName, (rpc: BridgeRequest) => handle(rpc));\n const initArgs = [\n bindingName,\n {\n address,\n chains: [...SVM_CHAINS],\n icon: \"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>\",\n name,\n publicKeyHex,\n },\n ] satisfies readonly [string, StandardInitInfo];\n\n await target.addInitScript(([binding, info]) => {\n const assertBinding: (\n value: unknown,\n ) => asserts value is (rpc: BridgeRequest) => Promise<Array<number>> = (value) => {\n if (typeof value !== \"function\") {\n throw new TypeError(`Missing Playwright binding: ${binding}`);\n }\n };\n const call: unknown = Object.getOwnPropertyDescriptor(window, binding)?.value;\n assertBinding(call);\n const publicKeyBytes = Uint8Array.from(\n (info.publicKeyHex.match(/.{2}/gv) ?? []).map((byte) => Number.parseInt(byte, 16)),\n );\n const account = {\n address: info.address,\n chains: info.chains,\n features: [\"solana:signMessage\"],\n label: info.name,\n publicKey: publicKeyBytes,\n };\n const wallet = {\n accounts: [account],\n chains: info.chains,\n features: {\n \"solana:signMessage\": {\n signMessage: (...inputs: Array<{ message: Uint8Array }>) =>\n Promise.all(\n inputs.map(async ({ message }) => ({\n signature: Uint8Array.from(\n await call({ message: [...message], method: \"solana:signMessage\" }),\n ),\n signedMessage: message,\n })),\n ),\n version: \"1.0.0\",\n },\n \"standard:connect\": {\n connect: () => Promise.resolve({ accounts: [account] }),\n version: \"1.0.0\",\n },\n \"standard:events\": {\n on: () => () => {},\n version: \"1.0.0\",\n },\n },\n icon: info.icon,\n name: info.name,\n version: \"1.0.0\" as const,\n };\n\n const callback = (api: { register: (w: typeof wallet) => void }) => {\n api.register(wallet);\n };\n try {\n window.dispatchEvent(\n new CustomEvent(\"wallet-standard:register-wallet\", { detail: callback }),\n );\n } catch {\n // The app may not be listening yet; the app-ready handler below covers that case.\n }\n window.addEventListener(\"wallet-standard:app-ready\", (event) => {\n // SAFETY: Wallet Standard dispatches this named event with the register callback contract.\n callback((event as CustomEvent<{ register: (w: typeof wallet) => void }>).detail);\n });\n }, initArgs);\n\n return { address, publicKeyHex };\n};\n\nexport { createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };\nexport type { MockStandardAccount, MockStandardWalletOptions };\n"],"mappings":"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"}
|
package/dist/mock.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { BrowserContext, Page } from "@playwright/test";
|
|
|
2
2
|
import { PrivateKeyAccount } from "viem/accounts";
|
|
3
3
|
//#region src/mock.d.ts
|
|
4
4
|
/** Headless EIP-6963 and EIP-1193 mock that signs with a real key. */
|
|
5
|
-
type MockWalletOptions = {
|
|
5
|
+
export type MockWalletOptions = {
|
|
6
6
|
chainId?: number;
|
|
7
7
|
/** Wallet name announced over EIP-6963. */
|
|
8
8
|
name?: string;
|
|
@@ -22,5 +22,5 @@ declare const createRpcHandler: (account: PrivateKeyAccount, chainIdHex: string)
|
|
|
22
22
|
*/
|
|
23
23
|
declare const installMockWallet: (target: BrowserContext | Page, options?: MockWalletOptions) => Promise<string>;
|
|
24
24
|
//#endregion
|
|
25
|
-
export {
|
|
25
|
+
export { createRpcHandler, installMockWallet };
|
|
26
26
|
//# sourceMappingURL=mock.d.mts.map
|