dappress 0.6.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ What changes for the users of Dappress, version by version. The format follows [
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.1] - 2026-10-05
8
+
9
+ ### Fixed
10
+
11
+ - `cy.confirmTransaction({ gas })` no longer confirms, now and then, with the fee MetaMask estimated in place of the one asked. MetaMask's confirmation carries the new fee a moment after the fee editor closes, and confirming meanwhile sent it the old one, which it kept. The command now waits for the "Network fee" row to change, as it did for the spending cap.
12
+
7
13
  ## [0.6.0] - 2026-10-05
8
14
 
9
15
  ### Fixed
@@ -92,7 +98,8 @@ What changes for the users of Dappress, version by version. The format follows [
92
98
  - The wallet setup file, `cypress/wallet.setup.ts`, and the opt-in profile cache.
93
99
  - TypeScript declarations.
94
100
 
95
- [Unreleased]: https://github.com/blassaut/dappress/compare/v0.6.0...HEAD
101
+ [Unreleased]: https://github.com/blassaut/dappress/compare/v0.6.1...HEAD
102
+ [0.6.1]: https://github.com/blassaut/dappress/compare/v0.6.0...v0.6.1
96
103
  [0.6.0]: https://github.com/blassaut/dappress/compare/v0.5.1...v0.6.0
97
104
  [0.5.1]: https://github.com/blassaut/dappress/compare/v0.5.0...v0.5.1
98
105
  [0.5.0]: https://github.com/blassaut/dappress/compare/v0.4.0...v0.5.0
@@ -153,7 +153,7 @@ async function withUsableWindow(page) {
153
153
  const { windowId, bounds } = await session.send('Browser.getWindowForTarget');
154
154
  if (bounds.windowState === 'normal' && (bounds.width ?? 0) >= 200 && (bounds.height ?? 0) >= 200)
155
155
  return page;
156
- console.log(`[dappress] MetaMask's popup is ${bounds.width}x${bounds.height} (${bounds.windowState}); resizing it to ${POPUP.width}x${POPUP.height}`);
156
+ console.log(`[dappress] MetaMask's popup is ${bounds.width}x${bounds.height} (${bounds.windowState}), as under Xvfb without a window manager; resizing it to ${POPUP.width}x${POPUP.height}`);
157
157
  if (bounds.windowState !== 'normal')
158
158
  await session.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });
159
159
  await session.send('Browser.setWindowBounds', { windowId, bounds: { ...POPUP } });
package/dist/metamask.js CHANGED
@@ -137,6 +137,8 @@ const selectors = {
137
137
  gasForm: testId('gas-fee-advanced-eip1559-modal'),
138
138
  gasField: (id) => [`#${id}`, `${testId(id)} input`],
139
139
  gasSave: [testId('gas-fee-modal-save-button'), buttonText('Save')],
140
+ // The "Network fee" row: its amount and the name of its estimate
141
+ feeSection: testId('gas-fee-section'),
140
142
  },
141
143
  // The older footer, still used by the permission update a network switch
142
144
  // asks for, by "Add suggested tokens", and by template confirmations
@@ -637,7 +639,7 @@ async function setSpendingCap(page, cap) {
637
639
  const s = selectors.transaction;
638
640
  if (!(await (0, page_helpers_1.isVisible)(page, s.editSpendingCap, 10000)))
639
641
  throw await (0, page_helpers_1.failure)(page, 'MetaMask shows no spending cap to edit: spendingCap is for an ERC-20 approval');
640
- const shown = await textOf(page, s.spendingCap);
642
+ const shown = await (0, page_helpers_1.textOf)(page, s.spendingCap);
641
643
  const asked = await openSpendingCap(page);
642
644
  await type(page, s.spendingCapInput, cap, 'spending cap');
643
645
  if (!(await pressToClose(page, s.spendingCapSave, s.spendingCapInput)))
@@ -645,9 +647,7 @@ async function setSpendingCap(page, cap) {
645
647
  // The modal closes before the confirmation has the new cap, and confirming
646
648
  // meanwhile approves the old one. Two caps may read the same once MetaMask
647
649
  // has rounded them, so a row that stays as it was is not a failure.
648
- const deadline = Date.now() + (Number(asked) === Number(cap) ? 0 : 5000);
649
- while (Date.now() < deadline && (await textOf(page, s.spendingCap)) === shown)
650
- await (0, page_helpers_1.sleep)(250);
650
+ await (0, page_helpers_1.waitForTextChange)(page, s.spendingCap, shown, Number(asked) === Number(cap) ? 0 : 5000);
651
651
  }
652
652
  /** Open the spending cap's modal. Yields the cap the dapp asked for, which its field starts with. */
653
653
  async function openSpendingCap(page) {
@@ -668,11 +668,14 @@ async function setGas(page, gas) {
668
668
  const s = selectors.transaction;
669
669
  if (!(await (0, page_helpers_1.isVisible)(page, s.editGas, 10000)))
670
670
  throw await (0, page_helpers_1.failure)(page, 'MetaMask shows no network fee to edit on this confirmation');
671
+ const shown = await (0, page_helpers_1.textOf)(page, s.feeSection);
671
672
  await openFeeEditor(page);
672
- if (typeof gas === 'string')
673
- await pickGasEstimate(page, gas);
674
- else
675
- await fillGasForm(page, gas);
673
+ const changed = typeof gas === 'string' ? await pickGasEstimate(page, gas) : await fillGasForm(page, gas);
674
+ // The editor closes before the confirmation carries the new fee, and
675
+ // confirming meanwhile sends MetaMask the old one, which it keeps: the row
676
+ // is waited for to change. A fee set as it already was changes nothing.
677
+ if (changed)
678
+ await (0, page_helpers_1.waitForTextChange)(page, s.feeSection, shown, 5000);
676
679
  }
677
680
  async function openFeeEditor(page) {
678
681
  const s = selectors.transaction;
@@ -687,32 +690,45 @@ async function openFeeEditor(page) {
687
690
  // Picking an estimate closes the editor. MetaMask lists the estimates it has
688
691
  // for the network: Low, Market and Aggressive where it has fee estimates,
689
692
  // "Network suggested" where it only has a gas price, as on a local node.
693
+ // Yields whether the transaction had another estimate before.
690
694
  async function pickGasEstimate(page, name) {
691
695
  const s = selectors.transaction;
692
696
  const option = s.gasOption(...gasEstimates[name]);
693
697
  if (!(await (0, page_helpers_1.isVisible)(page, option, 5000)))
694
698
  throw await (0, page_helpers_1.failure)(page, `MetaMask offers no "${name}" fee for this transaction`);
699
+ const selected = await hasEstimate(page, option);
695
700
  for (let attempt = 0; attempt < 3; attempt++) {
696
701
  await (0, page_helpers_1.click)(page, option);
697
702
  if (await (0, page_helpers_1.isGone)(page, s.gasEstimates, 10000))
698
- return;
703
+ return !selected;
699
704
  }
700
705
  throw await (0, page_helpers_1.failure)(page, `MetaMask did not take the "${name}" fee`);
701
706
  }
707
+ // The estimate the transaction has is marked by a class on its row
708
+ async function hasEstimate(page, option) {
709
+ const element = await (0, page_helpers_1.waitFor)(page, option);
710
+ return element.evaluate((el) => el.className.includes('--selected')).catch(() => false);
711
+ }
712
+ // Yields whether a field is given another value than it had
702
713
  async function fillGasForm(page, values) {
703
714
  const s = selectors.transaction;
704
715
  await openGasForm(page);
716
+ const asked = Object.keys(gasFields).filter((field) => values[field] !== undefined);
717
+ let changed = false;
718
+ for (const field of asked) {
719
+ if (Number(await valueOf(page, s.gasField(gasFields[field]))) !== Number(values[field]))
720
+ changed = true;
721
+ }
705
722
  // The form checks each fee against the other as it stood, and keeps the old
706
723
  // value of a fee it refused: the max base fee is typed again once the
707
724
  // priority fee is in, for two fees that both move below or above the old ones.
708
- const fields = Object.keys(gasFields).filter((field) => values[field] !== undefined);
709
- if (values.maxBaseFee !== undefined && values.priorityFee !== undefined)
710
- fields.push('maxBaseFee');
725
+ const fields = values.maxBaseFee !== undefined && values.priorityFee !== undefined ? [...asked, 'maxBaseFee'] : asked;
711
726
  for (const field of fields)
712
727
  await type(page, s.gasField(gasFields[field]), values[field], field);
713
728
  // "Save" stays disabled on a value the form refuses, and the error quotes what the form says of it
714
729
  if (!(await pressToClose(page, s.gasSave, s.gasForm)))
715
730
  throw await (0, page_helpers_1.failure)(page, 'MetaMask did not save the network fee');
731
+ return changed;
716
732
  }
717
733
  // "Advanced", in the list of estimates, replaces the list with the form. A
718
734
  // transaction with a gas price gets another form, which has none of these fields.
@@ -748,10 +764,6 @@ async function valueOf(page, selector) {
748
764
  const field = await (0, page_helpers_1.waitFor)(page, selector);
749
765
  return field.evaluate((el) => el.value);
750
766
  }
751
- async function textOf(page, selector) {
752
- const element = await page.$(selector);
753
- return element ? element.evaluate((el) => (el.textContent ?? '').trim()).catch(() => null) : null;
754
- }
755
767
  /**
756
768
  * Press a footer button, then wait for the confirmation to go away: the
757
769
  * popup closes, the side panel goes back to the home screen. A click that
@@ -29,5 +29,13 @@ export declare function fill(page: Page, selector: Selector, text: string, optio
29
29
  * words for the log and as a screenshot to look at.
30
30
  */
31
31
  export declare function failure(page: Page, message: string): Promise<Error>;
32
+ /** The text of the first element `selector` matches, or null when there is none. */
33
+ export declare function textOf(page: Page, selector: string): Promise<string | null>;
34
+ /**
35
+ * Wait for the text of `selector` to differ from `shown`, at most `timeout`
36
+ * ms. For a value MetaMask sets on its page a moment after a modal that
37
+ * edits it closes: acting on the page meanwhile acts on the old value.
38
+ */
39
+ export declare function waitForTextChange(page: Page, selector: string, shown: string | null, timeout: number): Promise<void>;
32
40
  export declare function sleep(ms: number): Promise<void>;
33
41
  export {};
@@ -18,6 +18,8 @@ exports.clickWhenEnabled = clickWhenEnabled;
18
18
  exports.dispatchClick = dispatchClick;
19
19
  exports.fill = fill;
20
20
  exports.failure = failure;
21
+ exports.textOf = textOf;
22
+ exports.waitForTextChange = waitForTextChange;
21
23
  exports.sleep = sleep;
22
24
  const node_fs_1 = __importDefault(require("node:fs"));
23
25
  const node_os_1 = __importDefault(require("node:os"));
@@ -194,6 +196,21 @@ async function pageText(page) {
194
196
  return '';
195
197
  }
196
198
  }
199
+ /** The text of the first element `selector` matches, or null when there is none. */
200
+ async function textOf(page, selector) {
201
+ const element = await page.$(selector);
202
+ return element ? element.evaluate((el) => (el.textContent ?? '').trim()).catch(() => null) : null;
203
+ }
204
+ /**
205
+ * Wait for the text of `selector` to differ from `shown`, at most `timeout`
206
+ * ms. For a value MetaMask sets on its page a moment after a modal that
207
+ * edits it closes: acting on the page meanwhile acts on the old value.
208
+ */
209
+ async function waitForTextChange(page, selector, shown, timeout) {
210
+ const deadline = Date.now() + timeout;
211
+ while (Date.now() < deadline && (await textOf(page, selector)) === shown)
212
+ await sleep(250);
213
+ }
197
214
  function sleep(ms) {
198
215
  return new Promise((resolve) => setTimeout(resolve, ms));
199
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dappress",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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",