The popup's markup no longer carries style attributes. The 42 in index.html and in the HTML the view helpers build are now Tailwind classes, each computing to the value it replaced, so style-src is 'self' in both manifests, pinned in tests/manifest.test.js. The address dot's 16 colours are written out as whole classes, because Tailwind builds only the classes it finds in the source. The Settings debug well is shown and hidden with the hidden class, since clearing an inline display no longer uncovers it. Two tests that found the colour dot by its inline style now find it by its class. Script that sets element.style is unaffected. Model: opus-5-5
316 lines
10 KiB
JavaScript
316 lines
10 KiB
JavaScript
// The recipient line of a contract creation
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/250).
|
|
//
|
|
// A transaction with no `to` creates a contract. The approval screen, the
|
|
// wait, success and error screens, the transaction detail view and the
|
|
// transaction history rows each say so in a sentence, where they used to show
|
|
// a blank line (an empty address, with a colour dot whose colour was
|
|
// `undefined`) or, on the approval screen, "(contract creation)". A
|
|
// transaction with a real `to` still shows that address.
|
|
//
|
|
// Driven against a minimal DOM stub in the shape
|
|
// tests/typedDataPermit.test.js uses.
|
|
|
|
jest.mock("../src/shared/log", () => ({
|
|
log: {
|
|
debugf: () => {},
|
|
infof: () => {},
|
|
warnf: () => {},
|
|
errorf: () => {},
|
|
},
|
|
// The transaction detail view fetches on-chain details after drawing; an
|
|
// answer that is not ok leaves the drawn lines as they are.
|
|
debugFetch: async () => ({ ok: false }),
|
|
setRuntimeDebug: () => {},
|
|
isDebug: () => false,
|
|
}));
|
|
|
|
// The wait screen polls for a receipt; this one never arrives.
|
|
jest.mock("../src/shared/balances", () => ({
|
|
getProvider: () => ({ getTransactionReceipt: () => new Promise(() => {}) }),
|
|
refreshBalances: () => {},
|
|
}));
|
|
|
|
// The history lists ask the explorer for their transactions and resolve ENS
|
|
// names for them; here the explorer answers with mockHistory and no name
|
|
// resolves.
|
|
let mockHistory = [];
|
|
jest.mock("../src/shared/transactions", () => ({
|
|
...jest.requireActual("../src/shared/transactions"),
|
|
fetchRecentTransactions: async () => mockHistory,
|
|
}));
|
|
jest.mock("../src/shared/ens", () => ({
|
|
...jest.requireActual("../src/shared/ens"),
|
|
resolveEnsNames: async () => new Map(),
|
|
}));
|
|
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
|
|
const { state } = require("../src/shared/state");
|
|
const approval = require("../src/popup/views/approval");
|
|
const txStatus = require("../src/popup/views/txStatus");
|
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
|
const home = require("../src/popup/views/home");
|
|
const addressDetail = require("../src/popup/views/addressDetail");
|
|
const addressToken = require("../src/popup/views/addressToken");
|
|
|
|
const SENTENCE =
|
|
"This transaction creates a new contract. It has no recipient.";
|
|
|
|
const FROM = "0x0000000000000000000000000000000000000a11";
|
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
|
const TX_HASH =
|
|
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
|
|
// Init code for a contract creation's data.
|
|
const INIT_CODE = "0x600160005500";
|
|
|
|
function makeElement(id) {
|
|
const classes = new Set();
|
|
const el = {
|
|
id,
|
|
textContent: "",
|
|
value: "",
|
|
innerHTML: "",
|
|
disabled: false,
|
|
style: {},
|
|
dataset: {},
|
|
classList: {
|
|
add: (...names) => names.forEach((n) => classes.add(n)),
|
|
remove: (...names) => names.forEach((n) => classes.delete(n)),
|
|
contains: (n) => classes.has(n),
|
|
toggle: (n, force) => {
|
|
const on = force === undefined ? !classes.has(n) : force;
|
|
if (on) classes.add(n);
|
|
else classes.delete(n);
|
|
return on;
|
|
},
|
|
},
|
|
addEventListener: () => {},
|
|
querySelectorAll: () => [],
|
|
appendChild: () => {},
|
|
};
|
|
// Views reach for .parentElement to hide whole sections.
|
|
Object.defineProperty(el, "parentElement", {
|
|
get: () => node(id + "-parent"),
|
|
});
|
|
return el;
|
|
}
|
|
|
|
function makeDocument() {
|
|
const els = new Map();
|
|
return {
|
|
getElementById(id) {
|
|
// The debug banner is created on demand by helpers.js; absent
|
|
// is the state a non-debug, non-testnet popup is in.
|
|
if (id === "debug-banner") return null;
|
|
if (!els.has(id)) els.set(id, makeElement(id));
|
|
return els.get(id);
|
|
},
|
|
createElement: () => makeElement("created"),
|
|
body: { prepend: () => {} },
|
|
};
|
|
}
|
|
|
|
function node(id) {
|
|
return globalThis.document.getElementById(id);
|
|
}
|
|
|
|
// The line a transaction with a real `to` shows: that address, and nothing
|
|
// left over from an empty one.
|
|
function expectAddressLine(html) {
|
|
expect(html).toContain(RECIPIENT);
|
|
expect(html).not.toContain(SENTENCE);
|
|
expect(html).not.toContain("undefined");
|
|
}
|
|
|
|
beforeEach(() => {
|
|
globalThis.document = makeDocument();
|
|
globalThis.window = { location: { search: "" } };
|
|
state.wallets = [];
|
|
state.trackedTokens = [];
|
|
state.viewData = {};
|
|
state.viewStack = [];
|
|
state.currentView = null;
|
|
txStatus.init({ doRefreshAndRender: () => {} });
|
|
});
|
|
|
|
afterEach(() => {
|
|
txStatus.endWait();
|
|
});
|
|
|
|
// Open the transaction approval screen the way the popup does: the background
|
|
// hands over the populated transaction and show() draws it.
|
|
async function openTxApproval(to, data) {
|
|
globalThis.chrome.runtime = {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (!reply) return;
|
|
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
|
|
reply({
|
|
type: "tx",
|
|
origin: "https://dapp.example",
|
|
isPhishingDomain: false,
|
|
approvedFrom: FROM,
|
|
approvedTx: {
|
|
type: 2,
|
|
from: FROM,
|
|
chainId: "0x1",
|
|
nonce: "0x7",
|
|
gasLimit: "0x5208",
|
|
maxPriorityFeePerGas: "0x3b9aca00",
|
|
maxFeePerGas: "0x77359400",
|
|
to,
|
|
value: "0x0",
|
|
data,
|
|
accessList: [],
|
|
},
|
|
});
|
|
},
|
|
};
|
|
approval.init({});
|
|
await approval.show(1);
|
|
}
|
|
|
|
describe("the transaction approval screen", () => {
|
|
test("a contract creation says so instead of naming a contract", async () => {
|
|
await openTxApproval(null, INIT_CODE);
|
|
expect(node("approve-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address", async () => {
|
|
await openTxApproval(RECIPIENT, "0x");
|
|
expectAddressLine(node("approve-tx-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// approval.js carries a contract creation to these screens with `to` as "".
|
|
describe("the wait, success and error screens", () => {
|
|
const creation = {
|
|
to: "",
|
|
amount: "0.0000",
|
|
token: "ETH",
|
|
tokenSymbol: null,
|
|
};
|
|
const transfer = { ...creation, to: RECIPIENT };
|
|
|
|
test("a contract creation says so on the wait screen", () => {
|
|
txStatus.showWait(creation, TX_HASH);
|
|
expect(node("wait-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address on the wait screen", () => {
|
|
txStatus.showWait(transfer, TX_HASH);
|
|
expectAddressLine(node("wait-tx-to").innerHTML);
|
|
});
|
|
|
|
test("a contract creation says so on the success and error screens", () => {
|
|
state.viewData = {
|
|
amount: "0.0000",
|
|
symbol: "ETH",
|
|
to: "",
|
|
hash: TX_HASH,
|
|
blockNumber: 1,
|
|
};
|
|
txStatus.renderSuccess();
|
|
expect(node("success-tx-to").innerHTML).toBe(SENTENCE);
|
|
|
|
txStatus.showError(creation, TX_HASH, "The transaction failed.");
|
|
expect(node("error-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address on the success and error screens", () => {
|
|
state.viewData = {
|
|
amount: "0.0050",
|
|
symbol: "ETH",
|
|
to: RECIPIENT,
|
|
hash: TX_HASH,
|
|
blockNumber: 1,
|
|
};
|
|
txStatus.renderSuccess();
|
|
expectAddressLine(node("success-tx-to").innerHTML);
|
|
|
|
txStatus.showError(transfer, TX_HASH, "The transaction failed.");
|
|
expectAddressLine(node("error-tx-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// A transaction FROM sent, as the history lists hold it. The explorer reports a
|
|
// contract creation with no `to`, which src/shared/transactions.js turns into
|
|
// `to: ""`.
|
|
function historyTx(to) {
|
|
return {
|
|
hash: TX_HASH,
|
|
from: FROM,
|
|
to,
|
|
value: "0.0000",
|
|
exactValue: "0.0",
|
|
rawAmount: "0",
|
|
rawUnit: "wei",
|
|
symbol: "ETH",
|
|
timestamp: 1790000000,
|
|
isError: false,
|
|
directionLabel: "Sent",
|
|
direction: "sent",
|
|
contractAddress: null,
|
|
};
|
|
}
|
|
|
|
// The detail view is opened with the transaction a history row holds.
|
|
describe("the transaction detail view", () => {
|
|
test("a contract creation says so", () => {
|
|
transactionDetail.show(historyTx(""));
|
|
expect(node("tx-detail-to").innerHTML).toBe(SENTENCE);
|
|
expect(node("tx-detail-type").textContent).toBe("Contract Creation");
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address", () => {
|
|
transactionDetail.show(historyTx(RECIPIENT));
|
|
expectAddressLine(node("tx-detail-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// The same rows are drawn on Home, AddressDetail and AddressToken (for ETH).
|
|
describe.each([
|
|
["Home", "home-tx-list", () => home.render({})],
|
|
["AddressDetail", "tx-list", () => addressDetail.show()],
|
|
["AddressToken", "address-token-tx-list", () => addressToken.show()],
|
|
])("the transaction history on %s", (_name, listId, open) => {
|
|
async function rowsFor(tx) {
|
|
mockHistory = [tx];
|
|
open();
|
|
// The list is drawn once the history has been fetched.
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
return node(listId).innerHTML;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
state.wallets = [
|
|
{
|
|
name: "Main",
|
|
type: "key",
|
|
addresses: [{ address: FROM, balance: "0.0000" }],
|
|
},
|
|
];
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
state.selectedToken = "ETH";
|
|
});
|
|
|
|
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
|
const html = await rowsFor(historyTx(""));
|
|
expect(html).toContain(SENTENCE);
|
|
expect(html).not.toContain("bg-[#");
|
|
expect(html).not.toContain("am-address");
|
|
expect(html).not.toContain("undefined");
|
|
});
|
|
|
|
test("a transaction with a recipient shows its colour dot and address", async () => {
|
|
const html = await rowsFor(historyTx(RECIPIENT));
|
|
expectAddressLine(html);
|
|
expect(html).toContain("bg-[#");
|
|
expect(html).toContain(`<div class="am-address">${RECIPIENT}</div>`);
|
|
});
|
|
});
|