fix: open an approval window while another one has focus (closes #290)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s

The background centred each approval window on the last focused window,
which could be an earlier approval window still open; headless Chrome
reports one as 1280x720, the browser refused the resulting position, and
the request failed with no window. It now centres only on a browser
window, and when the browser refuses a position it asks again without one.

In the Chrome suite a test could raise its prompt while the previous
test's window was still closing. After a passed test the runner now gives
approval windows five seconds to close and fails the test if one is still
open; after a failed test it closes them.

Model: opus-5-5
This commit was merged in pull request #453.
This commit is contained in:
2026-10-05 08:26:06 +02:00
parent a207ac70bd
commit 6613a1f6bc
5 changed files with 220 additions and 18 deletions
+82 -1
View File
@@ -2676,7 +2676,9 @@ function dappMessages(page, type) {
// The approval window the background opened. Approvals are raised from an
// RPC call rather than from a user gesture, so the extension opens a real
// popup window for them; it is an ordinary page in this context.
// popup window for them; it is an ordinary page in this context. It is the
// first one found: no test starts with an approval window open (see main()),
// so within a test it can only be this test's.
async function waitForApprovalWindow(ctx, timeout = 30000) {
const deadline = Date.now() + timeout;
for (;;) {
@@ -3633,6 +3635,55 @@ test("eth_sendTransaction rejected broadcasts nothing (#183)", async (env) => {
);
});
// The extension used to centre every approval window on the last focused
// window. Centred on another approval window, the new one landed where the
// browser refused to create it, and the request failed with no window at all
// (https://git.eeqj.de/sneak/AutistMask/issues/290).
test("a prompt raised while another approval window has focus opens its own (#290)", async (env) => {
await startRequest(env.dapp, "focus-sign", "personal_sign", [
SIGN_HEX,
env.expectedAddress,
]);
const signWindow = await waitForApprovalWindow(env.ctx);
await visible(signWindow, "#view-approve-sign");
await signWindow.bringToFront();
const focused = await env.page.evaluate(
() =>
new Promise((resolve) => {
chrome.windows.getLastFocused((w) => resolve(w.type));
}),
);
assert(
focused === "popup",
"the sign window does not have focus, so this proves nothing: the " +
"last focused window is a " +
focused,
);
const opened = env.ctx.waitForEvent("page");
await startRequest(env.dapp, "focus-tx", "eth_sendTransaction", [
{
from: env.expectedAddress,
to: STUB_COUNTERPARTY,
value: toQuantity(TX_VALUE_WEI),
data: TX_DATA,
},
]);
const txWindow = await opened.catch(async () => {
const outcome = await settleRequest(env.dapp, "focus-tx", 1000);
throw new Error(
"the extension opened no window for the transaction: " +
JSON.stringify(outcome),
);
});
await visible(txWindow, "#view-approve-tx");
// Closing a window is refusing its prompt, so both answer 4001.
await closeApprovalPages(env.ctx);
await assertUserRejection(env.dapp, "focus-tx", "the transaction prompt");
await assertUserRejection(env.dapp, "focus-sign", "the sign prompt");
});
// The closing pass over both boundaries at once. Every message the section
// put on either channel is re-read here and required to be free of the
// password — and required to be there at all, method by method, so the
@@ -4002,6 +4053,36 @@ async function main() {
failure = e.message;
}
// No test starts with an approval window open. One that closes itself,
// after a signature or on Reject, can still be closing when the next
// test raises its prompt, and waitForApprovalWindow() would take it
// for the new one. After a test that passed, its windows get five
// seconds to close, and one still open then fails the test: it should
// have closed itself, and closing it here unreported would hide that.
// After a test that already failed, they are closed unreported, so
// that a prompt it left unanswered does not refuse the next one from
// its site.
if (!failure) {
const deadline = Date.now() + 5000;
let open;
for (;;) {
open = env.ctx
.pages()
.filter(
(p) => !p.isClosed() && p.url().includes("?approval="),
);
if (open.length === 0 || Date.now() > deadline) break;
await sleep(50);
}
if (open.length > 0) {
failure =
"an approval window was still open 5000ms after the " +
"test: " +
open.map((p) => p.url()).join(", ");
}
}
await closeApprovalPages(env.ctx);
// Any uncaught page error, console.error or unstubbed request
// fails the test that provoked it, whether or not its assertions
// passed. This is the mechanism that caught #150.