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.d.mts CHANGED
@@ -2,10 +2,16 @@ import * as playwright_test from 'playwright/test';
2
2
  import { Page, test, BrowserContext } from '@playwright/test';
3
3
 
4
4
  interface IWallet {
5
- readonly page: Page;
5
+ page: Page;
6
6
  gotoOnboardPage(): Promise<void>;
7
7
  approve(): Promise<void>;
8
8
  deny(): Promise<void>;
9
+ /**
10
+ * Called by the cache builder after setupFn completes, before the browser
11
+ * context is closed. Wallet implementations can override this to force
12
+ * persistence of in-memory state (e.g., locking the vault).
13
+ */
14
+ beforeCacheClose?(): Promise<void>;
9
15
  }
10
16
  /**
11
17
  * Configuration object for a wallet.
@@ -78,7 +84,7 @@ declare function withWallets<const T extends readonly WalletConfig[]>(test: type
78
84
  }, playwright_test.PlaywrightWorkerArgs & playwright_test.PlaywrightWorkerOptions>;
79
85
 
80
86
  declare abstract class Wallet implements IWallet {
81
- readonly page: Page;
87
+ page: Page;
82
88
  readonly extensionId: string;
83
89
  constructor(page: Page, extensionId: string);
84
90
  abstract gotoOnboardPage(): Promise<void>;
@@ -121,10 +127,22 @@ declare class Metamask extends Wallet {
121
127
  * confirmation-cancel-button is available.
122
128
  */
123
129
  private dismissQueuedNotifications;
130
+ /**
131
+ * Close extra MetaMask extension pages that the service worker opened
132
+ * during onboarding (e.g. home.html#/onboarding/completion).
133
+ * Keeps only this.page alive.
134
+ */
135
+ private closeExtraExtensionPages;
136
+ /**
137
+ * Poll the URL until it leaves a confirmation route. Uses polling instead
138
+ * of waitForURL because MetaMask uses HashRouter and hash-only changes
139
+ * are not reliably caught by Playwright's navigation events.
140
+ */
141
+ private waitForConfirmRouteExit;
124
142
  /**
125
143
  * Wait for a target button while handling the Transaction Shield popup.
126
- * Always navigates to sidepanel.html fresh so MetaMask's
127
- * ConfirmationHandler can route to the pending approval.
144
+ * Navigates to sidepanel.html so MetaMask's ConfirmationHandler routes
145
+ * to the pending approval, then waits for the button to become visible.
128
146
  */
129
147
  private waitAndClickButton;
130
148
  approve(): Promise<void>;
@@ -134,10 +152,12 @@ declare class Metamask extends Wallet {
134
152
  */
135
153
  lock(): Promise<void>;
136
154
  /**
137
- * Unlock MetaMask with password.
138
- * After unlocking, stabilizes the wallet UI by handling post-unlock
139
- * screens (metametrics, onboarding completion) and dismissing queued
140
- * notifications. Ends on home.html with the wallet UI ready.
155
+ * Force-persist the vault by locking the wallet. Called by the cache builder
156
+ * before closing the browser context. MetaMask MV3 stores the vault lazily —
157
+ * locking forces the encrypted vault to be written to chrome.storage.local.
158
+ */
159
+ beforeCacheClose(): Promise<void>;
160
+ /**
141
161
  * Unlock MetaMask with password.
142
162
  * After unlocking, stabilizes the wallet UI by handling post-unlock
143
163
  * screens (metametrics, onboarding completion) and dismissing queued
package/dist/index.d.ts CHANGED
@@ -2,10 +2,16 @@ import * as playwright_test from 'playwright/test';
2
2
  import { Page, test, BrowserContext } from '@playwright/test';
3
3
 
4
4
  interface IWallet {
5
- readonly page: Page;
5
+ page: Page;
6
6
  gotoOnboardPage(): Promise<void>;
7
7
  approve(): Promise<void>;
8
8
  deny(): Promise<void>;
9
+ /**
10
+ * Called by the cache builder after setupFn completes, before the browser
11
+ * context is closed. Wallet implementations can override this to force
12
+ * persistence of in-memory state (e.g., locking the vault).
13
+ */
14
+ beforeCacheClose?(): Promise<void>;
9
15
  }
10
16
  /**
11
17
  * Configuration object for a wallet.
@@ -78,7 +84,7 @@ declare function withWallets<const T extends readonly WalletConfig[]>(test: type
78
84
  }, playwright_test.PlaywrightWorkerArgs & playwright_test.PlaywrightWorkerOptions>;
79
85
 
80
86
  declare abstract class Wallet implements IWallet {
81
- readonly page: Page;
87
+ page: Page;
82
88
  readonly extensionId: string;
83
89
  constructor(page: Page, extensionId: string);
84
90
  abstract gotoOnboardPage(): Promise<void>;
@@ -121,10 +127,22 @@ declare class Metamask extends Wallet {
121
127
  * confirmation-cancel-button is available.
122
128
  */
123
129
  private dismissQueuedNotifications;
130
+ /**
131
+ * Close extra MetaMask extension pages that the service worker opened
132
+ * during onboarding (e.g. home.html#/onboarding/completion).
133
+ * Keeps only this.page alive.
134
+ */
135
+ private closeExtraExtensionPages;
136
+ /**
137
+ * Poll the URL until it leaves a confirmation route. Uses polling instead
138
+ * of waitForURL because MetaMask uses HashRouter and hash-only changes
139
+ * are not reliably caught by Playwright's navigation events.
140
+ */
141
+ private waitForConfirmRouteExit;
124
142
  /**
125
143
  * Wait for a target button while handling the Transaction Shield popup.
126
- * Always navigates to sidepanel.html fresh so MetaMask's
127
- * ConfirmationHandler can route to the pending approval.
144
+ * Navigates to sidepanel.html so MetaMask's ConfirmationHandler routes
145
+ * to the pending approval, then waits for the button to become visible.
128
146
  */
129
147
  private waitAndClickButton;
130
148
  approve(): Promise<void>;
@@ -134,10 +152,12 @@ declare class Metamask extends Wallet {
134
152
  */
135
153
  lock(): Promise<void>;
136
154
  /**
137
- * Unlock MetaMask with password.
138
- * After unlocking, stabilizes the wallet UI by handling post-unlock
139
- * screens (metametrics, onboarding completion) and dismissing queued
140
- * notifications. Ends on home.html with the wallet UI ready.
155
+ * Force-persist the vault by locking the wallet. Called by the cache builder
156
+ * before closing the browser context. MetaMask MV3 stores the vault lazily —
157
+ * locking forces the encrypted vault to be written to chrome.storage.local.
158
+ */
159
+ beforeCacheClose(): Promise<void>;
160
+ /**
141
161
  * Unlock MetaMask with password.
142
162
  * After unlocking, stabilizes the wallet UI by handling post-unlock
143
163
  * screens (metametrics, onboarding completion) and dismissing queued
package/dist/index.js CHANGED
@@ -98,12 +98,12 @@ var ARIA_CLOSE_TIMEOUT = 500;
98
98
  var POPUP_HIDDEN_TIMEOUT = 3e3;
99
99
  var POST_UNLOCK_TIMEOUT = 3e4;
100
100
  var NOTIFICATION_CHECK_TIMEOUT = 5e3;
101
- var CONFIRMATION_ROUTE_TIMEOUT = 15e3;
102
101
  var POST_CLICK_TIMEOUT = 1e4;
103
102
  var BUTTON_OR_POPUP_TIMEOUT = 3e4;
104
103
  var LAST_RESORT_CLICK_TIMEOUT = 1e4;
105
104
  var LOCK_SCREEN_TIMEOUT = 3e4;
106
105
  var MENU_BUTTON_TIMEOUT = 3e4;
106
+ var ONBOARD_VISIBLE_TIMEOUT = 3e4;
107
107
  var MNEMONIC_KEY_DELAY = 5;
108
108
  var MNEMONIC_WORD_DELAY = 100;
109
109
 
@@ -293,7 +293,6 @@ async function findCachedExtension(context, ExtensionClass, expectedExtensionId,
293
293
  const page = await context.newPage();
294
294
  if (homeUrl) {
295
295
  await page.goto(`chrome-extension://${expectedExtensionId}/${homeUrl}`);
296
- await page.goto(`chrome-extension://${expectedExtensionId}/${homeUrl}`);
297
296
  }
298
297
  const extension = new ExtensionClass(page, expectedExtensionId);
299
298
  return extension;
@@ -360,7 +359,7 @@ var Metamask = class extends Wallet {
360
359
  await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
361
360
  await (0, import_test3.expect)(
362
361
  this.page.getByRole("button", { name: "I have an existing wallet" })
363
- ).toBeVisible({ timeout: config.expectTimeout });
362
+ ).toBeVisible({ timeout: ONBOARD_VISIBLE_TIMEOUT });
364
363
  }
365
364
  /**
366
365
  * Onboard MetaMask with a mnemonic phrase
@@ -385,7 +384,6 @@ var Metamask = class extends Wallet {
385
384
  }
386
385
  const continueBtn = this.page.getByTestId("import-srp-confirm");
387
386
  await (0, import_test3.expect)(continueBtn).toBeEnabled({ timeout: config.expectTimeout });
388
- await (0, import_test3.expect)(continueBtn).toBeEnabled({ timeout: config.expectTimeout });
389
387
  await continueBtn.click();
390
388
  const passwordInputs = this.page.locator('input[type="password"]');
391
389
  await passwordInputs.nth(0).fill(pwd);
@@ -399,10 +397,28 @@ var Metamask = class extends Wallet {
399
397
  });
400
398
  await openWalletBtn.click();
401
399
  await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
402
- await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
400
+ const readyIndicator = this.page.getByTestId("account-options-menu-button");
401
+ const lockInput = this.page.getByTestId("unlock-password");
402
+ const state = await Promise.race([
403
+ readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready"),
404
+ lockInput.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "locked")
405
+ ]).catch(() => "timeout");
406
+ debug(`metamask.onboard: post-navigate state=${state}`);
407
+ if (state === "locked") {
408
+ debug("metamask.onboard: vault locked after onboarding, auto-unlocking");
409
+ await lockInput.fill(password ?? this.defaultPassword);
410
+ await this.page.getByTestId("unlock-submit").click();
411
+ await this.page.waitForSelector('[data-testid="unlock-password"]', {
412
+ state: "hidden",
413
+ timeout: LOCK_SCREEN_TIMEOUT
414
+ });
415
+ await this.stabilizePostUnlock();
416
+ }
417
+ await this.closeExtraExtensionPages();
403
418
  await this.page.goto(
404
419
  `chrome-extension://${this.extensionId}/sidepanel.html`
405
420
  );
421
+ await this.page.waitForLoadState("networkidle");
406
422
  debug("metamask.onboard: complete");
407
423
  }
408
424
  /**
@@ -518,10 +534,37 @@ var Metamask = class extends Wallet {
518
534
  }
519
535
  await (0, import_test3.expect)(readyIndicator).toBeVisible({ timeout: POST_UNLOCK_TIMEOUT });
520
536
  }
537
+ /**
538
+ * Close extra MetaMask extension pages that the service worker opened
539
+ * during onboarding (e.g. home.html#/onboarding/completion).
540
+ * Keeps only this.page alive.
541
+ */
542
+ async closeExtraExtensionPages() {
543
+ const extOrigin = `chrome-extension://${this.extensionId}`;
544
+ const context = this.page.context();
545
+ const extras = context.pages().filter((p) => p !== this.page && p.url().startsWith(extOrigin));
546
+ if (extras.length > 0) {
547
+ debug(
548
+ `metamask.closeExtraExtensionPages: closing ${extras.length} extra page(s)`
549
+ );
550
+ await Promise.all(extras.map((p) => p.close()));
551
+ }
552
+ }
553
+ /**
554
+ * Poll the URL until it leaves a confirmation route. Uses polling instead
555
+ * of waitForURL because MetaMask uses HashRouter and hash-only changes
556
+ * are not reliably caught by Playwright's navigation events.
557
+ */
558
+ async waitForConfirmRouteExit(routePattern) {
559
+ const deadline = Date.now() + POST_CLICK_TIMEOUT;
560
+ while (routePattern.test(this.page.url()) && Date.now() < deadline) {
561
+ await this.page.waitForTimeout(500);
562
+ }
563
+ }
521
564
  /**
522
565
  * Wait for a target button while handling the Transaction Shield popup.
523
- * Always navigates to sidepanel.html fresh so MetaMask's
524
- * ConfirmationHandler can route to the pending approval.
566
+ * Navigates to sidepanel.html so MetaMask's ConfirmationHandler routes
567
+ * to the pending approval, then waits for the button to become visible.
525
568
  */
526
569
  async waitAndClickButton(btnLocator) {
527
570
  debug(`metamask.waitAndClickButton: navigating to sidepanel`);
@@ -532,63 +575,36 @@ var Metamask = class extends Wallet {
532
575
  btnLocator.first().waitFor({ state: "visible", timeout }).then(() => "button"),
533
576
  popup.first().waitFor({ state: "visible", timeout }).then(() => "popup")
534
577
  ]).catch(() => "timeout");
535
- const handlePopupAndClick = async () => {
536
- await this.dismissPopups();
537
- await btnLocator.first().waitFor({ state: "visible", timeout: BUTTON_OR_POPUP_TIMEOUT });
538
- await btnLocator.first().click();
539
- };
540
578
  await this.page.goto(sidepanelUrl);
541
- try {
542
- await this.page.waitForURL(confirmRoutePattern, {
543
- timeout: CONFIRMATION_ROUTE_TIMEOUT
544
- });
545
- } catch {
546
- console.warn(
547
- `[w3wallets] confirmation route not found, retrying. URL: ${this.page.url()}`
579
+ let result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
580
+ if (result === "timeout") {
581
+ debug(
582
+ `metamask.waitAndClickButton: first wait timed out, reloading. URL: ${this.page.url()}`
548
583
  );
549
584
  await this.page.goto(sidepanelUrl);
550
- try {
551
- await this.page.waitForURL(confirmRoutePattern, {
552
- timeout: CONFIRMATION_ROUTE_TIMEOUT
553
- });
554
- } catch {
555
- console.warn(
556
- `[w3wallets] confirmation route not found after retry. URL: ${this.page.url()}`
557
- );
558
- }
585
+ result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
559
586
  }
560
- const result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
561
587
  debug(
562
588
  `metamask.waitAndClickButton: result=${result}, URL=${this.page.url()}`
563
589
  );
564
590
  if (result === "button") {
565
591
  await btnLocator.first().click();
566
- await this.page.waitForURL((url) => !confirmRoutePattern.test(url.toString()), {
567
- timeout: POST_CLICK_TIMEOUT
568
- }).catch(() => {
569
- console.warn(
570
- `[w3wallets] still on confirmation route after click. URL: ${this.page.url()}`
571
- );
572
- });
592
+ await this.waitForConfirmRouteExit(confirmRoutePattern);
573
593
  return;
574
594
  }
575
595
  if (result === "popup") {
576
- await handlePopupAndClick();
577
- await this.page.waitForURL((url) => !confirmRoutePattern.test(url.toString()), {
578
- timeout: POST_CLICK_TIMEOUT
579
- }).catch(() => {
580
- console.warn(
581
- `[w3wallets] still on confirmation route after popup dismiss. URL: ${this.page.url()}`
582
- );
583
- });
596
+ await this.dismissPopups();
597
+ await btnLocator.first().waitFor({ state: "visible", timeout: BUTTON_OR_POPUP_TIMEOUT });
598
+ await btnLocator.first().click();
599
+ await this.waitForConfirmRouteExit(confirmRoutePattern);
584
600
  return;
585
601
  }
586
602
  const isOnConfirmRoute = confirmRoutePattern.test(this.page.url());
587
603
  debug(
588
- `metamask.waitAndClickButton: timeout after ${BUTTON_OR_POPUP_TIMEOUT}ms. URL: ${this.page.url()}, onConfirmRoute: ${isOnConfirmRoute}`
604
+ `metamask.waitAndClickButton: timeout. URL: ${this.page.url()}, onConfirmRoute: ${isOnConfirmRoute}`
589
605
  );
590
606
  console.warn(
591
- `[w3wallets] no button or popup found after ${BUTTON_OR_POPUP_TIMEOUT / 1e3}s. URL: ${this.page.url()}`
607
+ `[w3wallets] no button or popup found after waiting. URL: ${this.page.url()}`
592
608
  );
593
609
  await btnLocator.first().click({ timeout: LAST_RESORT_CLICK_TIMEOUT });
594
610
  }
@@ -610,14 +626,50 @@ var Metamask = class extends Wallet {
610
626
  await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
611
627
  const menuBtn = this.page.getByTestId("account-options-menu-button");
612
628
  await menuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT });
613
- await menuBtn.click();
629
+ await menuBtn.click({ force: true });
614
630
  await this.page.locator("text=Log out").click();
615
631
  }
616
632
  /**
617
- * Unlock MetaMask with password.
618
- * After unlocking, stabilizes the wallet UI by handling post-unlock
619
- * screens (metametrics, onboarding completion) and dismissing queued
620
- * notifications. Ends on home.html with the wallet UI ready.
633
+ * Force-persist the vault by locking the wallet. Called by the cache builder
634
+ * before closing the browser context. MetaMask MV3 stores the vault lazily —
635
+ * locking forces the encrypted vault to be written to chrome.storage.local.
636
+ */
637
+ async beforeCacheClose() {
638
+ debug("metamask.beforeCacheClose: locking wallet to force vault persistence");
639
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
640
+ const menuBtn = this.page.getByTestId("account-options-menu-button");
641
+ const lockInput = this.page.getByTestId("unlock-password");
642
+ const state = await Promise.race([
643
+ menuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "ready"),
644
+ lockInput.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "locked")
645
+ ]).catch(() => "timeout");
646
+ debug(`metamask.beforeCacheClose: state=${state}`);
647
+ if (state === "ready") {
648
+ await this.lock();
649
+ await lockInput.waitFor({ state: "visible", timeout: LOCK_SCREEN_TIMEOUT }).catch(() => {
650
+ });
651
+ } else if (state === "timeout") {
652
+ debug("metamask.beforeCacheClose: forcing state sync via new UI connection");
653
+ const syncPage = await this.page.context().newPage();
654
+ await syncPage.goto(`chrome-extension://${this.extensionId}/home.html`);
655
+ await syncPage.waitForLoadState("networkidle");
656
+ const syncMenuBtn = syncPage.getByTestId("account-options-menu-button");
657
+ const syncLockInput = syncPage.getByTestId("unlock-password");
658
+ const syncState = await Promise.race([
659
+ syncMenuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "ready"),
660
+ syncLockInput.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "locked")
661
+ ]).catch(() => "timeout");
662
+ debug(`metamask.beforeCacheClose: syncPage state=${syncState}`);
663
+ if (syncState === "ready") {
664
+ await syncMenuBtn.click({ force: true });
665
+ await syncPage.locator("text=Log out").click();
666
+ await syncLockInput.waitFor({ state: "visible", timeout: LOCK_SCREEN_TIMEOUT }).catch(() => {
667
+ });
668
+ }
669
+ await syncPage.close();
670
+ }
671
+ }
672
+ /**
621
673
  * Unlock MetaMask with password.
622
674
  * After unlocking, stabilizes the wallet UI by handling post-unlock
623
675
  * screens (metametrics, onboarding completion) and dismissing queued
@@ -627,14 +679,21 @@ var Metamask = class extends Wallet {
627
679
  debug("metamask.unlock: starting");
628
680
  const pwd = password ?? this.defaultPassword;
629
681
  await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
630
- await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
631
- const passwordInput = this.page.getByTestId("unlock-password");
632
- await passwordInput.fill(pwd);
633
- await this.page.getByTestId("unlock-submit").click();
634
- await this.page.waitForSelector('[data-testid="unlock-password"]', {
635
- state: "hidden",
636
- timeout: LOCK_SCREEN_TIMEOUT
637
- });
682
+ const lockInput = this.page.getByTestId("unlock-password");
683
+ const readyIndicator = this.page.getByTestId("account-options-menu-button");
684
+ const state = await Promise.race([
685
+ lockInput.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "locked"),
686
+ readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready")
687
+ ]).catch(() => "timeout");
688
+ debug(`metamask.unlock: state=${state}`);
689
+ if (state === "locked") {
690
+ await lockInput.fill(pwd);
691
+ await this.page.getByTestId("unlock-submit").click();
692
+ await this.page.waitForSelector('[data-testid="unlock-password"]', {
693
+ state: "hidden",
694
+ timeout: LOCK_SCREEN_TIMEOUT
695
+ });
696
+ }
638
697
  await this.stabilizePostUnlock();
639
698
  debug("metamask.unlock: complete");
640
699
  }
@@ -646,7 +705,7 @@ var Metamask = class extends Wallet {
646
705
  debug(`metamask.switchNetwork: ${networkName} (${networkType})`);
647
706
  await this.page.getByTestId("sort-by-networks").click();
648
707
  if (networkType === "Custom") {
649
- await this.page.getByRole("tab", { name: "Custom" }).click();
708
+ await this.page.getByRole("tab", { name: "Custom" }).click({ force: true });
650
709
  }
651
710
  await this.page.getByText(networkName).click();
652
711
  await (0, import_test3.expect)(this.page.getByTestId("sort-by-networks")).toHaveText(
@@ -672,7 +731,7 @@ var Metamask = class extends Wallet {
672
731
  await this.page.getByRole("button", { name: /save/i }).click();
673
732
  }
674
733
  async addCustomNetwork(settings) {
675
- await this.page.getByTestId("account-options-menu-button").click();
734
+ await this.page.getByTestId("account-options-menu-button").click({ force: true });
676
735
  await this.page.getByTestId("global-menu-networks").click();
677
736
  await this.page.getByRole("button", { name: "Add a custom network" }).click();
678
737
  await this.page.getByTestId("network-form-network-name").fill(settings.name);
@@ -683,11 +742,17 @@ var Metamask = class extends Wallet {
683
742
  await this.page.getByTestId("rpc-url-input-test").fill(settings.rpc);
684
743
  await this.page.getByRole("button", { name: "Add URL" }).click();
685
744
  await this.page.getByRole("button", { name: "Save" }).click();
745
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
746
+ await this.page.waitForLoadState("domcontentloaded");
686
747
  }
687
748
  async enableTestNetworks() {
688
- await this.page.getByTestId("account-options-menu-button").click();
749
+ await this.page.getByTestId("account-options-menu-button").click({ force: true });
689
750
  await this.page.getByTestId("global-menu-networks").click();
690
- await this.page.locator("text=Show test networks >> xpath=following-sibling::label").click();
751
+ const toggle = this.page.locator(
752
+ "text=Show test networks >> xpath=following-sibling::label"
753
+ );
754
+ await (0, import_test3.expect)(toggle).toBeVisible({ timeout: config.expectTimeout });
755
+ await toggle.click();
691
756
  await this.page.keyboard.press("Escape");
692
757
  }
693
758
  async importAccount(privateKey) {