3 Commits
Author SHA1 Message Date
clawbot acb2577fec test: the test suite exits on its own, without --forceExit (closes #585)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
Some tests left timers running when they ended, and --forceExit hid them; both test scripts drop it. Six background test files left the background's 20-second timer running while an approval was pending; each now stubs it for the whole file. Five popup test files left a flash message's two-second timer running; they now use fake timers, and a wait on real zero-delay timeouts became jest.advanceTimersByTimeAsync(0). Two ethers timers can still run for up to 250 ms after a test ends; they stop before jest checks for open handles.

Model: opus-5-5
2026-10-28 01:09:12 +01:00
clawbot a33a40a6fa build: say beside the install why yarn prints the workspaces warning (closes #595)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
Yarn 1 prints "Workspaces can only be enabled in private projects." for each dependency whose package.json has a workspaces field without "private": true. Two do: eslint 10.8.1, and babel-plugin-istanbul 7.0.1, which jest pulls in. No update removes the field, and removing the warning would mean downgrading both below what this repo and jest require. A one-line comment beside the install in script/bootstrap and in the Dockerfile now names both packages and says why the warning stays; every Dockerfile install runs script/bootstrap, so one comment there is enough.

Model: opus-5-5
2026-10-28 00:28:27 +01:00
clawbot 5192419cac docs: README matches ConfirmTx's wrong-password message and fee-bound refusal (closes #610)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
README's ConfirmTx section quoted "Wrong password." for a wrong password; the screen says "That password is incorrect. Please try again.", and the README now quotes that. The other password screens describe their error without quoting it, and DeleteWallet already quoted it right. The fee-bound refusal no longer says the RPC endpoint always fills in the gas limit and fees at signing: for an ETH amount that Max filled in, they come from the screen's own fee estimate. The transition names `assertWithinCeilings()` and lists what it checks.

Model: opus-5-5
2026-10-28 00:05:20 +01:00
15 changed files with 126 additions and 26 deletions
+1
View File
@@ -10,6 +10,7 @@ WORKDIR /app
COPY script/ script/ COPY script/ script/
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
# yarn warns about the workspaces field in eslint and babel-plugin-istanbul; no newer release drops it.
RUN script/bootstrap RUN script/bootstrap
COPY . . COPY . .
+15 -9
View File
@@ -2053,15 +2053,21 @@ view would leave a wallet one click from deletion.
fee estimate and the contract and never-sent recipient checks ask that fee estimate and the contract and never-sent recipient checks ask that
network's RPC endpoint, and the Etherscan label check asks that network's network's RPC endpoint, and the Etherscan label check asks that network's
explorer explorer
- "Sign & Send" (correct password) when the gas limit, a fee per gas or the - "Sign & Send" (correct password) when the gas limit or a fee is over its
total fee the RPC endpoint fills in at signing is over its bound in bound → nothing is signed; "The gas limit or fee the RPC endpoint quoted
`assertWithinCeilings()` (`src/shared/approvalVerify.js`) → nothing is for this transaction is more than this wallet will sign for, so it was not
signed; "The gas limit or fee the RPC endpoint quoted for this transaction sent. Please go back and try again." in its own box, no screen change. For
is more than this wallet will sign for, so it was not sent. Please go back an ETH amount that Send's "Max" filled in, the gas limit and fees are
and try again." in its own box, the same sentence for all three, no screen those of this screen's own fee estimate (see Amount above). For any other
change send, the RPC endpoint fills in the gas limit and fees at signing. Just
- "Sign & Send" (wrong password) → "Wrong password." on the password error before signing, `assertWithinCeilings()` (`src/shared/approvalVerify.js`)
line, no screen change refuses the send when any of these is over its bound:
- the gas limit
- each fee per gas the transaction sets
- the total fee: the gas limit times `maxFeePerGas`, or times `gasPrice`
when the transaction has no `maxFeePerGas`
- "Sign & Send" (wrong password) → "That password is incorrect. Please try
again." on the password error line, no screen change
- "Sign & Send" signs as the From address above, not as whichever address is - "Sign & Send" signs as the From address above, not as whichever address is
selected: the two are saved separately, so the popup can reopen onto this selected: the two are saved separately, so the popup can reopen onto this
screen with another address selected. When the selected wallet does not screen with another address selected. When the selected wallet does not
+2 -2
View File
@@ -6,8 +6,8 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"test": "jest --forceExit --maxWorkers=3", "test": "jest --maxWorkers=3",
"test:verbose": "jest --forceExit --maxWorkers=3 --verbose", "test:verbose": "jest --maxWorkers=3 --verbose",
"build": "node build.js", "build": "node build.js",
"lint": "eslint . && prettier --check .", "lint": "eslint . && prettier --check .",
"fmt": "prettier --write .", "fmt": "prettier --write .",
+1
View File
@@ -118,6 +118,7 @@ ensure_yarn() {
fi fi
} }
# yarn warns about the workspaces field in eslint and babel-plugin-istanbul; no newer release drops it.
install_js_deps() { install_js_deps() {
if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \ nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
+6 -1
View File
@@ -161,7 +161,7 @@ function click(element) {
// show() starts the fee estimate without awaiting it; this lets it settle. // show() starts the fee estimate without awaiting it; this lets it settle.
async function settle() { async function settle() {
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0)); for (let i = 0; i < 10; i++) await jest.advanceTimersByTimeAsync(0);
} }
// ------------------------------------------------------------ harness // ------------------------------------------------------------ harness
@@ -188,7 +188,11 @@ let sent;
// handler to the buttons it drew, as the browser's fresh list would have. // handler to the buttons it drew, as the browser's fresh list would have.
let plusButton; let plusButton;
// showFlash() starts a timer that clears the message one or two seconds later.
// Each test here runs on fake timers and drops them in afterEach, so that timer
// never outlives the test.
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers();
approvalDetails = null; approvalDetails = null;
sent = []; sent = [];
mockNode.broadcast = []; mockNode.broadcast = [];
@@ -239,6 +243,7 @@ beforeEach(() => {
}); });
afterEach(() => { afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
+11 -12
View File
@@ -537,7 +537,18 @@ async function settleIncludingRejections() {
await new Promise((resolve) => setImmediate(resolve)); await new Promise((resolve) => setImmediate(resolve));
} }
// While an approval is pending, the background starts a 20-second timer to
// keep itself running. The timer is stubbed and never starts, so no test in
// this file leaves it running.
let keepRunningTimer;
beforeEach(() => {
keepRunningTimer = jest
.spyOn(global, "setInterval")
.mockImplementation(() => 1);
});
afterEach(() => { afterEach(() => {
keepRunningTimer.mockRestore();
delete global.chrome; delete global.chrome;
jest.resetModules(); jest.resetModules();
}); });
@@ -1883,18 +1894,6 @@ describe("a closed approval window leaves the request pending", () => {
return bg.actionPopups[bg.actionPopups.length - 1]; return bg.actionPopups[bg.actionPopups.length - 1];
} }
// The timer that keeps the worker running while an approval is pending is
// stubbed, so no test here leaves that timer running when it ends.
let keepRunningTimer;
beforeEach(() => {
keepRunningTimer = jest
.spyOn(global, "setInterval")
.mockImplementation(() => 1);
});
afterEach(() => {
keepRunningTimer.mockRestore();
});
test("the toolbar popup opens a closed transaction approval, and approving it there sends", async () => { test("the toolbar popup opens a closed transaction approval, and approving it there sends", async () => {
const bg = loadBackground(); const bg = loadBackground();
const pending = bg.requestTx(); const pending = bg.requestTx();
+11
View File
@@ -103,7 +103,18 @@ function deferred() {
return { promise, resolve }; return { promise, resolve };
} }
// While an approval is pending, the background starts a 20-second timer to
// keep itself running. The timer is stubbed and never starts, so no test in
// this file leaves it running.
let keepRunningTimer;
beforeEach(() => {
keepRunningTimer = jest
.spyOn(global, "setInterval")
.mockImplementation(() => 1);
});
afterEach(() => { afterEach(() => {
keepRunningTimer.mockRestore();
delete global.chrome; delete global.chrome;
}); });
+11
View File
@@ -56,7 +56,18 @@ async function settle() {
for (let i = 0; i < 50; i++) await Promise.resolve(); for (let i = 0; i < 50; i++) await Promise.resolve();
} }
// While an approval is pending, the background starts a 20-second timer to
// keep itself running. The timer is stubbed and never starts, so no test in
// this file leaves it running.
let keepRunningTimer;
beforeEach(() => {
keepRunningTimer = jest
.spyOn(global, "setInterval")
.mockImplementation(() => 1);
});
afterEach(() => { afterEach(() => {
keepRunningTimer.mockRestore();
delete global.chrome; delete global.chrome;
}); });
+11
View File
@@ -64,7 +64,18 @@ async function settle() {
for (let i = 0; i < 50; i++) await Promise.resolve(); for (let i = 0; i < 50; i++) await Promise.resolve();
} }
// While an approval is pending, the background starts a 20-second timer to
// keep itself running. The timer is stubbed and never starts, so no test in
// this file leaves it running.
let keepRunningTimer;
beforeEach(() => {
keepRunningTimer = jest
.spyOn(global, "setInterval")
.mockImplementation(() => 1);
});
afterEach(() => { afterEach(() => {
keepRunningTimer.mockRestore();
delete global.chrome; delete global.chrome;
}); });
+11
View File
@@ -76,7 +76,18 @@ async function settle() {
for (let i = 0; i < 60; i++) await Promise.resolve(); for (let i = 0; i < 60; i++) await Promise.resolve();
} }
// While an approval is pending, the background starts a 20-second timer to
// keep itself running. The timer is stubbed and never starts, so no test in
// this file leaves it running.
let keepRunningTimer;
beforeEach(() => {
keepRunningTimer = jest
.spyOn(global, "setInterval")
.mockImplementation(() => 1);
});
afterEach(() => { afterEach(() => {
keepRunningTimer.mockRestore();
delete global.chrome; delete global.chrome;
}); });
+11
View File
@@ -57,7 +57,18 @@ async function settle() {
for (let i = 0; i < 50; i++) await Promise.resolve(); for (let i = 0; i < 50; i++) await Promise.resolve();
} }
// While an approval is pending, the background starts a 20-second timer to
// keep itself running. The timer is stubbed and never starts, so no test in
// this file leaves it running.
let keepRunningTimer;
beforeEach(() => {
keepRunningTimer = jest
.spyOn(global, "setInterval")
.mockImplementation(() => 1);
});
afterEach(() => { afterEach(() => {
keepRunningTimer.mockRestore();
delete global.chrome; delete global.chrome;
jest.resetModules(); jest.resetModules();
}); });
+11
View File
@@ -102,6 +102,17 @@ function makeDocument() {
// ------------------------------------------------------------ harness // ------------------------------------------------------------ harness
// showFlash() starts a timer that clears the message two seconds later. Each
// test here runs on fake timers and drops them in afterEach, so that timer
// never outlives the test.
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
function wallet(name, secret, addresses) { function wallet(name, secret, addresses) {
return { return {
type: "hd", type: "hd",
+9 -1
View File
@@ -131,10 +131,14 @@ async function review(amount, token) {
async function confirm(amount, token) { async function confirm(amount, token) {
const txInfo = await review("0.5", token); const txInfo = await review("0.5", token);
confirmTx.show({ ...txInfo, amount }); confirmTx.show({ ...txInfo, amount });
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0)); for (let i = 0; i < 10; i++) await jest.advanceTimersByTimeAsync(0);
} }
// showFlash() starts a timer that clears the message two seconds later. Each
// test here runs on fake timers and drops them in afterEach, so that timer
// never outlives the test.
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers();
elements.clear(); elements.clear();
state.wallets = [ state.wallets = [
{ {
@@ -167,6 +171,10 @@ beforeEach(() => {
state.currentView = null; state.currentView = null;
}); });
afterEach(() => {
jest.useRealTimers();
});
describe.each([ describe.each([
["1e-3", "ETH"], ["1e-3", "ETH"],
["1e-3", "TOK"], ["1e-3", "TOK"],
+9 -1
View File
@@ -277,14 +277,18 @@ async function review() {
// show() starts the fee estimate without awaiting it; this lets it settle. // show() starts the fee estimate without awaiting it; this lets it settle.
async function settle() { async function settle() {
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0)); for (let i = 0; i < 10; i++) await jest.advanceTimersByTimeAsync(0);
} }
function canSend() { function canSend() {
return !el("btn-confirm-send").disabled; return !el("btn-confirm-send").disabled;
} }
// showFlash() starts a timer that clears the message two seconds later. Each
// test here runs on fake timers and drops them in afterEach, so that timer
// never outlives the test.
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers();
elements.clear(); elements.clear();
log.errorf.mockClear(); log.errorf.mockClear();
pageClosed = new AbortController(); pageClosed = new AbortController();
@@ -297,6 +301,10 @@ beforeEach(() => {
mockNode.broadcast = []; mockNode.broadcast = [];
}); });
afterEach(() => {
jest.useRealTimers();
});
describe("Max on an ETH send", () => { describe("Max on an ETH send", () => {
test("fills in the exact balance minus the fee reserve", async () => { test("fills in the exact balance minus the fee reserve", async () => {
await refreshWith(BALANCE_WEI); await refreshWith(BALANCE_WEI);
+6
View File
@@ -357,6 +357,11 @@ describe("every way to a signature or the private key refuses a defective wallet
}); });
beforeEach(() => { beforeEach(() => {
// showFlash() starts a timer that clears the message two seconds
// later. Each test here runs on fake timers and drops them in
// afterEach, so that timer never outlives the test.
jest.useFakeTimers();
const broken = brokenXprvWallet(); const broken = brokenXprvWallet();
// A balance, so that no Send button's zero-balance refusal can stand // A balance, so that no Send button's zero-balance refusal can stand
// in for the defect check. // in for the defect check.
@@ -398,6 +403,7 @@ describe("every way to a signature or the private key refuses a defective wallet
}); });
afterEach(() => { afterEach(() => {
jest.useRealTimers();
state.wallets = []; state.wallets = [];
state.activeAddress = null; state.activeAddress = null;
state.selectedWallet = null; state.selectedWallet = null;