A network switch left the previous network's rows or failure message in Home's list until the new network's answer landed. Home now replaces them with "Loading..." when it first draws on a new network. A redraw on the same network keeps its rows. Home, AddressDetail and AddressToken compared the page's network before reading storage. A switch made in Settings could change the page during that read, and the answer was still drawn. They now compare after the read, for an answer and for a failure. Model: opus-5-5
622 lines
24 KiB
JavaScript
622 lines
24 KiB
JavaScript
// An open popup follows a network switch the background saves once the user
|
|
// approves a site's request: it takes the new network from the stored record
|
|
// and draws the screen it is on for it, as a switch in Settings does
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/650). Driven through the real
|
|
// popup over a stored profile on Ethereum Mainnet. The background's write is
|
|
// the storage stub's set, which the stub reports to the page as the browser
|
|
// does.
|
|
|
|
const {
|
|
bootPopup,
|
|
cleanupPopup,
|
|
unversionedValidProfile,
|
|
TOKEN_ADDRESS,
|
|
} = require("./support/popupBoot");
|
|
const { makeStorageStub } = require("./support/storageStub");
|
|
const {
|
|
applyChainSwitchFields,
|
|
copyChainSwitchFields,
|
|
} = require("../src/shared/chainSwitchFields");
|
|
const { NETWORKS } = require("../src/shared/networks");
|
|
const { STATE_SCHEMA_VERSION } = require("../src/shared/stateSchema");
|
|
|
|
afterEach(() => {
|
|
cleanupPopup();
|
|
});
|
|
|
|
const SEPOLIA_TOKEN = {
|
|
address: TOKEN_ADDRESS,
|
|
symbol: "TST",
|
|
decimals: 18,
|
|
name: "Test Token",
|
|
};
|
|
const MAINNET_TOKEN = {
|
|
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
|
symbol: "DAI",
|
|
decimals: 18,
|
|
name: "Dai Stablecoin",
|
|
};
|
|
|
|
// A profile on Ethereum Mainnet that reopens the popup onto AddressDetail,
|
|
// with DAI tracked there and TST tracked on Sepolia.
|
|
function profileOnAddress() {
|
|
return unversionedValidProfile({
|
|
currentView: "address",
|
|
viewStack: ["main"],
|
|
selectedWallet: 0,
|
|
selectedAddress: 0,
|
|
trackedTokens: [MAINNET_TOKEN],
|
|
networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] },
|
|
});
|
|
}
|
|
|
|
// The popup reopened onto it.
|
|
function bootOnAddress() {
|
|
return bootPopup(profileOnAddress());
|
|
}
|
|
|
|
// The background saves a switch to `networkId`, as its updateState() does
|
|
// once the user approves a site's request.
|
|
async function backgroundSwitch(page, networkId) {
|
|
const record = page.storage.read("autistmask");
|
|
applyChainSwitchFields(record, networkId);
|
|
await page.storage.local.set({ autistmask: record });
|
|
await page.settle();
|
|
await page.settle();
|
|
}
|
|
|
|
test("the popup open on AddressDetail shows the new network and its balances", async () => {
|
|
const page = await bootOnAddress();
|
|
expect(page.pageErrors).toEqual([]);
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
expect(page.node("address-balances").innerHTML).toContain("DAI");
|
|
expect(page.node("debug-banner")).toBeNull();
|
|
|
|
await backgroundSwitch(page, "sepolia");
|
|
|
|
const { state } = require("../src/shared/state");
|
|
expect(state.networkId).toBe("sepolia");
|
|
expect(state.rpcUrl).toBe(NETWORKS.sepolia.defaultRpcUrl);
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
const balances = page.node("address-balances").innerHTML;
|
|
expect(balances).toContain("TST");
|
|
expect(balances).toContain("SepoliaETH");
|
|
expect(balances).not.toContain("DAI");
|
|
expect(page.text("debug-banner")).toBe("[TESTNET]");
|
|
|
|
// Its next refresh runs on Sepolia, and what it fetched is stored.
|
|
await page.tick();
|
|
const { refreshBalances } = require("../src/shared/balances");
|
|
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
|
const stored = page.storage.read("autistmask");
|
|
expect(stored.networkId).toBe("sepolia");
|
|
expect(stored.lastBalanceRefresh).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("the popup open on Settings shows the new network, and Back draws the screen beneath for it", async () => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
expect(page.visibleViews()).toEqual(["settings"]);
|
|
|
|
await backgroundSwitch(page, "sepolia");
|
|
|
|
expect(page.visibleViews()).toEqual(["settings"]);
|
|
expect(page.value("settings-network")).toBe("sepolia");
|
|
expect(page.value("settings-rpc")).toBe(NETWORKS.sepolia.defaultRpcUrl);
|
|
expect(page.node("settings-tracked-tokens").innerHTML).toContain("TST");
|
|
expect(page.text("debug-banner")).toBe("[TESTNET]");
|
|
|
|
await page.click("btn-settings-back");
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
expect(page.node("address-balances").innerHTML).not.toContain("DAI");
|
|
});
|
|
|
|
// Neither can be drawn again for the new network: Send cannot be rendered from
|
|
// saved state, and DAI was picked on Ethereum Mainnet, where alone it is
|
|
// tracked. A reopened popup goes to Home in their place.
|
|
test.each([
|
|
["Send", (page) => page.click("btn-send")],
|
|
[
|
|
"AddressToken for DAI",
|
|
async (page) => {
|
|
const { state } = require("../src/shared/state");
|
|
state.selectedToken = MAINNET_TOKEN.address;
|
|
require("../src/popup/views/helpers").pushCurrentView();
|
|
require("../src/popup/views/addressToken").show();
|
|
await page.settle();
|
|
},
|
|
],
|
|
])("the popup open on %s goes to Home", async (_view, open) => {
|
|
const page = await bootOnAddress();
|
|
await open(page);
|
|
expect(page.visibleViews()).not.toEqual(["address"]);
|
|
|
|
await backgroundSwitch(page, "sepolia");
|
|
|
|
expect(page.visibleViews()).toEqual(["main"]);
|
|
expect(page.text("total-value")).toBe("0.0000 SepoliaETH");
|
|
});
|
|
|
|
// A request on Ethereum Mainnet fails, so Home's list holds the failure message.
|
|
// The list shows it neither before nor after Sepolia's answer lands.
|
|
test.each([
|
|
{
|
|
how: "saved by the background",
|
|
switchToSepolia: (page) => backgroundSwitch(page, "sepolia"),
|
|
},
|
|
{
|
|
how: "made in Settings",
|
|
switchToSepolia: async (page) => {
|
|
await page.click("btn-settings");
|
|
const select = page.node("settings-network");
|
|
select.value = "sepolia";
|
|
for (const fn of select.listeners.change) await fn();
|
|
await page.settle();
|
|
await page.click("btn-settings-back");
|
|
},
|
|
},
|
|
])(
|
|
"Home's list drops the previous network's message after a switch $how",
|
|
async ({ switchToSepolia }) => {
|
|
const page = await bootPopup(
|
|
unversionedValidProfile({ useBlockscout: true }),
|
|
);
|
|
const {
|
|
fetchRecentTransactions,
|
|
} = require("../src/shared/transactions");
|
|
fetchRecentTransactions.mockRejectedValueOnce(
|
|
new Error("Blockscout is down"),
|
|
);
|
|
await page.tick();
|
|
expect(page.node("home-tx-list").innerHTML).toContain(
|
|
"Failed to load transactions.",
|
|
);
|
|
|
|
let land;
|
|
fetchRecentTransactions.mockImplementationOnce(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
land = () => resolve([]);
|
|
}),
|
|
);
|
|
await switchToSepolia(page);
|
|
expect(page.visibleViews()).toEqual(["main"]);
|
|
expect(page.node("home-tx-list").innerHTML).toContain("Loading...");
|
|
|
|
land();
|
|
await page.settle();
|
|
expect(page.node("home-tx-list").innerHTML).toContain(
|
|
"No transactions found.",
|
|
);
|
|
},
|
|
);
|
|
|
|
// The report of the switch's own save names the network the page already has.
|
|
test("a switch in Settings is not taken a second time when its save is reported", async () => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
const { get, set } = page.storage.local;
|
|
const reads = get.mock.calls.length;
|
|
const writes = set.mock.calls.length;
|
|
|
|
const select = page.node("settings-network");
|
|
select.value = "sepolia";
|
|
for (const fn of select.listeners.change) await fn();
|
|
await page.settle();
|
|
await page.settle();
|
|
|
|
expect(get).toHaveBeenCalledTimes(reads + 1);
|
|
expect(set).toHaveBeenCalledTimes(writes + 1);
|
|
expect(page.text("flash-msg")).toBe("Switched to Sepolia Testnet.");
|
|
});
|
|
|
|
// The background saves the switch once the popup has read the stored record
|
|
// and before it listens for changes to it, so the browser tells the page
|
|
// nothing of it. Nor is any later write reported here: Chrome reports none
|
|
// that leaves the record as it was, such as the popup's own save at start-up.
|
|
test("a switch saved while the popup opens, before it listens for one, is followed", async () => {
|
|
const storage = makeStorageStub({ autistmask: profileOnAddress() });
|
|
storage.onChanged = { addListener: () => {} };
|
|
const read = storage.local.get.getMockImplementation();
|
|
storage.local.get.mockImplementationOnce(async (key) => {
|
|
const loaded = await read(key);
|
|
const record = storage.read("autistmask");
|
|
applyChainSwitchFields(record, "sepolia");
|
|
storage.write("autistmask", record);
|
|
return loaded;
|
|
});
|
|
const page = await bootPopup(undefined, { storage });
|
|
await page.settle();
|
|
|
|
const { state } = require("../src/shared/state");
|
|
expect(page.pageErrors).toEqual([]);
|
|
expect(state.networkId).toBe("sepolia");
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
expect(page.node("address-balances").innerHTML).toContain("SepoliaETH");
|
|
expect(page.text("debug-banner")).toBe("[TESTNET]");
|
|
});
|
|
|
|
// While the popup's follow of the background's switch to Sepolia waits for the
|
|
// lock, the user picks Sepolia in Settings and then Ethereum Mainnet again. The
|
|
// last pick is the network stored, and the last one sites are told of.
|
|
test("a switch in Settings made while a follow waits for the lock is not overwritten by it", async () => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
|
|
// Another write holds the lock, so every save of the page waits for it.
|
|
let releaseLock;
|
|
const lockHeld = navigator.locks.request(
|
|
"autistmask",
|
|
() =>
|
|
new Promise((resolve) => {
|
|
releaseLock = resolve;
|
|
}),
|
|
);
|
|
await backgroundSwitch(page, "sepolia");
|
|
const select = page.node("settings-network");
|
|
const picks = [];
|
|
for (const networkId of ["sepolia", "mainnet"]) {
|
|
select.value = networkId;
|
|
for (const fn of select.listeners.change) picks.push(fn());
|
|
}
|
|
releaseLock();
|
|
await lockHeld;
|
|
await Promise.all(picks);
|
|
await page.settle();
|
|
|
|
const { state } = require("../src/shared/state");
|
|
expect(state.networkId).toBe("mainnet");
|
|
expect(page.storage.read("autistmask").networkId).toBe("mainnet");
|
|
expect(page.value("settings-network")).toBe("mainnet");
|
|
const told = globalThis.chrome.runtime.sendMessage.mock.calls
|
|
.map(([message]) => message)
|
|
.filter((message) => message.type === "AUTISTMASK_NETWORK_CHANGED");
|
|
expect(told.at(-1).chainId).toBe(NETWORKS.mainnet.chainId);
|
|
});
|
|
|
|
// The popup follows the background's switch to Sepolia, but its own write in
|
|
// that save fails. The user then picks Ethereum Mainnet in Settings, the
|
|
// network the popup was on before: that switch is stored, and only then
|
|
// flashed and sent to sites.
|
|
test("a switch back in Settings after a follow whose write failed is stored", async () => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
const { set } = page.storage.local;
|
|
const write = set.getMockImplementation();
|
|
// The background's write lands; the next, the follow's, fails.
|
|
set.mockImplementationOnce(write).mockRejectedValueOnce(
|
|
new Error("quota exceeded"),
|
|
);
|
|
await backgroundSwitch(page, "sepolia");
|
|
const { state } = require("../src/shared/state");
|
|
expect(state.networkId).toBe("sepolia");
|
|
|
|
const select = page.node("settings-network");
|
|
select.value = "mainnet";
|
|
for (const fn of select.listeners.change) await fn();
|
|
await page.settle();
|
|
|
|
const stored = page.storage.read("autistmask");
|
|
expect(stored.networkId).toBe("mainnet");
|
|
expect(stored.rpcUrl).toBe(NETWORKS.mainnet.defaultRpcUrl);
|
|
expect(stored.trackedTokens).toEqual([
|
|
{ ...MAINNET_TOKEN, balanceNotFetched: true },
|
|
]);
|
|
expect(page.text("flash-msg")).toBe("Switched to Ethereum Mainnet.");
|
|
const told = globalThis.chrome.runtime.sendMessage.mock.calls
|
|
.map(([message]) => message)
|
|
.filter((message) => message.type === "AUTISTMASK_NETWORK_CHANGED");
|
|
expect(told.at(-1).chainId).toBe(NETWORKS.mainnet.chainId);
|
|
});
|
|
|
|
// The user saves an endpoint URL in Settings on Ethereum Mainnet. Before the
|
|
// URL is stored, the background saves a switch to Sepolia. Another write holds
|
|
// the lock, so the popup's follow of that switch and the URL's save both wait
|
|
// for the lock. Either may take it first. The URL is stored for neither
|
|
// network. The flash message says that the network changed.
|
|
test.each([
|
|
{ button: "btn-save-rpc", field: "settings-rpc", first: "follow" },
|
|
{ button: "btn-save-rpc", field: "settings-rpc", first: "save" },
|
|
{
|
|
button: "btn-save-blockscout",
|
|
field: "settings-blockscout",
|
|
first: "follow",
|
|
},
|
|
{
|
|
button: "btn-save-blockscout",
|
|
field: "settings-blockscout",
|
|
first: "save",
|
|
},
|
|
])(
|
|
"$button saves nothing when the network changes before the URL is stored ($first first)",
|
|
async ({ button, field, first }) => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
// This answer passes both endpoint checks. The RPC check reads
|
|
// Ethereum Mainnet's chain id from it. The Blockscout check reads `ok`.
|
|
globalThis.fetch.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ result: NETWORKS.mainnet.chainId }),
|
|
});
|
|
const url = "https://own-node.example.invalid";
|
|
|
|
let releaseLock;
|
|
const lockHeld = navigator.locks.request(
|
|
"autistmask",
|
|
() =>
|
|
new Promise((resolve) => {
|
|
releaseLock = resolve;
|
|
}),
|
|
);
|
|
// The click handler runs until its save waits for the lock.
|
|
const tapSave = async () => {
|
|
page.node(field).value = url;
|
|
const saving = page.node(button).listeners.click.map((fn) => fn());
|
|
await page.settle();
|
|
return saving;
|
|
};
|
|
let saving;
|
|
if (first === "save") saving = await tapSave();
|
|
await backgroundSwitch(page, "sepolia");
|
|
if (first === "follow") saving = await tapSave();
|
|
releaseLock();
|
|
await lockHeld;
|
|
await Promise.all(saving);
|
|
await page.settle();
|
|
|
|
const { state } = require("../src/shared/state");
|
|
expect(state.networkId).toBe("sepolia");
|
|
expect(JSON.stringify(page.storage.read("autistmask"))).not.toContain(
|
|
url,
|
|
);
|
|
expect(page.text("flash-msg")).toBe(
|
|
"The URL was not saved because the network changed.",
|
|
);
|
|
},
|
|
);
|
|
|
|
// The popup's follow of the background's switch to Sepolia has set `state` to
|
|
// Sepolia. The follow's write has not returned yet, so Settings still shows
|
|
// Ethereum Mainnet. The user taps "Save" there. The URL is for Ethereum
|
|
// Mainnet, so the save stores the URL for neither network.
|
|
test.each([
|
|
{ button: "btn-save-rpc", field: "settings-rpc" },
|
|
{ button: "btn-save-blockscout", field: "settings-blockscout" },
|
|
])(
|
|
"$button saves nothing when tapped while the follow's write waits",
|
|
async ({ button, field }) => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
// This answer passes both endpoint checks on Ethereum Mainnet.
|
|
globalThis.fetch.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ result: NETWORKS.mainnet.chainId }),
|
|
});
|
|
const url = "https://own-node.example.invalid";
|
|
|
|
const { set } = page.storage.local;
|
|
const write = set.getMockImplementation();
|
|
let releaseWrite;
|
|
const writeHeld = new Promise((resolve) => {
|
|
releaseWrite = resolve;
|
|
});
|
|
// The background's write lands; the next, the follow's, waits.
|
|
set.mockImplementationOnce(write).mockImplementationOnce(
|
|
async (items) => {
|
|
await writeHeld;
|
|
await write(items);
|
|
},
|
|
);
|
|
await backgroundSwitch(page, "sepolia");
|
|
const { state } = require("../src/shared/state");
|
|
expect(state.networkId).toBe("sepolia");
|
|
expect(page.value("settings-network")).toBe("mainnet");
|
|
|
|
page.node(field).value = url;
|
|
const saving = page.node(button).listeners.click.map((fn) => fn());
|
|
await page.settle();
|
|
releaseWrite();
|
|
await Promise.all(saving);
|
|
await page.settle();
|
|
|
|
expect(JSON.stringify(page.storage.read("autistmask"))).not.toContain(
|
|
url,
|
|
);
|
|
expect(page.text("flash-msg")).toBe(
|
|
"The URL was not saved because the network changed.",
|
|
);
|
|
},
|
|
);
|
|
|
|
// The user taps the RPC "Save" on Ethereum Mainnet. While the check waits for
|
|
// the node's answer, the popup follows the background's switch to Sepolia. The
|
|
// node answers with Sepolia's chain id. So the check refuses the node for
|
|
// Ethereum Mainnet, and nothing is stored.
|
|
test("the RPC check compares the answer with the network at the tap", async () => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
let answer;
|
|
globalThis.fetch.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
answer = resolve;
|
|
}),
|
|
);
|
|
const url = "https://sepolia-node.example.invalid";
|
|
|
|
page.node("settings-rpc").value = url;
|
|
const saving = page.node("btn-save-rpc").listeners.click.map((fn) => fn());
|
|
await page.settle();
|
|
await backgroundSwitch(page, "sepolia");
|
|
const { state } = require("../src/shared/state");
|
|
expect(state.networkId).toBe("sepolia");
|
|
answer({ json: async () => ({ result: NETWORKS.sepolia.chainId }) });
|
|
await Promise.all(saving);
|
|
await page.settle();
|
|
|
|
expect(JSON.stringify(page.storage.read("autistmask"))).not.toContain(url);
|
|
expect(page.text("flash-msg")).toBe(
|
|
"Wrong network: expected Ethereum Mainnet.",
|
|
);
|
|
});
|
|
|
|
// The user taps the Blockscout "Save" on Ethereum Mainnet. While the check
|
|
// waits for Blockscout's answer, the popup follows the background's switch to
|
|
// Sepolia. The URL is for Ethereum Mainnet, so nothing is stored.
|
|
test("the Blockscout save is for the network at the tap", async () => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
let answer;
|
|
globalThis.fetch.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
answer = resolve;
|
|
}),
|
|
);
|
|
const url = "https://own-explorer.example.invalid/api/v2";
|
|
|
|
page.node("settings-blockscout").value = url;
|
|
const saving = page
|
|
.node("btn-save-blockscout")
|
|
.listeners.click.map((fn) => fn());
|
|
await page.settle();
|
|
await backgroundSwitch(page, "sepolia");
|
|
expect(page.value("settings-network")).toBe("sepolia");
|
|
answer({ ok: true });
|
|
await Promise.all(saving);
|
|
await page.settle();
|
|
|
|
expect(JSON.stringify(page.storage.read("autistmask"))).not.toContain(url);
|
|
expect(page.text("flash-msg")).toBe(
|
|
"The URL was not saved because the network changed.",
|
|
);
|
|
});
|
|
|
|
// Settings is open when the network changes to Sepolia. The popup follows the
|
|
// background's switch, or the user picks Sepolia in the network selector. Each
|
|
// "Save" after the switch stores its URL for Sepolia.
|
|
test.each([
|
|
["a follow", (page) => backgroundSwitch(page, "sepolia")],
|
|
[
|
|
"the network selector",
|
|
async (page) => {
|
|
const select = page.node("settings-network");
|
|
select.value = "sepolia";
|
|
for (const fn of select.listeners.change) await fn();
|
|
await page.settle();
|
|
},
|
|
],
|
|
])(
|
|
"after a switch to Sepolia by %s, Settings saves each URL for Sepolia",
|
|
async (_how, switchToSepolia) => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
await switchToSepolia(page);
|
|
// This answer passes both endpoint checks on Sepolia. The RPC check
|
|
// reads Sepolia's chain id from it. The Blockscout check reads `ok`.
|
|
globalThis.fetch.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ result: NETWORKS.sepolia.chainId }),
|
|
});
|
|
const rpcUrl = "https://own-node.example.invalid";
|
|
const blockscoutUrl = "https://own-explorer.example.invalid/api/v2";
|
|
|
|
page.node("settings-rpc").value = rpcUrl;
|
|
await page.click("btn-save-rpc");
|
|
expect(page.text("flash-msg")).toBe("Saved.");
|
|
page.node("settings-blockscout").value = blockscoutUrl;
|
|
await page.click("btn-save-blockscout");
|
|
expect(page.text("flash-msg")).toBe("Saved.");
|
|
|
|
const stored = page.storage.read("autistmask");
|
|
expect(stored.networkId).toBe("sepolia");
|
|
expect(stored.rpcUrl).toBe(rpcUrl);
|
|
expect(stored.blockscoutUrl).toBe(blockscoutUrl);
|
|
},
|
|
);
|
|
|
|
// The user picks Sepolia in Settings, but the switch's write fails. The page is
|
|
// then on Sepolia, while Settings was last drawn for Ethereum Mainnet. A "Save"
|
|
// after the failed switch stores nothing for either network.
|
|
test.each([
|
|
{ button: "btn-save-rpc", field: "settings-rpc" },
|
|
{ button: "btn-save-blockscout", field: "settings-blockscout" },
|
|
])(
|
|
"$button saves nothing after a switch in Settings whose write failed",
|
|
async ({ button, field }) => {
|
|
const page = await bootOnAddress();
|
|
await page.click("btn-settings");
|
|
page.storage.local.set.mockRejectedValueOnce(
|
|
new Error("quota exceeded"),
|
|
);
|
|
const select = page.node("settings-network");
|
|
select.value = "sepolia";
|
|
for (const fn of select.listeners.change) {
|
|
await expect(fn()).rejects.toThrow("quota exceeded");
|
|
}
|
|
await page.settle();
|
|
const { state } = require("../src/shared/state");
|
|
expect(state.networkId).toBe("sepolia");
|
|
// This answer passes both endpoint checks on Ethereum Mainnet.
|
|
globalThis.fetch.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ result: NETWORKS.mainnet.chainId }),
|
|
});
|
|
const url = "https://own-node.example.invalid";
|
|
|
|
page.node(field).value = url;
|
|
await page.click(button);
|
|
|
|
expect(JSON.stringify(page.storage.read("autistmask"))).not.toContain(
|
|
url,
|
|
);
|
|
expect(page.text("flash-msg")).toBe(
|
|
"The URL was not saved because the network changed.",
|
|
);
|
|
},
|
|
);
|
|
|
|
// Such as one a newer build wrote, which this build cannot read. Nothing reads
|
|
// or writes the stored record for it: the popup's next save finds it, as
|
|
// before.
|
|
test("a record that names no network is not followed", async () => {
|
|
const page = await bootOnAddress();
|
|
const { get, set } = page.storage.local;
|
|
const reads = get.mock.calls.length;
|
|
|
|
await set({
|
|
autistmask: {
|
|
schemaVersion: STATE_SCHEMA_VERSION + 1,
|
|
wallets: [{ type: "hd", name: "Main", addresses: [] }],
|
|
},
|
|
});
|
|
await page.settle();
|
|
|
|
expect(get).toHaveBeenCalledTimes(reads);
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
});
|
|
|
|
// A record that takes the switch from the switched record ends up as that
|
|
// record, balances the background fetched since included.
|
|
test("copyChainSwitchFields() takes every field applyChainSwitchFields() sets", () => {
|
|
const before = unversionedValidProfile({
|
|
trackedTokens: [MAINNET_TOKEN],
|
|
networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] },
|
|
networkEndpoints: {
|
|
sepolia: {
|
|
rpcUrl: "http://127.0.0.1:8545",
|
|
blockscoutUrl: "http://127.0.0.1:4000/api/v2",
|
|
},
|
|
},
|
|
lastBalanceRefresh: 1,
|
|
tokenHolderCache: { [MAINNET_TOKEN.address]: 5000 },
|
|
fraudContracts: ["0x1111111111111111111111111111111111111111"],
|
|
});
|
|
const switched = structuredClone(before);
|
|
applyChainSwitchFields(switched, "sepolia");
|
|
switched.wallets[0].addresses[0].balance = "2.0";
|
|
|
|
const follower = structuredClone(before);
|
|
copyChainSwitchFields(follower, switched);
|
|
|
|
expect(follower).toEqual(switched);
|
|
});
|