@walletwright/core 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,15 +1,65 @@
1
- import { a as walletKindsByEcosystem, c as DEFAULT_NOTIFICATION_MATCH, d as placeApprovalWindow, f as profileKey, i as isWalletKind, l as extensionIdFromPath, m as waitUntil, n as restorePreviousProfile, o as wallets, p as formatTimeout, r as extensionContextOptions, s as DEFAULT_CACHE_DIR, t as buildCache, u as findNotificationPopup } from "./cache-BaWu5vyM.mjs";
2
- import { cp, mkdtemp, rm, stat } from "node:fs/promises";
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-ztGj4R8m.mjs";
2
+ import { cp, mkdtemp, rm } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { chromium, test } from "@playwright/test";
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
- /** The MV3 service worker spawns a popup slowly once the wallet's own UI has been driven. */
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
- const unsupported = (name) => /* @__PURE__ */ new Error(`[walletwright] ${definition.extensionName} does not support ${name}()`);
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 = (fn, name) => async (...args) => {
72
- if (!fn) throw unsupported(name);
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(definition.actions?.accounts?.add, "accounts.add"),
81
- importPrivateKey: action(definition.actions?.accounts?.importPrivateKey, "accounts.importPrivateKey"),
82
- rename: action(definition.actions?.accounts?.rename, "accounts.rename"),
83
- switch: action(definition.actions?.accounts?.switch, "accounts.switch")
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: () => approve({ optional: true }),
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
- add: action(definition.actions?.network?.add, "network.add"),
93
- switch: action(definition.actions?.network?.switch, "network.switch")
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(definition.actions?.settings?.lock, "settings.lock"),
101
- unlock: action(definition.actions?.settings?.unlock, "settings.unlock")
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 closed = await Promise.allSettled([context.close()]);
110
- const removed = await Promise.allSettled([rm(runDir, {
111
- force: true,
112
- recursive: true
113
- })]);
114
- const closeResult = closed[0];
115
- const removeResult = removed[0];
116
- if (closeResult?.status === "rejected" && removeResult?.status === "rejected") throw new AggregateError([closeResult.reason, removeResult.reason], `[walletwright] failed to close the browser and remove ${runDir}`);
117
- if (closeResult?.status === "rejected") throw closeResult.reason;
118
- if (removeResult?.status === "rejected") throw removeResult.reason;
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
- /** Launch and unlock a disposable profile copy. Headless mode requires verified wallet support. */
128
- const launchWallet = async (setup, { headless = false } = {}) => {
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 = path.resolve(setup.cacheDir ?? ".walletwright");
132
- const profileDir = path.join(cacheDir, profileKey(setup));
133
- await restorePreviousProfile(profileDir);
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
- context = await chromium.launchPersistentContext(runDir, extensionContextOptions(extensionPath, headless));
156
- const launched = context;
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
- await definition.prepareContext?.(launched);
159
- const home = await definition.reachUnlockScreen(launched, extensionId);
160
- await definition.unlock(home, setup.password);
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: async () => {
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
- /** Create Playwright fixtures backed by one unlocked wallet launch per test. */
187
- const createWalletFixtures = (setup) => test.extend({
188
- context: async ({ walletLaunch }, use) => {
189
- await use(walletLaunch.context);
190
- },
191
- wallet: async ({ walletLaunch }, use) => {
192
- await use(walletLaunch.wallet);
193
- },
194
- walletLaunch: async ({ headless }, use) => {
195
- const launched = await launchWallet(setup, { headless });
196
- await use(launched);
197
- await launched.close();
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
@@ -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/** The MV3 service worker spawns a popup slowly once the wallet's own UI has been driven. */\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: () => approve({ optional: true }),\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":"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"}
@@ -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 };
@@ -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 (let index = 0; index < digits.length; index += 1) {
14
- carry += digits[index] * 256;
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 hasBinding = (value) => typeof Object.getOwnPropertyDescriptor(value, binding)?.value === "function";
70
- if (!hasBinding(window)) throw new TypeError(`Missing Playwright binding: ${binding}`);
71
- const call = window[binding];
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 (let index = 0; index < digits.length; index += 1) {\n carry += digits[index] * 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/**\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 */\n/** Per install, for the same reason as the EVM mock: see the note on `installCount` in `mock.ts`. */\nlet installCount = 0;\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 hasBinding = <Value extends object>(\n value: Value,\n ): value is Value & Record<string, (rpc: BridgeRequest) => Promise<Array<number>>> =>\n typeof Object.getOwnPropertyDescriptor(value, binding)?.value === \"function\";\n if (!hasBinding(window)) {\n throw new TypeError(`Missing Playwright binding: ${binding}`);\n }\n const call = window[binding];\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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"}
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 { MockWalletOptions, createRpcHandler, installMockWallet };
25
+ export { createRpcHandler, installMockWallet };
26
26
  //# sourceMappingURL=mock.d.mts.map