1 Commits
Author SHA1 Message Date
sneak 905d20bb38 fix: Settings never shows "Saved." for a URL the input no longer shows (closes #695)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
The RPC and Blockscout "Save" buttons in Settings now store nothing if the input no longer shows the URL that was checked. The input loses that URL when Settings is drawn again during the check, after another page changed a setting. Before this change, the typed URL was stored and "Saved." appeared while the input showed the previous URL.

The save compares the input and sets the URL in `state` in one step. A save refused because the network changed shows "The URL was not saved because the network changed." A save refused because the input changed shows "The URL was not saved because it was changed during the check."

Model: opus-5-5
2026-10-27 12:35:44 +00:00
3 changed files with 87 additions and 13 deletions
+2 -1
View File
@@ -2280,7 +2280,8 @@ view would leave a wallet one click from deletion.
- If the network changes before either "Save" stores its URL, the URL is not
saved and a flash message says so
- Either "Save" stores its URL only if the input still shows that URL when
the endpoint answers
the endpoint answers. Otherwise a flash message says that the URL was not
saved
- Etherscan:
- "Check recipients on Etherscan" checkbox, unticked until the user
ticks it
+14 -9
View File
@@ -340,26 +340,31 @@ function showNewSettings() {
// the stored record too. "Saved." then means that the stored record holds
// `url`.
//
// `input` is the id of the field that the user typed `url` in. This page draws
// `input` is the id of the input that the user typed `url` in. This page draws
// Settings again after following another page's change to a setting. A redraw
// during the endpoint check puts the URL from `state` back in the field. So if
// the field no longer shows `url`, nothing is saved and no flash message
// appears. The field is compared inside the save, so a redraw after the
// comparison shows `url` from `state`.
// during the endpoint check puts the URL from `state` back in the input. So if
// the input no longer shows `url`, nothing is saved. The flash message then
// says that the URL was changed during the check. The save compares the input
// and sets `url` in `state` in one step. So a redraw after the comparison shows
// `url`.
async function saveEndpoint(field, input, url, networkId) {
let refused = false;
let fieldChanged = false;
let inputChanged = false;
await saveState((stored) => {
refused =
state.networkId !== networkId || stored.networkId !== networkId;
fieldChanged = $(input).value.trim() !== url;
if (refused || fieldChanged) return false;
inputChanged = $(input).value.trim() !== url;
if (refused || inputChanged) return false;
state[field] = url;
stored[field] = url;
});
if (refused) {
showFlash("The URL was not saved because the network changed.");
} else if (!fieldChanged) {
} else if (inputChanged) {
showFlash(
"The URL was not saved because it was changed during the check.",
);
} else {
showFlash("Saved.");
}
}
+71 -3
View File
@@ -51,8 +51,8 @@ test.each([
// The user types a URL in Settings and taps "Save". While the check waits for
// the endpoint's answer, another page turns "Use Blockscout" on. This page
// follows that change and draws Settings again, with the stored URL in the
// field. The check then passes. The typed URL is not stored, and the flash
// message never reads "Saved."
// input. The check then passes. The typed URL is not stored. The flash message
// says that the URL was changed during the check
// (https://git.eeqj.de/sneak/AutistMask/issues/695).
test.each([
{ button: "btn-save-rpc", input: "settings-rpc", field: "rpcUrl" },
@@ -96,6 +96,74 @@ test.each([
expect(page.storage.read("autistmask")[field]).toBe(storedUrl);
expect(page.value(input)).toBe(storedUrl);
expect(page.text("flash-msg")).not.toBe("Saved.");
expect(page.text("flash-msg")).toBe(
"The URL was not saved because it was changed during the check.",
);
},
);
// As above, but another write holds the lock. So this page's follow of the
// change to "Use Blockscout" waits for the lock. The endpoint answers while the
// input still shows the typed URL. The save then asks for the lock, after the
// follow did. So the follow draws Settings again before the save compares the
// input.
test.each([
{ button: "btn-save-rpc", input: "settings-rpc", field: "rpcUrl" },
{
button: "btn-save-blockscout",
input: "settings-blockscout",
field: "blockscoutUrl",
},
])(
"$button stores nothing when Settings is drawn again while the save waits for the lock",
async ({ button, input, field }) => {
const page = await bootPopup(unversionedValidProfile());
await page.click("btn-settings");
const storedUrl = page.value(input);
let answer;
globalThis.fetch.mockReturnValue(
new Promise((resolve) => {
answer = resolve;
}),
);
const typedUrl = "https://typed.example.invalid";
page.node(input).value = typedUrl;
const saving = page.node(button).listeners.click.map((fn) => fn());
await page.settle();
let releaseLock;
const lockHeld = navigator.locks.request(
"autistmask",
() =>
new Promise((resolve) => {
releaseLock = resolve;
}),
);
// The browser announces this write to this page.
await page.storage.local.set({
autistmask: {
...page.storage.read("autistmask"),
useBlockscout: true,
},
});
await page.settle();
// This answer passes both endpoint checks on Ethereum Mainnet.
answer({
ok: true,
json: async () => ({ result: NETWORKS.mainnet.chainId }),
});
await page.settle();
expect(page.value(input)).toBe(typedUrl);
releaseLock();
await lockHeld;
await Promise.all(saving);
await page.settle();
expect(page.node("settings-use-blockscout").checked).toBe(true);
expect(page.storage.read("autistmask")[field]).toBe(storedUrl);
expect(page.value(input)).toBe(storedUrl);
expect(page.text("flash-msg")).toBe(
"The URL was not saved because it was changed during the check.",
);
},
);