test: drive ConfirmTx in the e2e suite, gate assertion included (closes #238)
All checks were successful
check / check (push) Successful in 36s
All checks were successful
check / check (push) Successful in 36s
The screen that decides what gets signed had no automated coverage of its own behaviour: no unit tests, and the e2e suite never reached it. The arithmetic underneath is well covered in src/shared/txValidation.js; the gap was the wiring — which number reaches the gate, when the gate re-runs, what the fee block renders, and whether Send is enabled. The confirmation screen quotes the fee ESTIMATE (gasLimit * gasPrice) and gates on the fee RESERVE (gasLimit * maxFeePerGas). Reading the quoted number instead was issue #154, and until now that was correct by reading only — a mutant swapping the two passed the whole suite. It no longer does: two of the new tests fail on it, one per transaction type. The harness gains a funded-balance fixture to make any of this reachable. tests/e2e/network.js now serves a configurable ETH balance, an ERC-20 holding, a latest block with a baseFeePerGas (without which ethers has no maxFeePerGas and the reserve and the estimate collapse into one number), and decimals() for the stub token. It can also refuse a gas estimate, and hold one open so the pending state can be observed rather than raced. Nine tests, over both the native ETH and the ERC-20 path: Send disabled while the estimate is pending, enabled once it lands, the fee block quoting both numbers, the distinct message for an estimate that failed, refusal for a send past the balance, refusal for a send the reserve does not cover, and the view height constant across every one of those transitions. Driving a failure path means provoking the console.error the code is supposed to emit, which the harness fails a run on. ErrorCollector gains expect(): it consumes exactly one matching record, and a declaration nothing matched fails its test just as an undeclared error does, so it cannot be used to silence anything. Both halves of that were verified by running the suite against a deliberately wrong pattern.
This commit is contained in:
603
tests/e2e/run.js
603
tests/e2e/run.js
@@ -9,6 +9,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const { formatEther } = require("ethers");
|
||||
const {
|
||||
PASSWORD,
|
||||
createWallet,
|
||||
@@ -18,7 +19,13 @@ const {
|
||||
pageCompilesWasm,
|
||||
visible,
|
||||
} = require("./harness");
|
||||
const { STUB_TOKEN, STUB_TX_HASH } = require("./network");
|
||||
const {
|
||||
FEE_ESTIMATE_WEI,
|
||||
FEE_RESERVE_WEI,
|
||||
STUB_COUNTERPARTY,
|
||||
STUB_TOKEN,
|
||||
STUB_TX_HASH,
|
||||
} = require("./network");
|
||||
const { DUST_THRESHOLD_MESSAGE } = require("../../src/popup/dustThreshold");
|
||||
|
||||
const TEST_TIMEOUT_MS = 120000;
|
||||
@@ -623,6 +630,573 @@ test("a rejected dust threshold shifts no layout (#233)", async (env) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --------------------------------------------- confirmation screen (#238)
|
||||
//
|
||||
// The screen that decides what gets signed. The arithmetic underneath it
|
||||
// lives in src/shared/txValidation.js and is unit tested there; what these
|
||||
// tests cover is the wiring — which number reaches the gate, when the gate
|
||||
// re-runs, what the fee block renders, and whether Send is enabled.
|
||||
//
|
||||
// The load-bearing one is "gates on the fee RESERVE": the confirmation
|
||||
// screen quotes the ESTIMATE and gates on the RESERVE, and issue #154 was
|
||||
// the gate reading the quoted number. Every other assertion here would
|
||||
// survive that mutation, so the funded and gap sends are deliberately sized
|
||||
// on opposite sides of the reserve while sitting on the same side of the
|
||||
// estimate.
|
||||
|
||||
// The balance the funded fixture serves, and the amounts sent against it.
|
||||
const FUNDED_ETH_WEI = 10n ** 18n;
|
||||
const FUNDED_ETH_TEXT = "1.0";
|
||||
const COMFORTABLE_AMOUNT = "0.1";
|
||||
const OVER_BALANCE_AMOUNT = "2.0";
|
||||
|
||||
// A send the balance covers to the wei once the ESTIMATE is added, and does
|
||||
// not cover once the RESERVE is. Sending this is allowed by a gate reading
|
||||
// the estimate and refused by a gate reading the reserve, which is the whole
|
||||
// discrimination these tests exist to make.
|
||||
const GAP_AMOUNT = formatEther(FUNDED_ETH_WEI - FEE_ESTIMATE_WEI);
|
||||
|
||||
// The ERC-20 side. The ETH balance is set to exactly the estimate for the
|
||||
// fee test: it covers the expected cost to the wei and falls short of the
|
||||
// reserve, so the same swap flips this assertion too — through a different
|
||||
// balance and a different message than the ETH path uses.
|
||||
const TOKEN_BALANCE_TEXT = "1.5";
|
||||
const TOKEN_AMOUNT = "0.25";
|
||||
const OVER_TOKEN_AMOUNT = "9.0";
|
||||
const FEE_ONLY_ETH_WEI = FEE_ESTIMATE_WEI;
|
||||
|
||||
function toHexWei(wei) {
|
||||
return "0x" + wei.toString(16);
|
||||
}
|
||||
|
||||
// A fee in wei as the confirmation screen writes it. Deliberately a second
|
||||
// implementation of formatFeeEth() from src/popup/views/confirmTx.js rather
|
||||
// than an import of it: that module pulls in the whole popup and cannot be
|
||||
// required outside a browser, and asserting against an independent rendering
|
||||
// is stronger than asserting a function equals itself.
|
||||
function feeEth(wei) {
|
||||
const parts = formatEther(wei).split(".");
|
||||
const dec =
|
||||
parts.length > 1 ? parts[1].slice(0, 6).replace(/0+$/, "") || "0" : "0";
|
||||
return parts[0] + "." + dec + " ETH";
|
||||
}
|
||||
|
||||
// What the confirmation screen is showing right now, read out of the DOM in
|
||||
// one pass: whether sending is allowed, which reason it is giving, what the
|
||||
// fee block says, and how tall the whole view is.
|
||||
async function confirmState(page) {
|
||||
return page.evaluate(() => {
|
||||
const el = (id) => document.getElementById(id);
|
||||
// Both mechanisms matter. The two fee messages are dropped with
|
||||
// display:none for the transaction type they cannot apply to, and
|
||||
// shown or hidden with visibility for the one they can.
|
||||
const shown = (id) => {
|
||||
const cs = getComputedStyle(el(id));
|
||||
return cs.display !== "none" && cs.visibility === "visible";
|
||||
};
|
||||
return {
|
||||
height: el("view-confirm-tx").getBoundingClientRect().height,
|
||||
type: el("confirm-type").textContent.trim(),
|
||||
balance: el("confirm-balance").textContent.trim(),
|
||||
fee: el("confirm-fee-amount").textContent.trim(),
|
||||
reserve: el("confirm-fee-reserve").textContent.trim(),
|
||||
reserveShown: shown("confirm-fee-reserve"),
|
||||
errors: shown("confirm-errors")
|
||||
? el("confirm-errors").textContent.trim()
|
||||
: "",
|
||||
amountFeeError: shown("confirm-amount-fee-error"),
|
||||
gasError: shown("confirm-gas-error"),
|
||||
feeUnknownError: shown("confirm-fee-unknown-error"),
|
||||
sendDisabled: el("btn-confirm-send").disabled,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForEstimate(page) {
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.getElementById("confirm-fee-amount").textContent.trim() !==
|
||||
"Estimating...",
|
||||
null,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
}
|
||||
|
||||
async function backToAddress(page) {
|
||||
if (await page.isVisible("#view-confirm-tx")) {
|
||||
await page.click("#btn-confirm-back");
|
||||
await visible(page, "#view-send");
|
||||
}
|
||||
if (await page.isVisible("#view-send")) {
|
||||
await page.click("#btn-send-back");
|
||||
}
|
||||
await openAddressDetail(page);
|
||||
}
|
||||
|
||||
// Drive the popup to the confirmation screen for one send.
|
||||
//
|
||||
// It waits for the send screen to be showing `balance` before filling
|
||||
// anything in. That figure is the exact number the spend gate compares
|
||||
// against, so waiting for it — rather than for a refresh to have probably
|
||||
// landed — is what keeps every assertion below deterministic after a
|
||||
// fixture change.
|
||||
async function goToConfirm(page, { token, balance, amount }) {
|
||||
await backToAddress(page);
|
||||
await page.click("#btn-send");
|
||||
await visible(page, "#view-send");
|
||||
await page.selectOption("#send-token", token);
|
||||
await page.waitForFunction(
|
||||
(want) =>
|
||||
document.getElementById("send-balance").textContent.trim() === want,
|
||||
"Current balance: " + balance,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.fill("#send-to", STUB_COUNTERPARTY);
|
||||
await page.fill("#send-amount", amount);
|
||||
await page.click("#btn-send-review");
|
||||
await visible(page, "#view-confirm-tx");
|
||||
}
|
||||
|
||||
// A balance as the main view renders it: balanceLinesForAddress() writes
|
||||
// every quantity with four decimal places.
|
||||
function quantity(wei) {
|
||||
return parseFloat(formatEther(wei)).toFixed(4);
|
||||
}
|
||||
|
||||
// Wait on the main view until a changed balance fixture has been picked up.
|
||||
//
|
||||
// Deliberately not a reload: the popup re-refreshes on a 10-second timer by
|
||||
// itself, and reloading aborts whatever fetch the home screen has open at
|
||||
// that instant, which the extension reports through log.errorf and the
|
||||
// harness — correctly — fails the run on.
|
||||
//
|
||||
// It also deliberately settles on MAIN rather than on the address screen.
|
||||
// The address screen builds the send screen's token dropdown once, from the
|
||||
// balances it holds at that moment, and nothing rebuilds it when a later
|
||||
// refresh arrives, so entering it early leaves a dropdown with no token in
|
||||
// it and the ERC-20 path unreachable.
|
||||
async function settleOnMain(env, { ethWei, expectToken }) {
|
||||
await backToAddress(env.page);
|
||||
await env.page.click("#btn-address-back");
|
||||
await visible(env.page, "#view-main");
|
||||
await env.page.waitForFunction(
|
||||
(want) =>
|
||||
document.getElementById("wallet-list").textContent.includes(want),
|
||||
quantity(ethWei),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
if (expectToken) {
|
||||
await visible(
|
||||
env.page,
|
||||
'#wallet-list [data-token="' + STUB_TOKEN.address + '"]',
|
||||
60000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test("ConfirmTx blocks sending while the fee estimate is pending (#238)", async (env) => {
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||
env.routeOpts.seedTokenBalance = true;
|
||||
await settleOnMain(env, {
|
||||
ethWei: FUNDED_ETH_WEI,
|
||||
expectToken: true,
|
||||
});
|
||||
|
||||
env.routeOpts.holdGasEstimate = true;
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
env.ethPendingHeight = st.height;
|
||||
console.log("# confirm-tx ETH view height: " + st.height + "px");
|
||||
assert(
|
||||
st.type === "Native ETH transfer",
|
||||
"unexpected transaction type: " + JSON.stringify(st.type),
|
||||
);
|
||||
assert(
|
||||
st.fee === "Estimating...",
|
||||
"the fee line is not showing the pending placeholder: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(!st.reserveShown, "the reserve line is shown before any estimate");
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is enabled while the fee estimate is still in flight",
|
||||
);
|
||||
assert(
|
||||
!st.feeUnknownError,
|
||||
"the estimate-failed message is shown for an estimate that is merely pending",
|
||||
);
|
||||
assert(
|
||||
!st.amountFeeError && !st.gasError && st.errors === "",
|
||||
"a balance message is shown before the fee is known",
|
||||
);
|
||||
});
|
||||
|
||||
test("ConfirmTx enables Send once the estimate lands, quoting both numbers (#238)", async (env) => {
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
assert(
|
||||
st.balance === FUNDED_ETH_TEXT + " ETH",
|
||||
"the confirmation screen shows the wrong balance: " +
|
||||
JSON.stringify(st.balance),
|
||||
);
|
||||
assert(
|
||||
st.fee === "~" + feeEth(FEE_ESTIMATE_WEI),
|
||||
"the fee line does not quote the estimate: " + JSON.stringify(st.fee),
|
||||
);
|
||||
assert(st.reserveShown, "the reserve line is not shown once the fee lands");
|
||||
assert(
|
||||
st.reserve === "up to " + feeEth(FEE_RESERVE_WEI) + " reserved",
|
||||
"the reserve line does not quote the reserve: " +
|
||||
JSON.stringify(st.reserve),
|
||||
);
|
||||
assert(
|
||||
!st.sendDisabled,
|
||||
"Send is disabled for a comfortably funded transfer",
|
||||
);
|
||||
assert(
|
||||
st.errors === "" &&
|
||||
!st.amountFeeError &&
|
||||
!st.gasError &&
|
||||
!st.feeUnknownError,
|
||||
"a balance message is shown for a comfortably funded transfer",
|
||||
);
|
||||
assert(
|
||||
st.height === env.ethPendingHeight,
|
||||
"the view changed height when the estimate landed: " +
|
||||
env.ethPendingHeight +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
});
|
||||
|
||||
// The one that closes the hole. Everything else here survives a gate that
|
||||
// reads the displayed estimate instead of the reserve; this does not.
|
||||
test("ConfirmTx gates on the fee RESERVE, not the displayed estimate (#238)", async (env) => {
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: GAP_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
// Printed on every run, pass or fail: the two fee numbers and the gate's
|
||||
// decision side by side is the measurement this test is really making.
|
||||
console.log(
|
||||
"# gate probe: balance=" +
|
||||
FUNDED_ETH_TEXT +
|
||||
" ETH amount=" +
|
||||
GAP_AMOUNT +
|
||||
" estimate=" +
|
||||
feeEth(FEE_ESTIMATE_WEI) +
|
||||
" reserve=" +
|
||||
feeEth(FEE_RESERVE_WEI) +
|
||||
" sendDisabled=" +
|
||||
st.sendDisabled +
|
||||
" amountFeeError=" +
|
||||
st.amountFeeError,
|
||||
);
|
||||
assert(
|
||||
st.fee === "~" + feeEth(FEE_ESTIMATE_WEI),
|
||||
"the screen is not quoting the estimate, so this send is not in the gap: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is ENABLED for a transfer the fee RESERVE does not cover — the " +
|
||||
"spend gate is reading the displayed estimate, which is issue #154",
|
||||
);
|
||||
assert(
|
||||
st.amountFeeError,
|
||||
"the amount-plus-fee message is not shown for a send the reserve does not cover",
|
||||
);
|
||||
assert(
|
||||
st.height === env.ethPendingHeight,
|
||||
"the over-budget state is a different height than the pending state: " +
|
||||
env.ethPendingHeight +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
});
|
||||
|
||||
test("ConfirmTx refuses a send that exceeds the balance outright (#238)", async (env) => {
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: OVER_BALANCE_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
const want =
|
||||
"Insufficient balance. You have " +
|
||||
FUNDED_ETH_TEXT +
|
||||
" ETH but are trying to send " +
|
||||
OVER_BALANCE_AMOUNT +
|
||||
" ETH.";
|
||||
assert(
|
||||
st.errors === want,
|
||||
"wrong over-balance message: " + JSON.stringify(st.errors),
|
||||
);
|
||||
assert(st.sendDisabled, "Send is enabled for a send that exceeds balance");
|
||||
assert(
|
||||
!st.amountFeeError,
|
||||
"the amount-plus-fee message is shown for an amount that alone exceeds the balance",
|
||||
);
|
||||
});
|
||||
|
||||
test("ConfirmTx refuses to send when the fee estimate fails, with its own message (#238)", async (env) => {
|
||||
// The refusal is logged by confirmTx via log.errorf, i.e. console.error,
|
||||
// which fails a test on its own. Declaring it here consumes exactly that
|
||||
// one record — and fails this test if it never arrives.
|
||||
env.errors.expect(
|
||||
"confirmTx logging the failed gas estimate",
|
||||
/gas estimation failed/,
|
||||
);
|
||||
env.routeOpts.failGasEstimate = true;
|
||||
env.routeOpts.holdGasEstimate = true;
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
|
||||
const pending = await confirmState(env.page);
|
||||
assert(
|
||||
pending.fee === "Estimating..." && pending.sendDisabled,
|
||||
"the screen is not in the pending state before the estimate fails",
|
||||
);
|
||||
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
env.routeOpts.failGasEstimate = false;
|
||||
assert(
|
||||
st.fee === "Unable to estimate",
|
||||
"the fee line does not report the failure: " + JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
!st.reserveShown,
|
||||
"the reserve line is shown after a failed estimate",
|
||||
);
|
||||
assert(
|
||||
st.feeUnknownError,
|
||||
"the estimate-failed message is not shown after a failed estimate",
|
||||
);
|
||||
assert(
|
||||
!st.amountFeeError && !st.gasError && st.errors === "",
|
||||
"a balance message is shown for an estimate that simply failed",
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is enabled with no usable fee estimate — an unknown fee is being treated as zero",
|
||||
);
|
||||
assert(
|
||||
st.height === pending.height,
|
||||
"the view changed height when the estimate failed: " +
|
||||
pending.height +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
});
|
||||
|
||||
test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (env) => {
|
||||
env.routeOpts.holdGasEstimate = true;
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
amount: TOKEN_AMOUNT,
|
||||
});
|
||||
|
||||
const pending = await confirmState(env.page);
|
||||
env.erc20PendingHeight = pending.height;
|
||||
console.log("# confirm-tx ERC-20 view height: " + pending.height + "px");
|
||||
assert(
|
||||
pending.type === "ERC-20 token transfer (" + STUB_TOKEN.symbol + ")",
|
||||
"unexpected transaction type: " + JSON.stringify(pending.type),
|
||||
);
|
||||
assert(
|
||||
pending.balance === TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
"the ERC-20 screen shows the wrong balance: " +
|
||||
JSON.stringify(pending.balance),
|
||||
);
|
||||
assert(
|
||||
pending.fee === "Estimating..." && pending.sendDisabled,
|
||||
"the ERC-20 screen does not block sending while its estimate is pending",
|
||||
);
|
||||
|
||||
env.routeOpts.holdGasEstimate = false;
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
assert(
|
||||
st.fee === "~" + feeEth(FEE_ESTIMATE_WEI),
|
||||
"the ERC-20 fee line does not quote the estimate: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
st.reserveShown &&
|
||||
st.reserve === "up to " + feeEth(FEE_RESERVE_WEI) + " reserved",
|
||||
"the ERC-20 fee block does not quote the reserve: " +
|
||||
JSON.stringify(st.reserve),
|
||||
);
|
||||
assert(!st.sendDisabled, "Send is disabled for a funded ERC-20 transfer");
|
||||
assert(
|
||||
st.height === pending.height,
|
||||
"the ERC-20 view changed height when the estimate landed: " +
|
||||
pending.height +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
});
|
||||
|
||||
test("ConfirmTx refuses an ERC-20 send that exceeds the token balance (#238)", async (env) => {
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
amount: OVER_TOKEN_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
const want =
|
||||
"Insufficient " +
|
||||
STUB_TOKEN.symbol +
|
||||
" balance. You have " +
|
||||
TOKEN_BALANCE_TEXT +
|
||||
" " +
|
||||
STUB_TOKEN.symbol +
|
||||
" but are trying to send " +
|
||||
OVER_TOKEN_AMOUNT +
|
||||
" " +
|
||||
STUB_TOKEN.symbol +
|
||||
".";
|
||||
assert(
|
||||
st.errors === want,
|
||||
"wrong over-token-balance message: " + JSON.stringify(st.errors),
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is enabled for an ERC-20 transfer that exceeds the token balance",
|
||||
);
|
||||
assert(
|
||||
!st.gasError,
|
||||
"the ERC-20 gas message is shown for an ETH balance that covers the fee",
|
||||
);
|
||||
});
|
||||
|
||||
// The same swap, through the other balance and the other message: here the
|
||||
// token balance is ample and it is the ETH balance that must cover the fee.
|
||||
// It is set to exactly the estimate, so an estimate-reading gate lets this
|
||||
// through and the reserve-reading gate refuses it.
|
||||
test("ConfirmTx gates the ERC-20 fee on the RESERVE, with the ERC-20 message (#238)", async (env) => {
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FEE_ONLY_ETH_WEI);
|
||||
await settleOnMain(env, {
|
||||
ethWei: FEE_ONLY_ETH_WEI,
|
||||
expectToken: true,
|
||||
});
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
amount: TOKEN_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
console.log(
|
||||
"# erc-20 gate probe: ethBalance=" +
|
||||
formatEther(FEE_ONLY_ETH_WEI) +
|
||||
" estimate=" +
|
||||
feeEth(FEE_ESTIMATE_WEI) +
|
||||
" reserve=" +
|
||||
feeEth(FEE_RESERVE_WEI) +
|
||||
" sendDisabled=" +
|
||||
st.sendDisabled +
|
||||
" gasError=" +
|
||||
st.gasError,
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is ENABLED for an ERC-20 transfer whose fee RESERVE exceeds the " +
|
||||
"ETH balance — the spend gate is reading the displayed estimate (#154)",
|
||||
);
|
||||
assert(
|
||||
st.gasError,
|
||||
"the ERC-20 network-fee message is not shown when the ETH balance cannot cover the reserve",
|
||||
);
|
||||
assert(
|
||||
!st.amountFeeError,
|
||||
"the native-ETH over-budget message is shown on an ERC-20 transfer",
|
||||
);
|
||||
assert(
|
||||
st.errors === "",
|
||||
"a token-balance message is shown for a transfer the token balance covers: " +
|
||||
JSON.stringify(st.errors),
|
||||
);
|
||||
assert(
|
||||
st.height === env.erc20PendingHeight,
|
||||
"the ERC-20 fee-error state is a different height than its pending state: " +
|
||||
env.erc20PendingHeight +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
});
|
||||
|
||||
test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee problem (#238)", async (env) => {
|
||||
env.errors.expect(
|
||||
"confirmTx logging the failed ERC-20 gas estimate",
|
||||
/gas estimation failed/,
|
||||
);
|
||||
env.routeOpts.failGasEstimate = true;
|
||||
await goToConfirm(env.page, {
|
||||
token: STUB_TOKEN.address,
|
||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||
amount: TOKEN_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
|
||||
const st = await confirmState(env.page);
|
||||
env.routeOpts.failGasEstimate = false;
|
||||
assert(
|
||||
st.fee === "Unable to estimate",
|
||||
"the ERC-20 fee line does not report the failure: " +
|
||||
JSON.stringify(st.fee),
|
||||
);
|
||||
assert(
|
||||
st.feeUnknownError,
|
||||
"the estimate-failed message is not shown on the ERC-20 path",
|
||||
);
|
||||
assert(
|
||||
!st.gasError,
|
||||
"the ERC-20 network-fee message is shown for a fee that is unknown rather than unaffordable",
|
||||
);
|
||||
assert(
|
||||
st.sendDisabled,
|
||||
"Send is enabled on the ERC-20 path with no usable fee estimate",
|
||||
);
|
||||
assert(
|
||||
st.height === env.erc20PendingHeight,
|
||||
"the ERC-20 estimate-failed state is a different height than its pending state: " +
|
||||
env.erc20PendingHeight +
|
||||
"px -> " +
|
||||
st.height +
|
||||
"px",
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- runner
|
||||
|
||||
async function main() {
|
||||
@@ -638,7 +1212,15 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const routeOpts = { seedTokenTransfer: false };
|
||||
// Every fixture switch the suite can flip, declared in one place so the
|
||||
// starting state of a run is readable without hunting through tests.
|
||||
const routeOpts = {
|
||||
seedTokenTransfer: false,
|
||||
seedTokenBalance: false,
|
||||
ethBalanceWei: null,
|
||||
failGasEstimate: false,
|
||||
holdGasEstimate: false,
|
||||
};
|
||||
|
||||
let session;
|
||||
try {
|
||||
@@ -660,9 +1242,16 @@ async function main() {
|
||||
popupUrl: session.popupUrl,
|
||||
routeOpts,
|
||||
page: null,
|
||||
// The error collector, so a test that drives a failure path on
|
||||
// purpose can declare the console.error it is about to provoke.
|
||||
errors: session.errors,
|
||||
// The recovery phrase of the wallet created in test 2, so later
|
||||
// tests can assert on the real secret rather than its shape.
|
||||
phrase: null,
|
||||
// Confirmation-screen heights, measured in the pending state and
|
||||
// compared against every later state of the same screen.
|
||||
ethPendingHeight: null,
|
||||
erc20PendingHeight: null,
|
||||
};
|
||||
|
||||
// Attribution of collected errors is total. session.errors has no
|
||||
@@ -709,6 +1298,16 @@ async function main() {
|
||||
failure = "uncaught browser errors during this test";
|
||||
}
|
||||
|
||||
// A test that declared an error it meant to provoke and did not
|
||||
// provoke it asserted nothing. Failing here is what keeps expect()
|
||||
// from being usable as a mute.
|
||||
const unmatched = session.errors.unmatchedExpectations();
|
||||
if (!failure && unmatched.length > 0) {
|
||||
failure =
|
||||
"expected browser error(s) that never arrived: " +
|
||||
unmatched.join("; ");
|
||||
}
|
||||
|
||||
if (failure) {
|
||||
failed += 1;
|
||||
console.log("not ok " + n + " - " + t.name);
|
||||
|
||||
Reference in New Issue
Block a user