w3wallets 1.0.0-beta.7 → 1.0.0-beta.9

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
@@ -62,12 +62,12 @@ var ARIA_CLOSE_TIMEOUT = 500;
62
62
  var POPUP_HIDDEN_TIMEOUT = 3e3;
63
63
  var POST_UNLOCK_TIMEOUT = 3e4;
64
64
  var NOTIFICATION_CHECK_TIMEOUT = 5e3;
65
- var CONFIRMATION_ROUTE_TIMEOUT = 15e3;
66
65
  var POST_CLICK_TIMEOUT = 1e4;
67
66
  var BUTTON_OR_POPUP_TIMEOUT = 3e4;
68
67
  var LAST_RESORT_CLICK_TIMEOUT = 1e4;
69
68
  var LOCK_SCREEN_TIMEOUT = 3e4;
70
69
  var MENU_BUTTON_TIMEOUT = 3e4;
70
+ var ONBOARD_VISIBLE_TIMEOUT = 3e4;
71
71
  var MNEMONIC_KEY_DELAY = 5;
72
72
  var MNEMONIC_WORD_DELAY = 100;
73
73
 
@@ -257,7 +257,6 @@ async function findCachedExtension(context, ExtensionClass, expectedExtensionId,
257
257
  const page = await context.newPage();
258
258
  if (homeUrl) {
259
259
  await page.goto(`chrome-extension://${expectedExtensionId}/${homeUrl}`);
260
- await page.goto(`chrome-extension://${expectedExtensionId}/${homeUrl}`);
261
260
  }
262
261
  const extension = new ExtensionClass(page, expectedExtensionId);
263
262
  return extension;
@@ -324,7 +323,7 @@ var Metamask = class extends Wallet {
324
323
  await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
325
324
  await expect(
326
325
  this.page.getByRole("button", { name: "I have an existing wallet" })
327
- ).toBeVisible({ timeout: config.expectTimeout });
326
+ ).toBeVisible({ timeout: ONBOARD_VISIBLE_TIMEOUT });
328
327
  }
329
328
  /**
330
329
  * Onboard MetaMask with a mnemonic phrase
@@ -349,7 +348,6 @@ var Metamask = class extends Wallet {
349
348
  }
350
349
  const continueBtn = this.page.getByTestId("import-srp-confirm");
351
350
  await expect(continueBtn).toBeEnabled({ timeout: config.expectTimeout });
352
- await expect(continueBtn).toBeEnabled({ timeout: config.expectTimeout });
353
351
  await continueBtn.click();
354
352
  const passwordInputs = this.page.locator('input[type="password"]');
355
353
  await passwordInputs.nth(0).fill(pwd);
@@ -363,10 +361,28 @@ var Metamask = class extends Wallet {
363
361
  });
364
362
  await openWalletBtn.click();
365
363
  await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
366
- await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
364
+ const readyIndicator = this.page.getByTestId("account-options-menu-button");
365
+ const lockInput = this.page.getByTestId("unlock-password");
366
+ const state = await Promise.race([
367
+ readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready"),
368
+ lockInput.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "locked")
369
+ ]).catch(() => "timeout");
370
+ debug(`metamask.onboard: post-navigate state=${state}`);
371
+ if (state === "locked") {
372
+ debug("metamask.onboard: vault locked after onboarding, auto-unlocking");
373
+ await lockInput.fill(password ?? this.defaultPassword);
374
+ await this.page.getByTestId("unlock-submit").click();
375
+ await this.page.waitForSelector('[data-testid="unlock-password"]', {
376
+ state: "hidden",
377
+ timeout: LOCK_SCREEN_TIMEOUT
378
+ });
379
+ await this.stabilizePostUnlock();
380
+ }
381
+ await this.closeExtraExtensionPages();
367
382
  await this.page.goto(
368
383
  `chrome-extension://${this.extensionId}/sidepanel.html`
369
384
  );
385
+ await this.page.waitForLoadState("networkidle");
370
386
  debug("metamask.onboard: complete");
371
387
  }
372
388
  /**
@@ -482,10 +498,37 @@ var Metamask = class extends Wallet {
482
498
  }
483
499
  await expect(readyIndicator).toBeVisible({ timeout: POST_UNLOCK_TIMEOUT });
484
500
  }
501
+ /**
502
+ * Close extra MetaMask extension pages that the service worker opened
503
+ * during onboarding (e.g. home.html#/onboarding/completion).
504
+ * Keeps only this.page alive.
505
+ */
506
+ async closeExtraExtensionPages() {
507
+ const extOrigin = `chrome-extension://${this.extensionId}`;
508
+ const context = this.page.context();
509
+ const extras = context.pages().filter((p) => p !== this.page && p.url().startsWith(extOrigin));
510
+ if (extras.length > 0) {
511
+ debug(
512
+ `metamask.closeExtraExtensionPages: closing ${extras.length} extra page(s)`
513
+ );
514
+ await Promise.all(extras.map((p) => p.close()));
515
+ }
516
+ }
517
+ /**
518
+ * Poll the URL until it leaves a confirmation route. Uses polling instead
519
+ * of waitForURL because MetaMask uses HashRouter and hash-only changes
520
+ * are not reliably caught by Playwright's navigation events.
521
+ */
522
+ async waitForConfirmRouteExit(routePattern) {
523
+ const deadline = Date.now() + POST_CLICK_TIMEOUT;
524
+ while (routePattern.test(this.page.url()) && Date.now() < deadline) {
525
+ await this.page.waitForTimeout(500);
526
+ }
527
+ }
485
528
  /**
486
529
  * Wait for a target button while handling the Transaction Shield popup.
487
- * Always navigates to sidepanel.html fresh so MetaMask's
488
- * ConfirmationHandler can route to the pending approval.
530
+ * Navigates to sidepanel.html so MetaMask's ConfirmationHandler routes
531
+ * to the pending approval, then waits for the button to become visible.
489
532
  */
490
533
  async waitAndClickButton(btnLocator) {
491
534
  debug(`metamask.waitAndClickButton: navigating to sidepanel`);
@@ -496,63 +539,36 @@ var Metamask = class extends Wallet {
496
539
  btnLocator.first().waitFor({ state: "visible", timeout }).then(() => "button"),
497
540
  popup.first().waitFor({ state: "visible", timeout }).then(() => "popup")
498
541
  ]).catch(() => "timeout");
499
- const handlePopupAndClick = async () => {
500
- await this.dismissPopups();
501
- await btnLocator.first().waitFor({ state: "visible", timeout: BUTTON_OR_POPUP_TIMEOUT });
502
- await btnLocator.first().click();
503
- };
504
542
  await this.page.goto(sidepanelUrl);
505
- try {
506
- await this.page.waitForURL(confirmRoutePattern, {
507
- timeout: CONFIRMATION_ROUTE_TIMEOUT
508
- });
509
- } catch {
510
- console.warn(
511
- `[w3wallets] confirmation route not found, retrying. URL: ${this.page.url()}`
543
+ let result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
544
+ if (result === "timeout") {
545
+ debug(
546
+ `metamask.waitAndClickButton: first wait timed out, reloading. URL: ${this.page.url()}`
512
547
  );
513
548
  await this.page.goto(sidepanelUrl);
514
- try {
515
- await this.page.waitForURL(confirmRoutePattern, {
516
- timeout: CONFIRMATION_ROUTE_TIMEOUT
517
- });
518
- } catch {
519
- console.warn(
520
- `[w3wallets] confirmation route not found after retry. URL: ${this.page.url()}`
521
- );
522
- }
549
+ result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
523
550
  }
524
- const result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
525
551
  debug(
526
552
  `metamask.waitAndClickButton: result=${result}, URL=${this.page.url()}`
527
553
  );
528
554
  if (result === "button") {
529
555
  await btnLocator.first().click();
530
- await this.page.waitForURL((url) => !confirmRoutePattern.test(url.toString()), {
531
- timeout: POST_CLICK_TIMEOUT
532
- }).catch(() => {
533
- console.warn(
534
- `[w3wallets] still on confirmation route after click. URL: ${this.page.url()}`
535
- );
536
- });
556
+ await this.waitForConfirmRouteExit(confirmRoutePattern);
537
557
  return;
538
558
  }
539
559
  if (result === "popup") {
540
- await handlePopupAndClick();
541
- await this.page.waitForURL((url) => !confirmRoutePattern.test(url.toString()), {
542
- timeout: POST_CLICK_TIMEOUT
543
- }).catch(() => {
544
- console.warn(
545
- `[w3wallets] still on confirmation route after popup dismiss. URL: ${this.page.url()}`
546
- );
547
- });
560
+ await this.dismissPopups();
561
+ await btnLocator.first().waitFor({ state: "visible", timeout: BUTTON_OR_POPUP_TIMEOUT });
562
+ await btnLocator.first().click();
563
+ await this.waitForConfirmRouteExit(confirmRoutePattern);
548
564
  return;
549
565
  }
550
566
  const isOnConfirmRoute = confirmRoutePattern.test(this.page.url());
551
567
  debug(
552
- `metamask.waitAndClickButton: timeout after ${BUTTON_OR_POPUP_TIMEOUT}ms. URL: ${this.page.url()}, onConfirmRoute: ${isOnConfirmRoute}`
568
+ `metamask.waitAndClickButton: timeout. URL: ${this.page.url()}, onConfirmRoute: ${isOnConfirmRoute}`
553
569
  );
554
570
  console.warn(
555
- `[w3wallets] no button or popup found after ${BUTTON_OR_POPUP_TIMEOUT / 1e3}s. URL: ${this.page.url()}`
571
+ `[w3wallets] no button or popup found after waiting. URL: ${this.page.url()}`
556
572
  );
557
573
  await btnLocator.first().click({ timeout: LAST_RESORT_CLICK_TIMEOUT });
558
574
  }
@@ -574,14 +590,50 @@ var Metamask = class extends Wallet {
574
590
  await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
575
591
  const menuBtn = this.page.getByTestId("account-options-menu-button");
576
592
  await menuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT });
577
- await menuBtn.click();
593
+ await menuBtn.click({ force: true });
578
594
  await this.page.locator("text=Log out").click();
579
595
  }
580
596
  /**
581
- * Unlock MetaMask with password.
582
- * After unlocking, stabilizes the wallet UI by handling post-unlock
583
- * screens (metametrics, onboarding completion) and dismissing queued
584
- * notifications. Ends on home.html with the wallet UI ready.
597
+ * Force-persist the vault by locking the wallet. Called by the cache builder
598
+ * before closing the browser context. MetaMask MV3 stores the vault lazily —
599
+ * locking forces the encrypted vault to be written to chrome.storage.local.
600
+ */
601
+ async beforeCacheClose() {
602
+ debug("metamask.beforeCacheClose: locking wallet to force vault persistence");
603
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
604
+ const menuBtn = this.page.getByTestId("account-options-menu-button");
605
+ const lockInput = this.page.getByTestId("unlock-password");
606
+ const state = await Promise.race([
607
+ menuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "ready"),
608
+ lockInput.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "locked")
609
+ ]).catch(() => "timeout");
610
+ debug(`metamask.beforeCacheClose: state=${state}`);
611
+ if (state === "ready") {
612
+ await this.lock();
613
+ await lockInput.waitFor({ state: "visible", timeout: LOCK_SCREEN_TIMEOUT }).catch(() => {
614
+ });
615
+ } else if (state === "timeout") {
616
+ debug("metamask.beforeCacheClose: forcing state sync via new UI connection");
617
+ const syncPage = await this.page.context().newPage();
618
+ await syncPage.goto(`chrome-extension://${this.extensionId}/home.html`);
619
+ await syncPage.waitForLoadState("networkidle");
620
+ const syncMenuBtn = syncPage.getByTestId("account-options-menu-button");
621
+ const syncLockInput = syncPage.getByTestId("unlock-password");
622
+ const syncState = await Promise.race([
623
+ syncMenuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "ready"),
624
+ syncLockInput.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "locked")
625
+ ]).catch(() => "timeout");
626
+ debug(`metamask.beforeCacheClose: syncPage state=${syncState}`);
627
+ if (syncState === "ready") {
628
+ await syncMenuBtn.click({ force: true });
629
+ await syncPage.locator("text=Log out").click();
630
+ await syncLockInput.waitFor({ state: "visible", timeout: LOCK_SCREEN_TIMEOUT }).catch(() => {
631
+ });
632
+ }
633
+ await syncPage.close();
634
+ }
635
+ }
636
+ /**
585
637
  * Unlock MetaMask with password.
586
638
  * After unlocking, stabilizes the wallet UI by handling post-unlock
587
639
  * screens (metametrics, onboarding completion) and dismissing queued
@@ -591,14 +643,21 @@ var Metamask = class extends Wallet {
591
643
  debug("metamask.unlock: starting");
592
644
  const pwd = password ?? this.defaultPassword;
593
645
  await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
594
- await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
595
- const passwordInput = this.page.getByTestId("unlock-password");
596
- await passwordInput.fill(pwd);
597
- await this.page.getByTestId("unlock-submit").click();
598
- await this.page.waitForSelector('[data-testid="unlock-password"]', {
599
- state: "hidden",
600
- timeout: LOCK_SCREEN_TIMEOUT
601
- });
646
+ const lockInput = this.page.getByTestId("unlock-password");
647
+ const readyIndicator = this.page.getByTestId("account-options-menu-button");
648
+ const state = await Promise.race([
649
+ lockInput.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "locked"),
650
+ readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready")
651
+ ]).catch(() => "timeout");
652
+ debug(`metamask.unlock: state=${state}`);
653
+ if (state === "locked") {
654
+ await lockInput.fill(pwd);
655
+ await this.page.getByTestId("unlock-submit").click();
656
+ await this.page.waitForSelector('[data-testid="unlock-password"]', {
657
+ state: "hidden",
658
+ timeout: LOCK_SCREEN_TIMEOUT
659
+ });
660
+ }
602
661
  await this.stabilizePostUnlock();
603
662
  debug("metamask.unlock: complete");
604
663
  }
@@ -610,7 +669,7 @@ var Metamask = class extends Wallet {
610
669
  debug(`metamask.switchNetwork: ${networkName} (${networkType})`);
611
670
  await this.page.getByTestId("sort-by-networks").click();
612
671
  if (networkType === "Custom") {
613
- await this.page.getByRole("tab", { name: "Custom" }).click();
672
+ await this.page.getByRole("tab", { name: "Custom" }).click({ force: true });
614
673
  }
615
674
  await this.page.getByText(networkName).click();
616
675
  await expect(this.page.getByTestId("sort-by-networks")).toHaveText(
@@ -636,7 +695,7 @@ var Metamask = class extends Wallet {
636
695
  await this.page.getByRole("button", { name: /save/i }).click();
637
696
  }
638
697
  async addCustomNetwork(settings) {
639
- await this.page.getByTestId("account-options-menu-button").click();
698
+ await this.page.getByTestId("account-options-menu-button").click({ force: true });
640
699
  await this.page.getByTestId("global-menu-networks").click();
641
700
  await this.page.getByRole("button", { name: "Add a custom network" }).click();
642
701
  await this.page.getByTestId("network-form-network-name").fill(settings.name);
@@ -647,11 +706,17 @@ var Metamask = class extends Wallet {
647
706
  await this.page.getByTestId("rpc-url-input-test").fill(settings.rpc);
648
707
  await this.page.getByRole("button", { name: "Add URL" }).click();
649
708
  await this.page.getByRole("button", { name: "Save" }).click();
709
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
710
+ await this.page.waitForLoadState("domcontentloaded");
650
711
  }
651
712
  async enableTestNetworks() {
652
- await this.page.getByTestId("account-options-menu-button").click();
713
+ await this.page.getByTestId("account-options-menu-button").click({ force: true });
653
714
  await this.page.getByTestId("global-menu-networks").click();
654
- await this.page.locator("text=Show test networks >> xpath=following-sibling::label").click();
715
+ const toggle = this.page.locator(
716
+ "text=Show test networks >> xpath=following-sibling::label"
717
+ );
718
+ await expect(toggle).toBeVisible({ timeout: config.expectTimeout });
719
+ await toggle.click();
655
720
  await this.page.keyboard.press("Escape");
656
721
  }
657
722
  async importAccount(privateKey) {
@@ -89,7 +89,8 @@ async function waitForStorageStable(helperPage, helperUrl) {
89
89
  const POLL_INTERVAL = STORAGE_POLL_INTERVAL;
90
90
  const STABLE_CHECKS_REQUIRED = STORAGE_STABLE_CHECKS;
91
91
  const start = Date.now();
92
- let lastKeyCount = -1;
92
+ let lastHash = "";
93
+ let lastKeyCount = 0;
93
94
  let stableCount = 0;
94
95
  while (Date.now() - start < TIMEOUT) {
95
96
  await sleep(POLL_INTERVAL);
@@ -100,16 +101,19 @@ async function waitForStorageStable(helperPage, helperUrl) {
100
101
  { timeout: HELPER_PAGE_TIMEOUT }
101
102
  );
102
103
  const title = await helperPage.title();
103
- const keyCount = parseInt(title.split(":")[1], 10);
104
+ const parts = title.split(":");
105
+ const keyCount = parseInt(parts[1], 10);
106
+ const hash = parts[2] ?? "";
104
107
  if (keyCount === 0) continue;
105
- if (keyCount === lastKeyCount) {
108
+ if (hash === lastHash) {
106
109
  stableCount++;
107
110
  } else {
108
111
  stableCount = 1;
112
+ lastHash = hash;
109
113
  lastKeyCount = keyCount;
110
114
  }
111
115
  debug(
112
- `Storage poll: ${keyCount} keys (stable ${stableCount}/${STABLE_CHECKS_REQUIRED})`
116
+ `Storage poll: ${keyCount} keys, hash=${hash.slice(0, 8)} (stable ${stableCount}/${STABLE_CHECKS_REQUIRED})`
113
117
  );
114
118
  if (stableCount >= STABLE_CHECKS_REQUIRED) {
115
119
  console.log(` Storage stabilized at ${keyCount} keys`);
@@ -176,16 +180,26 @@ async function buildCacheForSetup(compiledFilePath, originalFilePath, options2 =
176
180
  await config.setupFn(wallet, page);
177
181
  await page.goto(`chrome-extension://${extensionId}/home.html`);
178
182
  await page.waitForLoadState("networkidle");
179
- await page.goto(`chrome-extension://${extensionId}/home.html`);
180
- await page.waitForLoadState("networkidle");
183
+ if (wallet.beforeCacheClose) {
184
+ debug("buildCache: calling wallet.beforeCacheClose()");
185
+ await wallet.beforeCacheClose();
186
+ await sleep(2e3);
187
+ }
181
188
  try {
182
189
  const extDir = import_path2.default.join(process.cwd(), W3WALLETS_DIR, config.extensionDir);
183
190
  const helperJs = import_path2.default.join(extDir, "_w3wallets_helper.js");
184
191
  const helperHtml = import_path2.default.join(extDir, "_w3wallets_helper.html");
185
192
  import_fs2.default.writeFileSync(
186
193
  helperJs,
187
- `chrome.storage.local.get(null, (data) => {
188
- document.title = "done:" + Object.keys(data).length;
194
+ `chrome.storage.local.get(null, async (data) => {
195
+ const keys = Object.keys(data);
196
+ const json = JSON.stringify(data);
197
+ // Compute a simple hash of all values via SubtleCrypto
198
+ const buf = new TextEncoder().encode(json);
199
+ const hashBuf = await crypto.subtle.digest("SHA-256", buf);
200
+ const hashArr = Array.from(new Uint8Array(hashBuf));
201
+ const hash = hashArr.map(b => b.toString(16).padStart(2, "0")).join("");
202
+ document.title = "done:" + keys.length + ":" + hash;
189
203
  });`
190
204
  );
191
205
  import_fs2.default.writeFileSync(
@@ -231,7 +245,8 @@ async function buildAllCaches(directory, options2 = {}) {
231
245
  outDir: distDir,
232
246
  format: ["cjs"],
233
247
  clean: true,
234
- silent: true
248
+ silent: true,
249
+ external: ["@playwright/test"]
235
250
  });
236
251
  let built = 0;
237
252
  let skipped = 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w3wallets",
3
3
  "description": "browser wallets for playwright",
4
- "version": "1.0.0-beta.7",
4
+ "version": "1.0.0-beta.9",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "homepage": "https://github.com/Maksandre/w3wallets",
@@ -28,7 +28,7 @@
28
28
  "files": [
29
29
  "dist"
30
30
  ],
31
- "bin": "./dist/scripts/download.js",
31
+ "bin": "./src/scripts/download.js",
32
32
  "scripts": {
33
33
  "download-wallets": "npx w3wallets pjs mm",
34
34
  "cache-wallets": "npx w3wallets cache --force ./tests/wallets-cache/",