dappress 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ What changes for the users of Dappress, version by version. The format follows [
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.0] - 2026-10-05
8
+
9
+ ### Fixed
10
+
11
+ - The peer dependency on Cypress asks for 15.10 or later, the first version with `Cypress.expose()`, which the commands read their options with. Earlier versions were accepted at install, then failed as soon as the support file loaded.
12
+ - A command sent right after another was answered in the popup no longer fails on "Timed out waiting for confirm-footer-button". The popup closes a moment after its button goes, and the next command could find it, still listed on the request just answered, then lose it. A decision now waits for the popup to close, and a confirmation that closes before it is acted on is looked for again.
13
+
7
14
  ## [0.5.1] - 2026-10-05
8
15
 
9
16
  ### Changed
@@ -85,7 +92,8 @@ What changes for the users of Dappress, version by version. The format follows [
85
92
  - The wallet setup file, `cypress/wallet.setup.ts`, and the opt-in profile cache.
86
93
  - TypeScript declarations.
87
94
 
88
- [Unreleased]: https://github.com/blassaut/dappress/compare/v0.5.1...HEAD
95
+ [Unreleased]: https://github.com/blassaut/dappress/compare/v0.6.0...HEAD
96
+ [0.6.0]: https://github.com/blassaut/dappress/compare/v0.5.1...v0.6.0
89
97
  [0.5.1]: https://github.com/blassaut/dappress/compare/v0.5.0...v0.5.1
90
98
  [0.5.0]: https://github.com/blassaut/dappress/compare/v0.4.0...v0.5.0
91
99
  [0.4.0]: https://github.com/blassaut/dappress/compare/v0.3.3...v0.4.0
package/dist/actions.js CHANGED
@@ -86,16 +86,27 @@ function createTasks(options) {
86
86
  await metamask.leaveUnlockForm(page, options);
87
87
  return state;
88
88
  }
89
- /** Approve the prompt a network change raised: "Add network" or the permission to switch. */
90
- async function approveNetworkChange(browser) {
91
- const page = await (0, metamask_pages_1.getConfirmationPage)(browser, await metamaskId(browser), options.timeout);
92
- await metamask.approveNetworkChange(page, options.timeout);
89
+ /**
90
+ * Find the confirmation and act on it. One that closes before it is acted
91
+ * on was the popup of the request before, found as it closed: the request's
92
+ * own is looked for once more.
93
+ */
94
+ async function onConfirmation(browser, act) {
95
+ for (let attempt = 0;; attempt++) {
96
+ const page = await (0, metamask_pages_1.getConfirmationPage)(browser, await metamaskId(browser), options.timeout);
97
+ try {
98
+ return await act(page);
99
+ }
100
+ catch (error) {
101
+ if (attempt > 0 || !(error instanceof metamask.ConfirmationClosed))
102
+ throw error;
103
+ }
104
+ }
93
105
  }
106
+ /** Approve the prompt a network change raised: "Add network" or the permission to switch. */
107
+ const approveNetworkChange = (browser) => onConfirmation(browser, (page) => metamask.approveNetworkChange(page, options.timeout));
94
108
  /** The task for a decision: find the confirmation, press its button. */
95
- const decide = (decision) => async (browser, argument) => {
96
- const page = await (0, metamask_pages_1.getConfirmationPage)(browser, await metamaskId(browser), options.timeout);
97
- await metamask.decide(decision, page, options.timeout, argument);
98
- };
109
+ const decide = (decision) => (browser, argument) => onConfirmation(browser, (page) => metamask.decide(decision, page, options.timeout, argument));
99
110
  const actions = {
100
111
  setupWallet,
101
112
  approveNetworkChange,
package/dist/browser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  // Connects Puppeteer to the browser Cypress launched, through the debugging
3
- // URL Cypress hands out in after:browser:launch (Cypress 13.6+).
3
+ // URL Cypress hands out in after:browser:launch (Cypress 15.10+).
4
4
  var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  return (mod && mod.__esModule) ? mod : { "default": mod };
6
6
  };
@@ -20,7 +20,7 @@ function captureDebuggerUrl(on) {
20
20
  */
21
21
  async function withBrowser(action) {
22
22
  if (!debuggerUrl) {
23
- throw new Error('[dappress] No browser to connect to. Is configureDappress() called in setupNodeEvents, with Cypress 13.6 or later?');
23
+ throw new Error('[dappress] No browser to connect to. Is configureDappress() called in setupNodeEvents, with Cypress 15.10 or later?');
24
24
  }
25
25
  const browser = await puppeteer_core_1.default.connect({ browserWSEndpoint: debuggerUrl, defaultViewport: null });
26
26
  try {
@@ -12,6 +12,16 @@ export declare function getRequestPages(browser: Browser, extensionId: string):
12
12
  * The page a dapp request is shown on. Waits for the popup to open, or takes
13
13
  * the side panel. A popup still on its home route is one closing after the
14
14
  * previous request, or one that hasn't routed to the request yet: skipped.
15
+ * The route is read from the page, which follows a change of hash at once,
16
+ * where the target can still name the request just answered.
15
17
  */
16
18
  export declare function getConfirmationPage(browser: Browser, extensionId: string, timeout: number): Promise<Page>;
19
+ /**
20
+ * Once the request shown on a popup is answered, wait for the popup to close,
21
+ * at most `timeout` ms. MetaMask closes it a moment after its button goes, and
22
+ * left to close on its own, the next command could find it, still listed on
23
+ * `request`, and act on a page that vanishes under it. A popup that goes on
24
+ * to another request instead is left to it; so is the side panel, which stays.
25
+ */
26
+ export declare function waitForDismissal(page: Page, request: string, timeout: number): Promise<void>;
17
27
  export declare function withUsableWindow(page: Page): Promise<Page>;
@@ -7,6 +7,7 @@ exports.findExtensionId = findExtensionId;
7
7
  exports.getHomePage = getHomePage;
8
8
  exports.getRequestPages = getRequestPages;
9
9
  exports.getConfirmationPage = getConfirmationPage;
10
+ exports.waitForDismissal = waitForDismissal;
10
11
  exports.withUsableWindow = withUsableWindow;
11
12
  const page_helpers_1 = require("./page-helpers");
12
13
  const HOME_PATH = '/home.html';
@@ -97,14 +98,20 @@ async function getRequestPages(browser, extensionId) {
97
98
  * The page a dapp request is shown on. Waits for the popup to open, or takes
98
99
  * the side panel. A popup still on its home route is one closing after the
99
100
  * previous request, or one that hasn't routed to the request yet: skipped.
101
+ * The route is read from the page, which follows a change of hash at once,
102
+ * where the target can still name the request just answered.
100
103
  */
101
104
  async function getConfirmationPage(browser, extensionId, timeout) {
102
105
  const deadline = Date.now() + timeout;
103
106
  while (Date.now() < deadline) {
104
107
  for (const pathname of CONFIRMATION_PATHS) {
105
- const [target] = pagesOf(browser, extensionId, pathname).filter((candidate) => !isHomeRoute(candidate.url()));
106
- if (target)
107
- return pathname === '/notification.html' ? withUsableWindow(await pageOf(target)) : pageOf(target);
108
+ for (const target of pagesOf(browser, extensionId, pathname)) {
109
+ // A target closing while its page is taken has none: not a confirmation either
110
+ const page = await target.page().catch(() => null);
111
+ if (!page || page.isClosed() || isHomeRoute(page.url()))
112
+ continue;
113
+ return pathname === '/notification.html' ? withUsableWindow(page) : page;
114
+ }
108
115
  }
109
116
  await (0, page_helpers_1.sleep)(250);
110
117
  }
@@ -115,6 +122,26 @@ async function getConfirmationPage(browser, extensionId, timeout) {
115
122
  .join(', ');
116
123
  throw new Error(`[dappress] MetaMask showed no confirmation within ${timeout}ms. Did the dapp send a request? MetaMask pages seen: ${seen || 'none'}`);
117
124
  }
125
+ /**
126
+ * Once the request shown on a popup is answered, wait for the popup to close,
127
+ * at most `timeout` ms. MetaMask closes it a moment after its button goes, and
128
+ * left to close on its own, the next command could find it, still listed on
129
+ * `request`, and act on a page that vanishes under it. A popup that goes on
130
+ * to another request instead is left to it; so is the side panel, which stays.
131
+ */
132
+ async function waitForDismissal(page, request, timeout) {
133
+ if (!request.includes('/notification.html'))
134
+ return;
135
+ const deadline = Date.now() + timeout;
136
+ while (Date.now() < deadline) {
137
+ if (page.isClosed())
138
+ return;
139
+ const url = page.url();
140
+ if (url !== request && !isHomeRoute(url))
141
+ return;
142
+ await (0, page_helpers_1.sleep)(100);
143
+ }
144
+ }
118
145
  // MetaMask asks Chrome for a 400x620 popup. Under Xvfb on a GitHub runner the
119
146
  // window it gets is 1x1: nothing is laid out, the pane of a confirmation that
120
147
  // must be read to the end has no height, so it can never be scrolled there and
@@ -48,6 +48,13 @@ export declare function disconnectSite(page: Page, origin: string): Promise<void
48
48
  type CommandOptions = ConnectOptions & TransactionOptions;
49
49
  /** Press the button of `decision` on the confirmation shown on `page`, once it is set as `options` ask. */
50
50
  export declare function decide(decision: Decision, page: Page, timeout: number, options?: CommandOptions): Promise<void>;
51
+ /**
52
+ * Thrown when the confirmation page closed before its button was pressed: it
53
+ * was the popup of the request before, found as it closed. The request's own
54
+ * popup is there to be looked for.
55
+ */
56
+ export declare class ConfirmationClosed extends Error {
57
+ }
51
58
  /** wallet_addEthereumChain on a network MetaMask knows behaves like a switch: either prompt may show. */
52
59
  export declare function approveNetworkChange(page: Page, timeout: number): Promise<void>;
53
60
  export {};
package/dist/metamask.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // first, then a fallback on the button's English text. When a MetaMask release
8
8
  // moves something, this is the file to fix.
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.decisions = void 0;
10
+ exports.ConfirmationClosed = exports.decisions = void 0;
11
11
  exports.walletState = walletState;
12
12
  exports.onboard = onboard;
13
13
  exports.unlock = unlock;
@@ -20,6 +20,7 @@ exports.disconnectSite = disconnectSite;
20
20
  exports.decide = decide;
21
21
  exports.approveNetworkChange = approveNetworkChange;
22
22
  const page_helpers_1 = require("./page-helpers");
23
+ const metamask_pages_1 = require("./metamask-pages");
23
24
  const testId = (id) => `[data-testid="${id}"]`;
24
25
  // XPath rather than Puppeteer's ::-p-text(): the latter needs MutationObserver,
25
26
  // which MetaMask's sandbox (LavaMoat) blocks in its pages.
@@ -553,11 +554,30 @@ async function decide(decision, page, timeout, options) {
553
554
  const adjust = adjustments[decision];
554
555
  if (!adjust)
555
556
  throw new Error(`[dappress] ${decision} takes no options`);
556
- await (0, page_helpers_1.waitFor)(page, exports.decisions[decision], { timeout });
557
+ await waitForButton(page, exports.decisions[decision], timeout);
557
558
  await adjust(page, options, timeout);
558
559
  }
559
560
  return pressAndWaitForDismissal(page, exports.decisions[decision], timeout);
560
561
  }
562
+ /**
563
+ * Thrown when the confirmation page closed before its button was pressed: it
564
+ * was the popup of the request before, found as it closed. The request's own
565
+ * popup is there to be looked for.
566
+ */
567
+ class ConfirmationClosed extends Error {
568
+ }
569
+ exports.ConfirmationClosed = ConfirmationClosed;
570
+ // The button of a confirmation, or ConfirmationClosed if the page went meanwhile
571
+ async function waitForButton(page, button, timeout) {
572
+ try {
573
+ await (0, page_helpers_1.waitFor)(page, button, { timeout });
574
+ }
575
+ catch (error) {
576
+ if (page.isClosed())
577
+ throw new ConfirmationClosed(`[dappress] The confirmation closed before "${(0, page_helpers_1.describe)(button)}" was pressed`);
578
+ throw error;
579
+ }
580
+ }
561
581
  /** wallet_addEthereumChain on a network MetaMask knows behaves like a switch: either prompt may show. */
562
582
  function approveNetworkChange(page, timeout) {
563
583
  return pressAndWaitForDismissal(page, [...selectors.confirmation.confirm, ...selectors.pageContainer.confirm], timeout);
@@ -741,7 +761,7 @@ async function textOf(page, selector) {
741
761
  * what MetaMask logged.
742
762
  */
743
763
  async function pressAndWaitForDismissal(page, button, timeout) {
744
- await (0, page_helpers_1.waitFor)(page, button, { timeout });
764
+ await waitForButton(page, button, timeout);
745
765
  await dismissModal(page);
746
766
  const logged = recordErrors(page);
747
767
  const request = page.url();
@@ -751,7 +771,7 @@ async function pressAndWaitForDismissal(page, button, timeout) {
751
771
  if (await (0, page_helpers_1.isVisible)(page, selectors.alert.acknowledge, 500))
752
772
  await (0, page_helpers_1.click)(page, selectors.alert.acknowledge);
753
773
  if (await (0, page_helpers_1.isGone)(page, button, 3000))
754
- return;
774
+ return (0, metamask_pages_1.waitForDismissal)(page, request, 3000);
755
775
  await dismissModal(page);
756
776
  await (0, page_helpers_1.dispatchClick)(page, button, { timeout: 3000 }).catch(() => { });
757
777
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dappress",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "MetaMask automation for Cypress: load the real extension, import a test wallet, answer dapp requests with cy.* commands. Tracks the latest MetaMask release.",
5
5
  "license": "MIT",
6
6
  "author": "Benjamin Lassaut",
@@ -61,7 +61,7 @@
61
61
  "format:check": "prettier --check ."
62
62
  },
63
63
  "peerDependencies": {
64
- "cypress": ">=13.6.0"
64
+ "cypress": ">=15.10.0"
65
65
  },
66
66
  "dependencies": {
67
67
  "@scure/bip39": "^1.6.0",