The browser can close the approval window when the user clicks away from it, for example to copy the password from a password manager. Closing it used to reject the request with 4001. Now the request stays pending and the site gets no answer. Until it is answered, the toolbar popup opens the oldest such approval. The toolbar popup skips an approval while its transaction is being sent or its signature checked. A worker that starts holds no approvals, so it points the toolbar popup back at the wallet. Reject now closes its window only after the background has the rejection. A rejection the background refuses leaves the window open with the background's message. Site-connection and network-switch windows still reject the request when closed. Model: opus-5-5
311 lines
11 KiB
JavaScript
311 lines
11 KiB
JavaScript
// The approval window shows no screen until the background has described the
|
|
// approval it answers (https://git.eeqj.de/sneak/AutistMask/issues/408). The
|
|
// connection prompt's "Allow" and the network switch prompt's "Switch" answer
|
|
// on the same port, so a window that showed the connection prompt while it
|
|
// waited could approve a network switch.
|
|
//
|
|
// Booted through the real popup entry point, which is where the connection
|
|
// prompt used to be put up before the background had answered.
|
|
|
|
const {
|
|
bootPopup,
|
|
cleanupPopup,
|
|
settle,
|
|
unversionedValidProfile,
|
|
ADDRESS,
|
|
} = require("./support/popupBoot");
|
|
|
|
afterEach(cleanupPopup);
|
|
|
|
test("the approval window shows no screen until the background describes the approval", async () => {
|
|
// The background's answer, held until the test gives it.
|
|
let answer = null;
|
|
const env = await bootPopup(unversionedValidProfile(), {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") answer = reply;
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(answer).not.toBeNull();
|
|
// No screen at all, the connection prompt included.
|
|
expect(env.visibleViews()).toEqual([]);
|
|
|
|
answer({
|
|
type: "network",
|
|
origin: "https://dapp.example",
|
|
currentNetworkId: "mainnet",
|
|
requestedNetworkId: "sepolia",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
|
|
expect(env.visibleViews()).toEqual(["approve-network"]);
|
|
expect(env.pageErrors).toEqual([]);
|
|
});
|
|
|
|
// The background opens the approval window as wide as the page, but the
|
|
// browser counts the window's frame in that width, and a screen taller than
|
|
// the window takes a vertical scrollbar out of it too
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/518). The browser suites run
|
|
// without frames, and only the Firefox suite's sign prompt at a zoom other
|
|
// than 100%, so the widening is checked here too.
|
|
//
|
|
// A window 396 screen pixels wide whose page is drawn at `drawn` screen pixels
|
|
// to the page pixel. Widening it gives the page its new width, less the 15
|
|
// screen pixels of the scrollbar the approval screen has, and then calls the
|
|
// page's resize listeners, each of which asked to be called once.
|
|
function windowsStub(type, drawn) {
|
|
const updates = [];
|
|
return {
|
|
updates,
|
|
getCurrent: (done) => done({ id: 7, type, width: 396 }),
|
|
update: (id, info, done) => {
|
|
updates.push({ id, info });
|
|
done({});
|
|
globalThis.document.documentElement.clientWidth = Math.floor(
|
|
(info.width - 15) / drawn,
|
|
);
|
|
const resized = globalThis.window.listeners.resize || [];
|
|
globalThis.window.listeners.resize = [];
|
|
for (const fn of resized) fn();
|
|
},
|
|
};
|
|
}
|
|
|
|
// The approval page's own tab is at `zoom`. Asked without a tab id, Firefox
|
|
// answers for the window focused last, here a site's tab at 100%.
|
|
function tabsStub(zoom) {
|
|
return {
|
|
getCurrent: (done) => done({ id: 11 }),
|
|
getZoom: (...args) => {
|
|
const done = args.pop();
|
|
done(args[0] === 11 ? zoom : 1);
|
|
},
|
|
};
|
|
}
|
|
|
|
// An approval window at `zoom`, `clientWidth` page pixels wide before its
|
|
// screen is up, whose background holds its answer until the test gives it.
|
|
async function bootZoomedApprovalWindow(windows, zoom, clientWidth) {
|
|
let answer = null;
|
|
const env = await bootPopup(unversionedValidProfile(), {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") answer = reply;
|
|
},
|
|
},
|
|
windows,
|
|
tabs: tabsStub(zoom),
|
|
documentElement: { scrollWidth: 396, clientWidth },
|
|
});
|
|
return { env, answer };
|
|
}
|
|
|
|
test("at 150% zoom the approval window widens by what its screen lacks, scrollbar included", async () => {
|
|
const windows = windowsStub("popup", 1.5);
|
|
// At 150% a window 396 screen pixels wide shows 264 page pixels.
|
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.5, 264);
|
|
|
|
// Not before there is a screen to fit.
|
|
expect(windows.updates).toEqual([]);
|
|
|
|
// The screen is taller than the window, so once it is up a vertical
|
|
// scrollbar takes 15 screen pixels, 10 page pixels at 150%.
|
|
env.document.documentElement.clientWidth = 254;
|
|
answer({
|
|
type: "network",
|
|
origin: "https://dapp.example",
|
|
currentNetworkId: "mainnet",
|
|
requestedNetworkId: "sepolia",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
|
|
// 396 - 254 = 142 page pixels short, which at 150% is 213 screen pixels.
|
|
// At the 100% of the site's tab it would be 142, and the screen would
|
|
// still not fit.
|
|
expect(env.visibleViews()).toEqual(["approve-network"]);
|
|
expect(windows.updates).toEqual([{ id: 7, info: { width: 609 } }]);
|
|
expect(env.document.documentElement.clientWidth).toBe(396);
|
|
expect(env.pageErrors).toEqual([]);
|
|
});
|
|
|
|
test("at 170% the approval window is widened again while its screen still falls short", async () => {
|
|
// Firefox reports 170% as 1.7 but draws a page pixel about 1.714 screen
|
|
// pixels wide, so a window 396 wide shows 231 page pixels.
|
|
const windows = windowsStub("popup", 1.714);
|
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.7, 231);
|
|
|
|
// Less the scrollbar once the screen is up.
|
|
env.document.documentElement.clientWidth = 222;
|
|
answer({
|
|
type: "network",
|
|
origin: "https://dapp.example",
|
|
currentNetworkId: "mainnet",
|
|
requestedNetworkId: "sepolia",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
|
|
// 174 page pixels short, so 296 screen pixels wider at 1.7. That shows 394
|
|
// page pixels, still 2 short, so 4 screen pixels wider again.
|
|
expect(windows.updates).toEqual([
|
|
{ id: 7, info: { width: 692 } },
|
|
{ id: 7, info: { width: 696 } },
|
|
]);
|
|
const page = env.document.documentElement;
|
|
expect(page.scrollWidth).toBeLessThanOrEqual(page.clientWidth);
|
|
expect(env.pageErrors).toEqual([]);
|
|
});
|
|
|
|
test("a connection prompt in the toolbar popup leaves the browser window alone", async () => {
|
|
const windows = windowsStub("normal", 1.5);
|
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.5, 264);
|
|
|
|
answer({ origin: "https://dapp.example", isPhishingDomain: false });
|
|
await settle();
|
|
|
|
expect(env.visibleViews()).toEqual(["approve-site"]);
|
|
expect(windows.updates).toEqual([]);
|
|
expect(env.pageErrors).toEqual([]);
|
|
});
|
|
|
|
// Each row holds a transaction or signature approval as the background
|
|
// describes it, plus the element ids of the screen that shows it. `refusal` is
|
|
// the message the background refuses a Reject with.
|
|
const TX_AND_SIGN_APPROVALS = [
|
|
{
|
|
kind: "transaction",
|
|
view: "approve-tx",
|
|
reject: "btn-reject-tx",
|
|
error: "approve-tx-error",
|
|
type: "AUTISTMASK_TX_RESPONSE",
|
|
refusal: "This transaction is already being sent.",
|
|
approval: {
|
|
type: "tx",
|
|
approvedFrom: ADDRESS,
|
|
approvedTx: {
|
|
chainId: 1,
|
|
to: ADDRESS,
|
|
value: "0",
|
|
data: "0x",
|
|
gasLimit: "21000",
|
|
maxFeePerGas: "1000000000",
|
|
maxPriorityFeePerGas: "1000000000",
|
|
nonce: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
kind: "signature",
|
|
view: "approve-sign",
|
|
reject: "btn-reject-sign",
|
|
error: "approve-sign-error",
|
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
|
refusal: "This request is already being signed.",
|
|
approval: {
|
|
type: "sign",
|
|
approvedFrom: ADDRESS,
|
|
signParams: {
|
|
method: "personal_sign",
|
|
message: "0x48656c6c6f",
|
|
from: ADDRESS,
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
// A closed transaction or signature window leaves its request pending
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/673). So Reject closes the
|
|
// window only once the background has the rejection. A message sent as the
|
|
// window closes can be lost, and the request would stay pending although the
|
|
// user rejected it.
|
|
test.each(TX_AND_SIGN_APPROVALS)(
|
|
"Reject on a $kind approval closes the window only once the background has answered",
|
|
async ({ view, reject, type, approval }) => {
|
|
let describeApproval = null;
|
|
let answerReject = null;
|
|
const sent = [];
|
|
const env = await bootPopup(unversionedValidProfile(), {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
|
describeApproval = reply;
|
|
}
|
|
if (msg.type === type) {
|
|
sent.push(msg);
|
|
answerReject = reply;
|
|
}
|
|
},
|
|
},
|
|
});
|
|
describeApproval({
|
|
...approval,
|
|
origin: "https://dapp.example",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
expect(env.visibleViews()).toEqual([view]);
|
|
const close = jest.fn();
|
|
globalThis.window.close = close;
|
|
|
|
const clicked = env.click(reject);
|
|
await settle();
|
|
expect(sent).toEqual([{ type, id: "approval-1", approved: false }]);
|
|
expect(close).not.toHaveBeenCalled();
|
|
|
|
answerReject(null);
|
|
await clicked;
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
expect(env.pageErrors).toEqual([]);
|
|
},
|
|
);
|
|
|
|
// After Confirm or Sign, the background refuses a Reject while the transaction
|
|
// is being sent or the signature checked. The request is not rejected then, so
|
|
// the window stays open and shows the background's message.
|
|
test.each(TX_AND_SIGN_APPROVALS)(
|
|
"Reject on a $kind approval that the background refuses shows the background's message and keeps the window open",
|
|
async ({ view, reject, error, type, refusal, approval }) => {
|
|
let describeApproval = null;
|
|
let answerReject = null;
|
|
const env = await bootPopup(unversionedValidProfile(), {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
|
describeApproval = reply;
|
|
}
|
|
if (msg.type === type) answerReject = reply;
|
|
},
|
|
},
|
|
});
|
|
describeApproval({
|
|
...approval,
|
|
origin: "https://dapp.example",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
const close = jest.fn();
|
|
globalThis.window.close = close;
|
|
|
|
const clicked = env.click(reject);
|
|
await settle();
|
|
answerReject({ error: refusal, retryable: false });
|
|
await clicked;
|
|
expect(close).not.toHaveBeenCalled();
|
|
expect(env.visibleViews()).toEqual([view]);
|
|
expect(env.text(error)).toBe(refusal);
|
|
expect(env.pageErrors).toEqual([]);
|
|
},
|
|
);
|