@walletwright/core 0.2.1 → 0.3.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/dist/index.mjs CHANGED
@@ -1,8 +1,7 @@
1
- import { a as wallets, c as extensionIdFromPath, d as placeApprovalWindow, f as profileKey, i as walletKindsByEcosystem, l as findNotificationPopup, m as waitUntil, n as extensionContextOptions, o as DEFAULT_CACHE_DIR, p as formatTimeout, s as DEFAULT_NOTIFICATION_MATCH, t as buildCache, u as hasNotificationPopup } from "./cache-Bwv2i-hv.mjs";
2
- import { cp, mkdtemp, rm } from "node:fs/promises";
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";
3
3
  import path from "node:path";
4
4
  import { chromium, test } from "@playwright/test";
5
- import { existsSync } from "node:fs";
6
5
  import os from "node:os";
7
6
 
8
7
  //#region src/internal/controller.ts
@@ -11,6 +10,22 @@ const POPUP_CLOSE_TIMEOUT_MS = 15e3;
11
10
  const OPTIONAL_POPUP_TIMEOUT_MS = 1e4;
12
11
  /** The MV3 service worker spawns a popup slowly once the wallet's own UI has been driven. */
13
12
  const REQUIRED_POPUP_TIMEOUT_MS = 3e4;
13
+ /**
14
+ * Drive `popup` with `settle` and return the page that actually took the approval. A popup that
15
+ * closes mid-settle is the wallet respawning the request, so re-acquire once and drive the new one.
16
+ */
17
+ const settleApproval = async (settle, popup, acquire) => {
18
+ try {
19
+ await settle(popup);
20
+ return popup;
21
+ } catch (error) {
22
+ if (!popup.isClosed()) throw error;
23
+ const fresh = await acquire();
24
+ if (!fresh) throw error;
25
+ await settle(fresh);
26
+ return fresh;
27
+ }
28
+ };
14
29
  /** Build the runtime controller that drives an unlocked wallet against the dapp under test. */
15
30
  const createWallet = ({ context, definition, extensionId, home, password }) => {
16
31
  const match = definition.notificationMatch ?? "notification.html";
@@ -20,60 +35,39 @@ const createWallet = ({ context, definition, extensionId, home, password }) => {
20
35
  home,
21
36
  password
22
37
  };
23
- /**
24
- * Make the dapp the active tab. While an extension page holds that spot, MetaMask renders a new
25
- * approval inline in it rather than opening a notification window, and the window that does open
26
- * shows the wallet home instead of the request. The wallet's own page can end up fronted both
27
- * after an action and after a previous approval settles.
28
- */
38
+ /** MetaMask opens requests inline while its extension page owns focus. */
29
39
  const frontDapp = async () => {
30
40
  await context.pages().find((page) => new RegExp("^https?:", "v").test(page.url()) && !page.isClosed())?.bringToFront().catch(() => {});
31
41
  };
32
- /** Wait for the approval window the wallet opens for a pending request. */
33
- const findApproval = (optional) => findNotificationPopup({
34
- approvalControls: definition.approvalControls,
35
- context,
36
- extensionId,
37
- match,
38
- timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS
39
- });
40
- /** Drive the pending approval popup with `settle`, then wait for it to close. */
42
+ /** Find and place the approval window before its controls are driven. */
43
+ const acquireApproval = async (optional) => {
44
+ const popup = await findNotificationPopup({
45
+ approvalControls: definition.approvalControls,
46
+ context,
47
+ extensionId,
48
+ match,
49
+ timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS
50
+ });
51
+ if (popup) await placeApprovalWindow(popup);
52
+ return popup;
53
+ };
54
+ /** Drive the pending approval popup with `settle`, then wait for that popup to close. */
41
55
  const resolvePopup = async (settle, { optional = false }) => {
42
56
  await frontDapp();
43
- const find = () => findApproval(optional);
44
- const popup = await find();
57
+ const acquire = () => acquireApproval(optional);
58
+ const popup = await acquire();
45
59
  if (!popup) {
46
60
  if (optional) return;
47
61
  const open = context.pages().map((page) => page.url());
48
62
  throw new Error(`[walletwright] approval popup did not appear. Open pages: ${open.join(" | ") || "none"}`);
49
63
  }
50
- await placeApprovalWindow(popup);
51
- try {
52
- await settle(popup);
53
- } catch (error) {
54
- if (!popup.isClosed()) throw error;
55
- const fresh = await find();
56
- if (!fresh) throw error;
57
- await settle(fresh);
58
- }
59
- const closed = await waitUntil(() => !hasNotificationPopup(context, extensionId, match), { timeoutMs: POPUP_CLOSE_TIMEOUT_MS });
60
- if (!optional && closed === void 0) throw new Error(`[walletwright] approval popup did not close after ${formatTimeout(POPUP_CLOSE_TIMEOUT_MS)} (the approval may not have registered)`);
64
+ const driven = await settleApproval(settle, popup, acquire);
65
+ 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)`);
61
66
  };
62
67
  const unsupported = (name) => /* @__PURE__ */ new Error(`[walletwright] ${definition.extensionName} does not support ${name}()`);
63
68
  const approve = (options = {}) => resolvePopup((popup) => definition.approve(popup, password), options);
64
- const reject = async (options = {}) => {
65
- const { reject: rejectPopup } = definition;
66
- if (!rejectPopup) throw unsupported("reject");
67
- await resolvePopup((popup) => rejectPopup(popup), options);
68
- };
69
- /**
70
- * Bind an optional capability, or fail loudly naming the wallet and action. A wallet declares only
71
- * what has been driven against the real extension, so an undeclared action is a real gap rather
72
- * than something to swallow. Extra args (e.g. a network config) forward after the context.
73
- *
74
- * After the action, focus returns to the dapp (see `frontDapp`), since actions drive the wallet's
75
- * own page via `bringToFront`.
76
- */
69
+ const reject = (options = {}) => resolvePopup((popup) => definition.reject(popup), options);
70
+ /** Bind a verified wallet action and restore focus to the dapp when it finishes. */
77
71
  const action = (fn, name) => async (...args) => {
78
72
  if (!fn) throw unsupported(name);
79
73
  if (home.isClosed()) throw new Error(`[walletwright] wallet home page is closed; cannot run ${name}()`);
@@ -111,50 +105,68 @@ const createWallet = ({ context, definition, extensionId, home, password }) => {
111
105
 
112
106
  //#endregion
113
107
  //#region src/internal/launch.ts
114
- /**
115
- * Drop the tabs nobody drives: Chromium's initial `about:blank` and the extension's own auto-opened
116
- * tab. We navigate a tab of our own to the wallet instead of adopting that one (it's unreliable, see
117
- * `reachUnlockScreen`), which otherwise leaves a second, still-locked wallet tab open all run.
118
- * Approval popups are unaffected: they're matched by URL, not by being the only extension page.
119
- */
108
+ 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;
119
+ };
120
+ /** Close Chromium's blank tab and the extension's auto-opened tab. */
120
121
  const closeStrayPages = async (context, home) => {
121
122
  const isStray = (page) => page !== home && !page.isClosed() && (page.url() === "about:blank" || page.url().startsWith("chrome-extension://"));
122
123
  const closing = [];
123
124
  for (const page of context.pages()) if (isStray(page)) closing.push(page.close());
124
125
  await Promise.allSettled(closing);
125
126
  };
126
- /**
127
- * Launch a fresh persistent context from the onboarded cache and return an unlocked wallet
128
- * controller. Headed by default; `headless` needs the wallet to declare `headlessApprovals`, since
129
- * some wallets create an approval window headless that is never exposed as a page. Outside
130
- * Playwright fixtures, `context.close()` is yours.
131
- */
127
+ /** Launch and unlock a disposable profile copy. Headless mode requires verified wallet support. */
132
128
  const launchWallet = async (setup, { headless = false } = {}) => {
133
129
  const definition = wallets[setup.wallet];
134
130
  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\`).`);
135
131
  const cacheDir = path.resolve(setup.cacheDir ?? ".walletwright");
136
132
  const profileDir = path.join(cacheDir, profileKey(setup));
137
- if (!existsSync(profileDir)) throw new Error(`[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \`walletwright cache\`.`);
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
+ }
138
139
  const extensionPath = await definition.prepareExtension(cacheDir, setup.version);
140
+ const extensionId = await extensionIdFromPath(extensionPath);
141
+ /** Cleanup must cover copy and launch failures that occur before a context exists. */
139
142
  const runDir = await mkdtemp(path.join(os.tmpdir(), "walletwright-"));
140
- await cp(profileDir, runDir, { recursive: true });
141
- const context = await chromium.launchPersistentContext(runDir, extensionContextOptions(extensionPath, headless));
142
- context.on("close", async () => {
143
- await rm(runDir, {
144
- force: true,
145
- recursive: true
146
- }).catch(() => {});
143
+ const removeRunDir = () => rm(runDir, {
144
+ force: true,
145
+ recursive: true
147
146
  });
148
- const extensionId = extensionIdFromPath(extensionPath);
147
+ const handleContextClose = async () => {
148
+ try {
149
+ await removeRunDir();
150
+ } catch {}
151
+ };
152
+ let context;
149
153
  try {
150
- await definition.prepareContext?.(context);
151
- const home = await definition.reachUnlockScreen(context, extensionId);
154
+ await cp(profileDir, runDir, { recursive: true });
155
+ context = await chromium.launchPersistentContext(runDir, extensionContextOptions(extensionPath, headless));
156
+ const launched = context;
157
+ launched.on("close", handleContextClose);
158
+ await definition.prepareContext?.(launched);
159
+ const home = await definition.reachUnlockScreen(launched, extensionId);
152
160
  await definition.unlock(home, setup.password);
153
- await closeStrayPages(context, home);
161
+ await closeStrayPages(launched, home);
154
162
  return {
155
- context,
163
+ close: async () => {
164
+ launched.off("close", handleContextClose);
165
+ await closeLaunch(launched, runDir);
166
+ },
167
+ context: launched,
156
168
  wallet: createWallet({
157
- context,
169
+ context: launched,
158
170
  definition,
159
171
  extensionId,
160
172
  home,
@@ -162,26 +174,16 @@ const launchWallet = async (setup, { headless = false } = {}) => {
162
174
  })
163
175
  };
164
176
  } catch (error) {
165
- await context.close().catch(() => {});
177
+ context?.off("close", handleContextClose);
178
+ await context?.close().catch(() => {});
179
+ await removeRunDir().catch(() => {});
166
180
  throw error;
167
181
  }
168
182
  };
169
183
 
170
184
  //#endregion
171
185
  //#region src/fixtures.ts
172
- /**
173
- * Build a Playwright `test` whose `context`/`page` run inside a persistent Chromium with the wallet
174
- * loaded and unlocked, exposing a `wallet` fixture to approve connect/sign popups.
175
- *
176
- * ```ts
177
- * const test = createWalletFixtures(setup);
178
- * test("connect", async ({ page, wallet }) => {
179
- * await page.goto("/");
180
- * await page.getByRole("button", { name: "Connect" }).click();
181
- * await wallet.connectToDapp();
182
- * });
183
- * ```
184
- */
186
+ /** Create Playwright fixtures backed by one unlocked wallet launch per test. */
185
187
  const createWalletFixtures = (setup) => test.extend({
186
188
  context: async ({ walletLaunch }, use) => {
187
189
  await use(walletLaunch.context);
@@ -192,10 +194,10 @@ const createWalletFixtures = (setup) => test.extend({
192
194
  walletLaunch: async ({ headless }, use) => {
193
195
  const launched = await launchWallet(setup, { headless });
194
196
  await use(launched);
195
- await launched.context.close();
197
+ await launched.close();
196
198
  }
197
199
  });
198
200
 
199
201
  //#endregion
200
- export { buildCache, createWalletFixtures, launchWallet, walletKindsByEcosystem, wallets };
202
+ export { buildCache, createWalletFixtures, isWalletKind, launchWallet, walletKindsByEcosystem, wallets };
201
203
  //# 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 {\n DEFAULT_NOTIFICATION_MATCH,\n findNotificationPopup,\n hasNotificationPopup,\n placeApprovalWindow,\n} 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\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 /**\n * Make the dapp the active tab. While an extension page holds that spot, MetaMask renders a new\n * approval inline in it rather than opening a notification window, and the window that does open\n * shows the wallet home instead of the request. The wallet's own page can end up fronted both\n * after an action and after a previous approval settles.\n */\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 /** Wait for the approval window the wallet opens for a pending request. */\n const findApproval = (optional: boolean): Promise<Page | undefined> =>\n findNotificationPopup({\n approvalControls: definition.approvalControls,\n context,\n extensionId,\n match,\n timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS,\n });\n\n /** Drive the pending approval popup with `settle`, then wait for it to close. */\n const resolvePopup = async (\n settle: (popup: Page) => Promise<void>,\n { optional = false }: ResolveOptions,\n ): Promise<void> => {\n await frontDapp();\n const find = () => findApproval(optional);\n const popup = await find();\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 await placeApprovalWindow(popup); // it can open partly off a small or virtual display\n try {\n await settle(popup);\n } catch (error) {\n if (!popup.isClosed()) {\n throw error;\n }\n const fresh = await find();\n if (!fresh) {\n throw error;\n }\n await settle(fresh);\n }\n\n const closed = await waitUntil(() => !hasNotificationPopup(context, extensionId, match), {\n timeoutMs: POPUP_CLOSE_TIMEOUT_MS,\n });\n if (!optional && 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 = async (options: ResolveOptions = {}): Promise<void> => {\n const { reject: rejectPopup } = definition;\n if (!rejectPopup) {\n throw unsupported(\"reject\");\n }\n await resolvePopup((popup) => rejectPopup(popup), options);\n };\n\n /**\n * Bind an optional capability, or fail loudly naming the wallet and action. A wallet declares only\n * what has been driven against the real extension, so an undeclared action is a real gap rather\n * than something to swallow. Extra args (e.g. a network config) forward after the context.\n *\n * After the action, focus returns to the dapp (see `frontDapp`), since actions drive the wallet's\n * own page via `bringToFront`.\n */\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 { existsSync } from \"node:fs\";\nimport { cp, mkdtemp, rm } 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 { extensionContextOptions } from \"./chromium\";\nimport { createWallet } from \"./controller\";\nimport { DEFAULT_CACHE_DIR, extensionIdFromPath, profileKey } from \"./utils\";\n\nexport type LaunchedWallet = {\n context: BrowserContext;\n wallet: Wallet;\n};\n\n/**\n * Drop the tabs nobody drives: Chromium's initial `about:blank` and the extension's own auto-opened\n * tab. We navigate a tab of our own to the wallet instead of adopting that one (it's unreliable, see\n * `reachUnlockScreen`), which otherwise leaves a second, still-locked wallet tab open all run.\n * Approval popups are unaffected: they're matched by URL, not by being the only extension page.\n */\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/**\n * Launch a fresh persistent context from the onboarded cache and return an unlocked wallet\n * controller. Headed by default; `headless` needs the wallet to declare `headlessApprovals`, since\n * some wallets create an approval window headless that is never exposed as a page. Outside\n * Playwright fixtures, `context.close()` is yours.\n */\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 if (!existsSync(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\n const extensionPath = await definition.prepareExtension(cacheDir, setup.version);\n\n const runDir = await mkdtemp(path.join(os.tmpdir(), \"walletwright-\"));\n await cp(profileDir, runDir, { recursive: true });\n\n const context = await chromium.launchPersistentContext(\n runDir,\n extensionContextOptions(extensionPath, headless),\n );\n\n context.on(\"close\", async () => {\n await rm(runDir, { force: true, recursive: true }).catch(() => {});\n });\n\n const extensionId = extensionIdFromPath(extensionPath);\n\n try {\n await definition.prepareContext?.(context);\n const home = await definition.reachUnlockScreen(context, extensionId);\n await definition.unlock(home, setup.password);\n await closeStrayPages(context, home);\n\n return {\n context,\n wallet: createWallet({\n context,\n definition,\n extensionId,\n home,\n password: setup.password,\n }),\n };\n } catch (error) {\n await context.close().catch(() => {});\n throw error;\n }\n};\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/**\n * Build a Playwright `test` whose `context`/`page` run inside a persistent Chromium with the wallet\n * loaded and unlocked, exposing a `wallet` fixture to approve connect/sign popups.\n *\n * ```ts\n * const test = createWalletFixtures(setup);\n * test(\"connect\", async ({ page, wallet }) => {\n * await page.goto(\"/\");\n * await page.getByRole(\"button\", { name: \"Connect\" }).click();\n * await wallet.connectToDapp();\n * });\n * ```\n */\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.context.close();\n },\n });\n"],"mappings":";;;;;;;;AAYA,MAAM,yBAAyB;;AAE/B,MAAM,4BAA4B;;AAElC,MAAM,4BAA4B;;AAalC,MAAa,gBAAgB,EAC3B,SACA,YACA,aACA,MACA,eACiC;CACjC,MAAM,QAAQ,WAAW;CACzB,MAAM,MAA2B;EAAE;EAAS;EAAa;EAAM;CAAS;;;;;;;CAQxE,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,gBAAgB,aACpB,sBAAsB;EACpB,kBAAkB,WAAW;EAC7B;EACA;EACA;EACA,WAAW,WAAW,4BAA4B;CACpD,CAAC;;CAGH,MAAM,eAAe,OACnB,QACA,EAAE,WAAW,YACK;EAClB,MAAM,UAAU;EAChB,MAAM,aAAa,aAAa,QAAQ;EACxC,MAAM,QAAQ,MAAM,KAAK;EACzB,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;EACA,MAAM,oBAAoB,KAAK;EAC/B,IAAI;GACF,MAAM,OAAO,KAAK;EACpB,SAAS,OAAO;GACd,IAAI,CAAC,MAAM,SAAS,GAClB,MAAM;GAER,MAAM,QAAQ,MAAM,KAAK;GACzB,IAAI,CAAC,OACH,MAAM;GAER,MAAM,OAAO,KAAK;EACpB;EAEA,MAAM,SAAS,MAAM,gBAAgB,CAAC,qBAAqB,SAAS,aAAa,KAAK,GAAG,EACvF,WAAW,uBACb,CAAC;EACD,IAAI,CAAC,YAAY,WAAW,QAC1B,MAAM,IAAI,MACR,qDAAqD,cAAc,sBAAsB,EAAE,wCAC7F;CAEJ;CAEA,MAAM,eAAe,yBACnB,IAAI,MAAM,kBAAkB,WAAW,cAAc,oBAAoB,KAAK,GAAG;CAEnF,MAAM,WAAW,UAA0B,CAAC,MAC1C,cAAc,UAAU,WAAW,QAAQ,OAAO,QAAQ,GAAG,OAAO;CAEtE,MAAM,SAAS,OAAO,UAA0B,CAAC,MAAqB;EACpE,MAAM,EAAE,QAAQ,gBAAgB;EAChC,IAAI,CAAC,aACH,MAAM,YAAY,QAAQ;EAE5B,MAAM,cAAc,UAAU,YAAY,KAAK,GAAG,OAAO;CAC3D;;;;;;;;;CAUA,MAAM,UAEF,IACA,SAEF,OAAO,GAAG,SAA2B;EACnC,IAAI,CAAC,IACH,MAAM,YAAY,IAAI;EAExB,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;CAEF,OAAO;EACL,UAAU;GACR,KAAK,OAAO,WAAW,SAAS,UAAU,KAAK,cAAc;GAC7D,kBAAkB,OAChB,WAAW,SAAS,UAAU,kBAC9B,2BACF;GACA,QAAQ,OAAO,WAAW,SAAS,UAAU,QAAQ,iBAAiB;GACtE,QAAQ,OAAO,WAAW,SAAS,UAAU,QAAQ,iBAAiB;EACxE;EACA;EACA,wBAAwB,QAAQ;EAChC,0BAA0B,QAAQ;EAClC,qBAAqB,QAAQ,EAAE,UAAU,KAAK,CAAC;EAC/C;EACA;EACA,SAAS;GACP,KAAK,OAAO,WAAW,SAAS,SAAS,KAAK,aAAa;GAC3D,QAAQ,OAAO,WAAW,SAAS,SAAS,QAAQ,gBAAgB;EACtE;EACA;EACA,wBAAwB,OAAO;EAC/B,uBAAuB,OAAO;EAC9B,yBAAyB,OAAO;EAChC,UAAU;GACR,MAAM,OAAO,WAAW,SAAS,UAAU,MAAM,eAAe;GAChE,QAAQ,OAAO,WAAW,SAAS,UAAU,QAAQ,iBAAiB;EACxE;CACF;AACF;;;;;;;;;;AChJA,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;;;;;;;AAQA,MAAa,eAAe,OAC1B,OACA,EAAE,WAAW,UAAkC,CAAC,MACpB;CAC5B,MAAM,aAAa,QAAQ,MAAM;CACjC,IAAI,YAAY,WAAW,sBAAsB,MAC/C,MAAM,IAAI,MACR,kBAAkB,WAAW,cAAc,gHAC7C;CAEF,MAAM,WAAW,KAAK,QAAQ,MAAM,2BAA6B;CACjE,MAAM,aAAa,KAAK,KAAK,UAAU,WAAW,KAAK,CAAC;CACxD,IAAI,CAAC,WAAW,UAAU,GACxB,MAAM,IAAI,MACR,6CAA6C,WAAW,8DAC1D;CAGF,MAAM,gBAAgB,MAAM,WAAW,iBAAiB,UAAU,MAAM,OAAO;CAE/E,MAAM,SAAS,MAAM,QAAQ,KAAK,KAAK,GAAG,OAAO,GAAG,eAAe,CAAC;CACpE,MAAM,GAAG,YAAY,QAAQ,EAAE,WAAW,KAAK,CAAC;CAEhD,MAAM,UAAU,MAAM,SAAS,wBAC7B,QACA,wBAAwB,eAAe,QAAQ,CACjD;CAEA,QAAQ,GAAG,SAAS,YAAY;EAC9B,MAAM,GAAG,QAAQ;GAAE,OAAO;GAAM,WAAW;EAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;CACnE,CAAC;CAED,MAAM,cAAc,oBAAoB,aAAa;CAErD,IAAI;EACF,MAAM,WAAW,iBAAiB,OAAO;EACzC,MAAM,OAAO,MAAM,WAAW,kBAAkB,SAAS,WAAW;EACpE,MAAM,WAAW,OAAO,MAAM,MAAM,QAAQ;EAC5C,MAAM,gBAAgB,SAAS,IAAI;EAEnC,OAAO;GACL;GACA,QAAQ,aAAa;IACnB;IACA;IACA;IACA;IACA,UAAU,MAAM;GAClB,CAAC;EACH;CACF,SAAS,OAAO;EACd,MAAM,QAAQ,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;EACpC,MAAM;CACR;AACF;;;;;;;;;;;;;;;;;ACzEA,MAAa,wBAAwB,UACnCA,KAAK,OAAuC;CAC1C,SAAS,OAAO,EAAE,gBAAgB,QAAQ;EACxC,MAAM,IAAI,aAAa,OAAO;CAChC;CACA,QAAQ,OAAO,EAAE,gBAAgB,QAAQ;EACvC,MAAM,IAAI,aAAa,MAAM;CAC/B;CACA,cAAc,OAAO,EAAE,YAAY,QAAQ;EACzC,MAAM,WAAW,MAAM,aAAa,OAAO,EAAE,SAAS,CAAC;EACvD,MAAM,IAAI,QAAQ;EAClB,MAAM,SAAS,QAAQ,MAAM;CAC/B;AACF,CAAC"}
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,18 +1,7 @@
1
1
  import { BrowserContext, Page } from "@playwright/test";
2
2
  //#region src/mock-standard.d.ts
3
- /**
4
- * A headless Wallet-Standard fake for Solana dapp tests that don't need a real extension. It
5
- * hand-rolls the Wallet-Standard registration protocol (no browser-side library) and answers
6
- * `standard:connect` plus `solana:signMessage`, signing with a real ed25519 key so signatures
7
- * verify. Isolated from the EVM `mock.ts`; `@walletwright/core/mock-standard` is its own entry
8
- * point.
9
- *
10
- * ed25519 signing runs Node-side (viem is secp256k1-only), reusing the same exposeFunction bridge
11
- * pattern as the EVM mock. No extra dependency: Node's built-in `crypto` signs ed25519 natively.
12
- */
13
- type MockEcosystem = "svm";
3
+ /** Headless Wallet-Standard Solana mock that signs messages with Node's ed25519 implementation. */
14
4
  type MockStandardWalletOptions = {
15
- ecosystem?: MockEcosystem;
16
5
  /** Wallet name announced over Wallet Standard. */
17
6
  name?: string;
18
7
  /** 32-byte ed25519 seed as hex. Defaults to a fixed test seed so the address is stable. */
@@ -25,17 +14,13 @@ type MockStandardAccount = {
25
14
  };
26
15
  declare const encodeBase58: (bytes: Uint8Array) => string;
27
16
  declare const privateKeyFromSeed: (seed: Buffer) => import("node:crypto").KeyObject;
17
+ /** `message` is required: defaulting it would sign the empty message instead of rejecting. */
28
18
  type BridgeRequest = {
29
- message?: Array<number>;
19
+ message: Array<number>;
30
20
  method: string;
31
21
  };
32
22
  declare const createStandardHandler: (seed: Buffer) => ({ message, method }: BridgeRequest) => Promise<Array<number>>;
33
- /**
34
- * Install the headless Wallet-Standard mock for Solana connect + sign on a context (every page) or a
35
- * single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.
36
- * Returns the account it announces so a test can verify signatures against the public key.
37
- */
38
23
  declare const installMockStandardWallet: (target: BrowserContext | Page, options?: MockStandardWalletOptions) => Promise<MockStandardAccount>;
39
24
  //#endregion
40
- export { type MockEcosystem, type MockStandardAccount, type MockStandardWalletOptions, createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };
25
+ export { type MockStandardAccount, type MockStandardWalletOptions, createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };
41
26
  //# sourceMappingURL=mock-standard.d.mts.map
@@ -35,7 +35,7 @@ const rawPublicKey = (seed) => {
35
35
  };
36
36
  const signEd25519 = (seed, message) => [...sign(null, Buffer.from(message), privateKeyFromSeed(seed))];
37
37
  const SVM_CHAINS = ["solana:mainnet", "solana:devnet"];
38
- const createStandardHandler = (seed) => ({ message = [], method }) => {
38
+ const createStandardHandler = (seed) => ({ message, method }) => {
39
39
  switch (method) {
40
40
  case "solana:signMessage": return Promise.resolve(signEd25519(seed, Uint8Array.from(message)));
41
41
  default: return Promise.reject(/* @__PURE__ */ new Error(`[walletwright/mock-standard] unsupported method: ${method}`));
@@ -46,22 +46,28 @@ const createStandardHandler = (seed) => ({ message = [], method }) => {
46
46
  * single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.
47
47
  * Returns the account it announces so a test can verify signatures against the public key.
48
48
  */
49
+ /** Per install, for the same reason as the EVM mock: see the note on `installCount` in `mock.ts`. */
50
+ let installCount = 0;
49
51
  const installMockStandardWallet = async (target, options = {}) => {
50
- const { ecosystem = "svm", name = "Walletwright Mock", seedHex = DEFAULT_SEED_HEX } = options;
51
- if (ecosystem !== "svm") throw new Error(`[walletwright/mock-standard] prototype supports only svm, got ${String(ecosystem)}`);
52
+ const { name = "Walletwright Mock", seedHex = DEFAULT_SEED_HEX } = options;
52
53
  const seed = Buffer.from(seedHex, "hex");
53
54
  if (seed.length !== 32) throw new Error("[walletwright/mock-standard] seedHex must be 32 bytes");
54
55
  const publicKey = rawPublicKey(seed);
55
56
  const address = encodeBase58(publicKey);
56
57
  const publicKeyHex = publicKey.toString("hex");
57
58
  const handle = createStandardHandler(seed);
58
- const bindingName = "__walletwrightMockStandardSign";
59
- try {
60
- await target.exposeFunction(bindingName, (rpc) => handle(rpc));
61
- } catch (error) {
62
- if (!(error instanceof Error && error.message.includes("already registered"))) throw error;
63
- }
59
+ const bindingName = `__walletwrightMockStandardSign_${installCount++}`;
60
+ await target.exposeFunction(bindingName, (rpc) => handle(rpc));
61
+ const initArgs = [bindingName, {
62
+ address,
63
+ chains: [...SVM_CHAINS],
64
+ icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
65
+ name,
66
+ publicKeyHex
67
+ }];
64
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}`);
65
71
  const call = window[binding];
66
72
  const publicKeyBytes = Uint8Array.from((info.publicKeyHex.match(new RegExp(".{2}", "gv")) ?? []).map((byte) => Number.parseInt(byte, 16)));
67
73
  const account = {
@@ -107,13 +113,7 @@ const installMockStandardWallet = async (target, options = {}) => {
107
113
  window.addEventListener("wallet-standard:app-ready", (event) => {
108
114
  callback(event.detail);
109
115
  });
110
- }, [bindingName, {
111
- address,
112
- chains: [...SVM_CHAINS],
113
- icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
114
- name,
115
- publicKeyHex
116
- }]);
116
+ }, initArgs);
117
117
  return {
118
118
  address,
119
119
  publicKeyHex
@@ -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/**\n * A headless Wallet-Standard fake for Solana dapp tests that don't need a real extension. It\n * hand-rolls the Wallet-Standard registration protocol (no browser-side library) and answers\n * `standard:connect` plus `solana:signMessage`, signing with a real ed25519 key so signatures\n * verify. Isolated from the EVM `mock.ts`; `@walletwright/core/mock-standard` is its own entry\n * point.\n *\n * ed25519 signing runs Node-side (viem is secp256k1-only), reusing the same exposeFunction bridge\n * pattern as the EVM mock. No extra dependency: Node's built-in `crypto` signs ed25519 natively.\n */\ntype MockEcosystem = \"svm\";\n\ntype MockStandardWalletOptions = {\n ecosystem?: MockEcosystem;\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\ntype BridgeRequest = { message?: Array<number>; method: string };\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 */\nconst installMockStandardWallet = async (\n target: BrowserContext | Page,\n options: MockStandardWalletOptions = {},\n): Promise<MockStandardAccount> => {\n const { ecosystem = \"svm\", name = \"Walletwright Mock\", seedHex = DEFAULT_SEED_HEX } = options;\n if (ecosystem !== \"svm\") {\n throw new Error(\n `[walletwright/mock-standard] prototype supports only svm, got ${String(ecosystem)}`,\n );\n }\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\";\n try {\n await target.exposeFunction(bindingName, (rpc: BridgeRequest) => handle(rpc));\n } catch (error) {\n if (!(error instanceof Error && error.message.includes(\"already registered\"))) {\n throw error;\n }\n }\n\n await target.addInitScript(\n ([binding, info]) => {\n const call = (\n window as unknown as Record<string, (rpc: BridgeRequest) => Promise<Array<number>>>\n )[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 callback((event as CustomEvent<{ register: (w: typeof wallet) => void }>).detail);\n });\n },\n [\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 ] as const,\n );\n\n return { address, publicKeyHex };\n};\n\nexport { createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };\nexport type { MockEcosystem, 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 (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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"}
package/dist/mock.d.mts CHANGED
@@ -1,12 +1,7 @@
1
1
  import { BrowserContext, Page } from "@playwright/test";
2
2
  import { PrivateKeyAccount } from "viem/accounts";
3
3
  //#region src/mock.d.ts
4
- /**
5
- * A headless injected-wallet fake for dapp tests that don't need a real extension. It announces
6
- * itself over EIP-6963 and answers the EIP-1193 requests a connect/sign flow makes, signing with a
7
- * real key so signatures verify. `@walletwright/core/mock` is a separate entry point that needs
8
- * `viem` (optional peer); the extension-driving core doesn't.
9
- */
4
+ /** Headless EIP-6963 and EIP-1193 mock that signs with a real key. */
10
5
  type MockWalletOptions = {
11
6
  chainId?: number;
12
7
  /** Wallet name announced over EIP-6963. */
@@ -18,8 +13,9 @@ type Rpc = {
18
13
  method: string;
19
14
  params?: ReadonlyArray<unknown>;
20
15
  };
16
+ type RpcResult = ReadonlyArray<string> | string | null;
21
17
  /** The EIP-1193 handler a mock provider answers with. Exported for direct, browser-free testing. */
22
- declare const createRpcHandler: (account: PrivateKeyAccount, chainIdHex: string) => ({ method, params }: Rpc) => Promise<unknown>;
18
+ declare const createRpcHandler: (account: PrivateKeyAccount, chainIdHex: string) => ({ method, params }: Rpc) => Promise<RpcResult>;
23
19
  /**
24
20
  * Install the mock on a context (every page) or a single page. Call before `goto`, so the provider
25
21
  * exists when the dapp looks for it. Returns the account address it announces.
package/dist/mock.mjs CHANGED
@@ -20,6 +20,8 @@ const createRpcHandler = (account, chainIdHex) => ({ method, params = [] }) => {
20
20
  default: return Promise.reject(/* @__PURE__ */ new Error(`[walletwright/mock] unsupported method: ${method}`));
21
21
  }
22
22
  };
23
+ /** Each install needs its own Playwright binding and EIP-6963 identity. */
24
+ let installCount = 0;
23
25
  /**
24
26
  * Install the mock on a context (every page) or a single page. Call before `goto`, so the provider
25
27
  * exists when the dapp looks for it. Returns the account address it announces.
@@ -29,13 +31,18 @@ const installMockWallet = async (target, options = {}) => {
29
31
  const account = privateKeyToAccount(privateKey);
30
32
  const chainIdHex = toHex(chainId);
31
33
  const handle = createRpcHandler(account, chainIdHex);
32
- const bindingName = "__walletwrightMockRpc";
33
- try {
34
- await target.exposeFunction(bindingName, (rpc) => handle(rpc));
35
- } catch (error) {
36
- if (!(error instanceof Error && error.message.includes("already registered"))) throw error;
37
- }
34
+ const install = installCount++;
35
+ const bindingName = `__walletwrightMockRpc_${install}`;
36
+ await target.exposeFunction(bindingName, (rpc) => handle(rpc));
37
+ const initArgs = [bindingName, {
38
+ icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
39
+ name,
40
+ rdns: "sh.walletwright.mock",
41
+ uuid: `00000000-0000-0000-0000-${String(install).padStart(12, "0")}`
42
+ }];
38
43
  await target.addInitScript(([binding, info]) => {
44
+ const hasBinding = (value) => typeof Object.getOwnPropertyDescriptor(value, binding)?.value === "function";
45
+ if (!hasBinding(window)) throw new TypeError(`Missing Playwright binding: ${binding}`);
39
46
  const call = window[binding];
40
47
  const provider = {
41
48
  isMetaMask: true,
@@ -52,12 +59,7 @@ const installMockWallet = async (target, options = {}) => {
52
59
  };
53
60
  window.addEventListener("eip6963:requestProvider", announce);
54
61
  announce();
55
- }, [bindingName, {
56
- icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
57
- name,
58
- rdns: "sh.walletwright.mock",
59
- uuid: "00000000-0000-0000-0000-000000000000"
60
- }]);
62
+ }, initArgs);
61
63
  return account.address;
62
64
  };
63
65
 
package/dist/mock.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mock.mjs","names":[],"sources":["../src/mock.ts"],"sourcesContent":["import type { BrowserContext, Page } from \"@playwright/test\";\nimport type { PrivateKeyAccount } from \"viem/accounts\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\n/**\n * A headless injected-wallet fake for dapp tests that don't need a real extension. It announces\n * itself over EIP-6963 and answers the EIP-1193 requests a connect/sign flow makes, signing with a\n * real key so signatures verify. `@walletwright/core/mock` is a separate entry point that needs\n * `viem` (optional peer); the extension-driving core doesn't.\n */\nexport type MockWalletOptions = {\n chainId?: number;\n /** Wallet name announced over EIP-6963. */\n name?: string;\n /** The account's private key. Defaults to anvil/hardhat account #0. */\n privateKey?: `0x${string}`;\n};\n\nconst DEFAULT_KEY = \"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80\";\n\ntype Rpc = { method: string; params?: ReadonlyArray<unknown> };\n\nconst toHex = (value: number): string => `0x${value.toString(16)}`;\n\nconst isHexMessage = (value: string): value is `0x${string}` => /^0x[\\da-fA-F]*$/v.test(value);\n\n/** The EIP-1193 handler a mock provider answers with. Exported for direct, browser-free testing. */\nconst createRpcHandler =\n (account: PrivateKeyAccount, chainIdHex: string) =>\n ({ method, params = [] }: Rpc): Promise<unknown> => {\n switch (method) {\n case \"eth_requestAccounts\":\n case \"eth_accounts\": {\n return Promise.resolve([account.address]);\n }\n case \"eth_chainId\": {\n return Promise.resolve(chainIdHex);\n }\n case \"personal_sign\": {\n const [message] = params;\n if (typeof message !== \"string\") {\n return Promise.reject(\n new Error(\"[walletwright/mock] personal_sign expects a string message\"),\n );\n }\n return account.signMessage({\n message: isHexMessage(message) ? { raw: message } : message,\n });\n }\n case \"wallet_switchEthereumChain\":\n case \"wallet_addEthereumChain\": {\n return Promise.resolve(null);\n }\n default: {\n return Promise.reject(new Error(`[walletwright/mock] unsupported method: ${method}`));\n }\n }\n };\n\n/**\n * Install the mock on a context (every page) or a single page. Call before `goto`, so the provider\n * exists when the dapp looks for it. Returns the account address it announces.\n */\nconst installMockWallet = async (\n target: BrowserContext | Page,\n options: MockWalletOptions = {},\n): Promise<string> => {\n const { chainId = 31_337, name = \"Walletwright Mock\", privateKey = DEFAULT_KEY } = options;\n const account = privateKeyToAccount(privateKey);\n const chainIdHex = toHex(chainId);\n const handle = createRpcHandler(account, chainIdHex);\n\n const bindingName = \"__walletwrightMockRpc\";\n try {\n await target.exposeFunction(bindingName, (rpc: Rpc) => handle(rpc));\n } catch (error) {\n if (!(error instanceof Error && error.message.includes(\"already registered\"))) {\n throw error;\n }\n }\n\n await target.addInitScript(\n ([binding, info]) => {\n const call = (window as unknown as Record<string, (rpc: Rpc) => Promise<unknown>>)[binding];\n const provider = {\n isMetaMask: true,\n on: () => provider,\n removeListener: () => provider,\n request: (rpc: Rpc) => call(rpc),\n };\n (window as unknown as { ethereum?: unknown }).ethereum = provider;\n\n const announce = () => {\n window.dispatchEvent(\n new CustomEvent(\"eip6963:announceProvider\", {\n detail: Object.freeze({ info, provider }),\n }),\n );\n };\n window.addEventListener(\"eip6963:requestProvider\", announce);\n announce();\n },\n [\n bindingName,\n {\n icon: \"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>\",\n name,\n rdns: \"sh.walletwright.mock\",\n uuid: \"00000000-0000-0000-0000-000000000000\",\n },\n ] as const,\n );\n\n return account.address;\n};\n\nexport { createRpcHandler, installMockWallet };\n"],"mappings":";;;AAkBA,MAAM,cAAc;AAIpB,MAAM,SAAS,UAA0B,KAAK,MAAM,SAAS,EAAE;AAE/D,MAAM,gBAAgB,UAA0C,kCAAiB,CAAC,CAAC,KAAK,KAAK;;AAG7F,MAAM,oBACH,SAA4B,gBAC5B,EAAE,QAAQ,SAAS,CAAC,QAA+B;CAClD,QAAQ,QAAR;EACE,KAAK;EACL,KAAK,gBACH,OAAO,QAAQ,QAAQ,CAAC,QAAQ,OAAO,CAAC;EAE1C,KAAK,eACH,OAAO,QAAQ,QAAQ,UAAU;EAEnC,KAAK,iBAAiB;GACpB,MAAM,CAAC,WAAW;GAClB,IAAI,OAAO,YAAY,UACrB,OAAO,QAAQ,uBACb,IAAI,MAAM,4DAA4D,CACxE;GAEF,OAAO,QAAQ,YAAY,EACzB,SAAS,aAAa,OAAO,IAAI,EAAE,KAAK,QAAQ,IAAI,QACtD,CAAC;EACH;EACA,KAAK;EACL,KAAK,2BACH,OAAO,QAAQ,QAAQ,IAAI;EAE7B,SACE,OAAO,QAAQ,uBAAO,IAAI,MAAM,2CAA2C,QAAQ,CAAC;CAExF;AACF;;;;;AAMF,MAAM,oBAAoB,OACxB,QACA,UAA6B,CAAC,MACV;CACpB,MAAM,EAAE,UAAU,OAAQ,OAAO,qBAAqB,aAAa,gBAAgB;CACnF,MAAM,UAAU,oBAAoB,UAAU;CAC9C,MAAM,aAAa,MAAM,OAAO;CAChC,MAAM,SAAS,iBAAiB,SAAS,UAAU;CAEnD,MAAM,cAAc;CACpB,IAAI;EACF,MAAM,OAAO,eAAe,cAAc,QAAa,OAAO,GAAG,CAAC;CACpE,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,SAAS,MAAM,QAAQ,SAAS,oBAAoB,IACzE,MAAM;CAEV;CAEA,MAAM,OAAO,eACV,CAAC,SAAS,UAAU;EACnB,MAAM,OAAQ,OAAqE;EACnF,MAAM,WAAW;GACf,YAAY;GACZ,UAAU;GACV,sBAAsB;GACtB,UAAU,QAAa,KAAK,GAAG;EACjC;EACA,AAAC,OAA6C,WAAW;EAEzD,MAAM,iBAAiB;GACrB,OAAO,cACL,IAAI,YAAY,4BAA4B,EAC1C,QAAQ,OAAO,OAAO;IAAE;IAAM;GAAS,CAAC,EAC1C,CAAC,CACH;EACF;EACA,OAAO,iBAAiB,2BAA2B,QAAQ;EAC3D,SAAS;CACX,GACA,CACE,aACA;EACE,MAAM;EACN;EACA,MAAM;EACN,MAAM;CACR,CACF,CACF;CAEA,OAAO,QAAQ;AACjB"}
1
+ {"version":3,"file":"mock.mjs","names":[],"sources":["../src/mock.ts"],"sourcesContent":["import type { BrowserContext, Page } from \"@playwright/test\";\nimport type { PrivateKeyAccount } from \"viem/accounts\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\n/** Headless EIP-6963 and EIP-1193 mock that signs with a real key. */\nexport type MockWalletOptions = {\n chainId?: number;\n /** Wallet name announced over EIP-6963. */\n name?: string;\n /** The account's private key. Defaults to anvil/hardhat account #0. */\n privateKey?: `0x${string}`;\n};\n\nconst DEFAULT_KEY = \"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80\";\n\ntype Rpc = { method: string; params?: ReadonlyArray<unknown> };\ntype RpcResult = ReadonlyArray<string> | string | null;\n\nconst toHex = (value: number): `0x${string}` => `0x${value.toString(16)}`;\n\nconst isHexMessage = (value: string): value is `0x${string}` => /^0x[\\da-fA-F]*$/v.test(value);\n\n/** The EIP-1193 handler a mock provider answers with. Exported for direct, browser-free testing. */\nconst createRpcHandler =\n (account: PrivateKeyAccount, chainIdHex: string) =>\n ({ method, params = [] }: Rpc): Promise<RpcResult> => {\n switch (method) {\n case \"eth_requestAccounts\":\n case \"eth_accounts\": {\n return Promise.resolve([account.address]);\n }\n case \"eth_chainId\": {\n return Promise.resolve(chainIdHex);\n }\n case \"personal_sign\": {\n const [message] = params;\n if (typeof message !== \"string\") {\n return Promise.reject(\n new Error(\"[walletwright/mock] personal_sign expects a string message\"),\n );\n }\n return account.signMessage({\n message: isHexMessage(message) ? { raw: message } : message,\n });\n }\n case \"wallet_switchEthereumChain\":\n case \"wallet_addEthereumChain\": {\n return Promise.resolve(null);\n }\n default: {\n return Promise.reject(new Error(`[walletwright/mock] unsupported method: ${method}`));\n }\n }\n };\n\n/** Each install needs its own Playwright binding and EIP-6963 identity. */\nlet installCount = 0;\n\n/**\n * Install the mock on a context (every page) or a single page. Call before `goto`, so the provider\n * exists when the dapp looks for it. Returns the account address it announces.\n */\nconst installMockWallet = async (\n target: BrowserContext | Page,\n options: MockWalletOptions = {},\n): Promise<string> => {\n const { chainId = 31_337, name = \"Walletwright Mock\", privateKey = DEFAULT_KEY } = options;\n const account = privateKeyToAccount(privateKey);\n const chainIdHex = toHex(chainId);\n const handle = createRpcHandler(account, chainIdHex);\n\n const install = installCount++;\n const bindingName = `__walletwrightMockRpc_${install}`;\n await target.exposeFunction(bindingName, (rpc: Rpc) => handle(rpc));\n const initArgs = [\n bindingName,\n {\n icon: \"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>\",\n name,\n rdns: \"sh.walletwright.mock\",\n uuid: `00000000-0000-0000-0000-${String(install).padStart(12, \"0\")}`,\n },\n ] satisfies readonly [string, { icon: string; name: string; rdns: string; uuid: string }];\n\n await target.addInitScript(([binding, info]) => {\n const hasBinding = <Value extends object>(\n value: Value,\n ): value is Value & Record<string, (rpc: Rpc) => Promise<RpcResult>> =>\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 provider = {\n isMetaMask: true,\n on: () => provider,\n removeListener: () => provider,\n request: (rpc: Rpc) => call(rpc),\n };\n // SAFETY: EIP-1193 providers own window.ethereum, which the DOM library does not declare.\n (window as Window & { ethereum?: typeof provider }).ethereum = provider;\n\n const announce = () => {\n window.dispatchEvent(\n new CustomEvent(\"eip6963:announceProvider\", {\n detail: Object.freeze({ info, provider }),\n }),\n );\n };\n window.addEventListener(\"eip6963:requestProvider\", announce);\n announce();\n }, initArgs);\n\n return account.address;\n};\n\nexport { createRpcHandler, installMockWallet };\n"],"mappings":";;;AAaA,MAAM,cAAc;AAKpB,MAAM,SAAS,UAAiC,KAAK,MAAM,SAAS,EAAE;AAEtE,MAAM,gBAAgB,UAA0C,kCAAiB,CAAC,CAAC,KAAK,KAAK;;AAG7F,MAAM,oBACH,SAA4B,gBAC5B,EAAE,QAAQ,SAAS,CAAC,QAAiC;CACpD,QAAQ,QAAR;EACE,KAAK;EACL,KAAK,gBACH,OAAO,QAAQ,QAAQ,CAAC,QAAQ,OAAO,CAAC;EAE1C,KAAK,eACH,OAAO,QAAQ,QAAQ,UAAU;EAEnC,KAAK,iBAAiB;GACpB,MAAM,CAAC,WAAW;GAClB,IAAI,OAAO,YAAY,UACrB,OAAO,QAAQ,uBACb,IAAI,MAAM,4DAA4D,CACxE;GAEF,OAAO,QAAQ,YAAY,EACzB,SAAS,aAAa,OAAO,IAAI,EAAE,KAAK,QAAQ,IAAI,QACtD,CAAC;EACH;EACA,KAAK;EACL,KAAK,2BACH,OAAO,QAAQ,QAAQ,IAAI;EAE7B,SACE,OAAO,QAAQ,uBAAO,IAAI,MAAM,2CAA2C,QAAQ,CAAC;CAExF;AACF;;AAGF,IAAI,eAAe;;;;;AAMnB,MAAM,oBAAoB,OACxB,QACA,UAA6B,CAAC,MACV;CACpB,MAAM,EAAE,UAAU,OAAQ,OAAO,qBAAqB,aAAa,gBAAgB;CACnF,MAAM,UAAU,oBAAoB,UAAU;CAC9C,MAAM,aAAa,MAAM,OAAO;CAChC,MAAM,SAAS,iBAAiB,SAAS,UAAU;CAEnD,MAAM,UAAU;CAChB,MAAM,cAAc,yBAAyB;CAC7C,MAAM,OAAO,eAAe,cAAc,QAAa,OAAO,GAAG,CAAC;CAClE,MAAM,WAAW,CACf,aACA;EACE,MAAM;EACN;EACA,MAAM;EACN,MAAM,2BAA2B,OAAO,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG;CACnE,CACF;CAEA,MAAM,OAAO,eAAe,CAAC,SAAS,UAAU;EAC9C,MAAM,cACJ,UAEA,OAAO,OAAO,yBAAyB,OAAO,OAAO,CAAC,EAAE,UAAU;EACpE,IAAI,CAAC,WAAW,MAAM,GACpB,MAAM,IAAI,UAAU,+BAA+B,SAAS;EAE9D,MAAM,OAAO,OAAO;EACpB,MAAM,WAAW;GACf,YAAY;GACZ,UAAU;GACV,sBAAsB;GACtB,UAAU,QAAa,KAAK,GAAG;EACjC;EAEA,AAAC,OAAmD,WAAW;EAE/D,MAAM,iBAAiB;GACrB,OAAO,cACL,IAAI,YAAY,4BAA4B,EAC1C,QAAQ,OAAO,OAAO;IAAE;IAAM;GAAS,CAAC,EAC1C,CAAC,CACH;EACF;EACA,OAAO,iBAAiB,2BAA2B,QAAQ;EAC3D,SAAS;CACX,GAAG,QAAQ;CAEX,OAAO,QAAQ;AACjB"}