Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df89f20099 | ||
|
|
f86740ce69 |
@@ -45,16 +45,27 @@ but the review is broader than any of them.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-04: The Chrome end-to-end suite no longer loses a dApp prompt under
|
||||
load ([#287](https://git.eeqj.de/sneak/AutistMask/issues/287)). The suite
|
||||
drives a site-connection prompt in a tab while the toolbar popup for the same
|
||||
approval is still loading. When the tab settled it first, the toolbar popup
|
||||
closed unloaded, the extension opened its fallback window for the settled
|
||||
approval and removed it again, and the next test could take that window for
|
||||
its own prompt and lose it under its wait. The suite now takes an approval
|
||||
window only while the background still holds its approval. The blocklist
|
||||
test's Reject, whose window closes itself, is clicked as the other site Reject
|
||||
is, with the click witnessed.
|
||||
- 2026-10-05: The extension no longer opens a window for a site-connection
|
||||
prompt already answered
|
||||
([#287](https://git.eeqj.de/sneak/AutistMask/issues/287)). When the prompt was
|
||||
decided before the toolbar popup raised for it had loaded, that popup was torn
|
||||
down, `chrome.action.openPopup()` rejected, and the background opened its
|
||||
fallback window for the answered approval and then removed it. In the Chrome
|
||||
end-to-end suite the next test could take that window for its own prompt and
|
||||
lose it under its wait. `openApprovalWindow()` now opens nothing for an
|
||||
approval that is no longer pending. The blocklist test's Reject, whose window
|
||||
closes itself, is clicked as the other site Reject is, with the click
|
||||
witnessed.
|
||||
- 2026-10-04: A popup boot in the tests loads transactions without failing
|
||||
([#429](https://git.eeqj.de/sneak/AutistMask/issues/429)). The stand-in for
|
||||
`filterTransactions` in `tests/support/popupBoot.js` returned a bare list,
|
||||
while the real one returns `{ transactions, newFraudContracts }`, so every
|
||||
boot onto Home, AddressDetail or AddressToken failed inside its transaction
|
||||
loading and logged `loadHomeTxs failed` or `loadTransactions failed`; the rest
|
||||
of that code never ran. The stand-in now returns the real shape, and
|
||||
`tests/persistedFieldContract.test.js` boots onto each of the three and
|
||||
asserts neither message is logged. `make test` time did not change measurably.
|
||||
|
||||
- 2026-10-04: The native token's label follows the network
|
||||
([#372](https://git.eeqj.de/sneak/AutistMask/issues/372)). `networks.js` gives
|
||||
each network a `nativeCurrency` and nothing read it: every screen wrote `ETH`,
|
||||
|
||||
@@ -413,7 +413,7 @@ function releaseApproval(approval) {
|
||||
}
|
||||
}
|
||||
|
||||
// Open approval in a separate popup window.
|
||||
// Open approval in a separate popup window, unless it is no longer pending.
|
||||
// This is the primary mechanism for tx/sign approvals (triggered programmatically,
|
||||
// not from a user gesture) and the fallback for site-connection approvals.
|
||||
// Never rejects. Its callers raise it from inside a Promise executor and drop
|
||||
@@ -446,6 +446,10 @@ async function openApprovalWindow(id) {
|
||||
);
|
||||
}
|
||||
|
||||
// Already answered: a site-connection prompt decided before the toolbar
|
||||
// popup raised for it had loaded, whose openPopup() rejects only now.
|
||||
if (!pendingApprovals[id]) return;
|
||||
|
||||
let win = null;
|
||||
try {
|
||||
win = await windowsCreate(opts);
|
||||
|
||||
@@ -2235,6 +2235,27 @@ describe("a site connection decided as the popup closes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The prompt is decided before the toolbar popup raised for it has
|
||||
// loaded; that popup is torn down and openPopup() rejects only after.
|
||||
test("a toolbar prompt already decided opens no window when openPopup() rejects", async () => {
|
||||
const bg = loadBackground({ actionPopup: true });
|
||||
const opening = deferred();
|
||||
bg.openPopup.mockImplementation(() => opening.promise);
|
||||
const pending = bg.requestSite();
|
||||
await settle();
|
||||
|
||||
const port = bg.connectApproval(pending.id());
|
||||
port.decide(true, false);
|
||||
port.disconnect();
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: [signer.address] });
|
||||
|
||||
opening.reject(new Error("the toolbar popup closed before it loaded"));
|
||||
await settle();
|
||||
|
||||
expect(bg.created).toHaveLength(0);
|
||||
});
|
||||
|
||||
// The port carries a decision now, so it carries the sender check the
|
||||
// one-off message used to carry. A content script that guessed an
|
||||
// approval id must not be able to connect the site it is running on.
|
||||
|
||||
+17
-41
@@ -2600,42 +2600,15 @@ function dappMessages(page, type) {
|
||||
);
|
||||
}
|
||||
|
||||
// The open approval page whose approval the background still holds, or null.
|
||||
//
|
||||
// A page at an approval URL is not enough. When a site-connection prompt is
|
||||
// settled in the reserved tab (see reserveApprovalTab) before the toolbar popup
|
||||
// raised for the same approval has loaded, closing the tab tears that popup
|
||||
// down, chrome.action.openPopup() rejects, and src/background/index.js opens
|
||||
// its fallback window for the settled approval and then removes it. That window
|
||||
// can be up when the next test looks for its prompt; taken for it, it showed
|
||||
// the site view and then closed under the wait for the sign view
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/287).
|
||||
async function findPendingApprovalPage(env) {
|
||||
for (const page of env.ctx.pages()) {
|
||||
if (page.isClosed() || !page.url().includes("?approval=")) continue;
|
||||
const id = new URL(page.url()).searchParams.get("approval");
|
||||
const approval = await env.page.evaluate(
|
||||
(approvalId) =>
|
||||
new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: "AUTISTMASK_GET_APPROVAL", id: approvalId },
|
||||
resolve,
|
||||
);
|
||||
}),
|
||||
id,
|
||||
);
|
||||
if (approval) return page;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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.
|
||||
async function waitForApprovalWindow(env, timeout = 30000) {
|
||||
async function waitForApprovalWindow(ctx, timeout = 30000) {
|
||||
const deadline = Date.now() + timeout;
|
||||
for (;;) {
|
||||
const page = await findPendingApprovalPage(env);
|
||||
const page = ctx
|
||||
.pages()
|
||||
.find((p) => !p.isClosed() && p.url().includes("?approval="));
|
||||
if (page) return page;
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(
|
||||
@@ -2706,7 +2679,9 @@ async function reserveApprovalTab(env) {
|
||||
async function openSiteApprovalPopup(env, timeout = 30000) {
|
||||
const deadline = Date.now() + timeout;
|
||||
for (;;) {
|
||||
const existing = await findPendingApprovalPage(env);
|
||||
const existing = env.ctx
|
||||
.pages()
|
||||
.find((p) => !p.isClosed() && p.url().includes("?approval="));
|
||||
if (existing) return existing;
|
||||
|
||||
const url = await env.page.evaluate(
|
||||
@@ -2731,8 +2706,9 @@ async function openSiteApprovalPopup(env, timeout = 30000) {
|
||||
}
|
||||
}
|
||||
|
||||
// Retire every approval page still open, so none outlives the test that
|
||||
// raised it.
|
||||
// Retire every approval page still open. A settled approval whose page is
|
||||
// left behind would be found by the next waitForApprovalWindow() and driven
|
||||
// as if it were the next approval.
|
||||
async function closeApprovalPages(ctx) {
|
||||
for (const page of ctx.pages()) {
|
||||
if (!page.isClosed() && page.url().includes("?approval=")) {
|
||||
@@ -3170,7 +3146,7 @@ test("personal_sign signs, and the signature recovers to the address (#183)", as
|
||||
SIGN_HEX,
|
||||
env.expectedAddress,
|
||||
]);
|
||||
const popup = await waitForApprovalWindow(env);
|
||||
const popup = await waitForApprovalWindow(env.ctx);
|
||||
await visible(popup, "#view-approve-sign");
|
||||
const boundary = await watchApprovalBoundary(popup, env);
|
||||
|
||||
@@ -3247,7 +3223,7 @@ test("personal_sign rejected returns a rejection to the page (#183)", async (env
|
||||
SIGN_HEX,
|
||||
env.expectedAddress,
|
||||
]);
|
||||
const popup = await waitForApprovalWindow(env);
|
||||
const popup = await waitForApprovalWindow(env.ctx);
|
||||
await visible(popup, "#view-approve-sign");
|
||||
await clickAndClose(popup, "#btn-reject-sign");
|
||||
|
||||
@@ -3275,7 +3251,7 @@ test("a personal message is laid out in the order of its bytes (#403)", async (e
|
||||
hexlify(toUtf8Bytes(text)),
|
||||
env.expectedAddress,
|
||||
]);
|
||||
const popup = await waitForApprovalWindow(env);
|
||||
const popup = await waitForApprovalWindow(env.ctx);
|
||||
await visible(popup, "#view-approve-sign");
|
||||
|
||||
// The text on screen, marks included, and the left edge of each of its
|
||||
@@ -3321,7 +3297,7 @@ test("eth_signTypedData_v4 signs, and the signature recovers (#183)", async (env
|
||||
env.expectedAddress,
|
||||
TYPED_DATA_JSON,
|
||||
]);
|
||||
const popup = await waitForApprovalWindow(env);
|
||||
const popup = await waitForApprovalWindow(env.ctx);
|
||||
await visible(popup, "#view-approve-sign");
|
||||
const boundary = await watchApprovalBoundary(popup, env);
|
||||
|
||||
@@ -3408,7 +3384,7 @@ test("eth_signTypedData_v4 rejected returns a rejection to the page (#183)", asy
|
||||
env.expectedAddress,
|
||||
TYPED_DATA_JSON,
|
||||
]);
|
||||
const popup = await waitForApprovalWindow(env);
|
||||
const popup = await waitForApprovalWindow(env.ctx);
|
||||
await visible(popup, "#view-approve-sign");
|
||||
await clickAndClose(popup, "#btn-reject-sign");
|
||||
|
||||
@@ -3427,7 +3403,7 @@ test("eth_sendTransaction signs the approved transaction and broadcasts it (#183
|
||||
data: TX_DATA,
|
||||
};
|
||||
await startRequest(env.dapp, "tx", "eth_sendTransaction", [txParams]);
|
||||
const popup = await waitForApprovalWindow(env);
|
||||
const popup = await waitForApprovalWindow(env.ctx);
|
||||
await visible(popup, "#view-approve-tx");
|
||||
const boundary = await watchApprovalBoundary(popup, env);
|
||||
|
||||
@@ -3568,7 +3544,7 @@ test("eth_sendTransaction rejected broadcasts nothing (#183)", async (env) => {
|
||||
data: TX_DATA,
|
||||
},
|
||||
]);
|
||||
const popup = await waitForApprovalWindow(env);
|
||||
const popup = await waitForApprovalWindow(env.ctx);
|
||||
await visible(popup, "#view-approve-tx");
|
||||
await clickAndClose(popup, "#btn-reject-tx");
|
||||
|
||||
|
||||
@@ -722,6 +722,28 @@ describe("the base profile the sweep corrupts", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Home, AddressDetail and AddressToken load their transactions inside a catch
|
||||
// that only logs, so a boot that fails there still renders the view and passes
|
||||
// the tests above while none of that code runs.
|
||||
describe("the base profile loads transactions", () => {
|
||||
for (const view of ["main", "address", "address-token"]) {
|
||||
test(`on ${view} without logging a failure`, async () => {
|
||||
const consoleError = jest.spyOn(console, "error");
|
||||
try {
|
||||
await bootPopup(restoringOnto(view));
|
||||
const failures = consoleError.mock.calls
|
||||
.map((args) => args.join(" "))
|
||||
.filter((line) =>
|
||||
/loadHomeTxs failed|loadTransactions failed/.test(line),
|
||||
);
|
||||
expect(failures).toEqual([]);
|
||||
} finally {
|
||||
consoleError.mockRestore();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// A field the ROUTER itself reads — the two it gates on and the two
|
||||
// hasValidAddress() indexes with. A hostile value in one of these legitimately
|
||||
// changes which view renders, so each gets its own boot per view and is held
|
||||
|
||||
@@ -263,9 +263,12 @@ async function bootPopup(stored, options) {
|
||||
getProvider: () => ({}),
|
||||
scanForAddresses: jest.fn(async () => []),
|
||||
}));
|
||||
// filterTransactions() answers in the real one's shape: Home,
|
||||
// AddressDetail and AddressToken read both fields, and a bare list makes
|
||||
// their transaction loading throw into a catch that only logs.
|
||||
jest.doMock("../../src/shared/transactions", () => ({
|
||||
fetchRecentTransactions: jest.fn(async () => []),
|
||||
filterTransactions: () => [],
|
||||
filterTransactions: () => ({ transactions: [], newFraudContracts: [] }),
|
||||
}));
|
||||
|
||||
const storage =
|
||||
|
||||
Reference in New Issue
Block a user