// Another page stores a different RPC or Blockscout URL for the same network. // Then the user taps "Save" in Settings on this page, on the URL that this page // loaded. The URL on screen is stored, and the flash message reads "Saved." // (https://git.eeqj.de/sneak/AutistMask/issues/688). Driven through the real // popup over a stored profile on Ethereum Mainnet. const { bootPopup, cleanupPopup, unversionedValidProfile, } = require("./support/popupBoot"); const { NETWORKS } = require("../src/shared/networks"); afterEach(() => { cleanupPopup(); }); test.each([ { button: "btn-save-rpc", input: "settings-rpc", field: "rpcUrl" }, { button: "btn-save-blockscout", input: "settings-blockscout", field: "blockscoutUrl", }, ])( "$button stores the URL on screen after another page stored a different one", async ({ button, input, field }) => { const page = await bootPopup(unversionedValidProfile()); await page.click("btn-settings"); const url = page.value(input); // This write sends this page no storage change event. page.storage.write("autistmask", { ...page.storage.read("autistmask"), [field]: "https://other-page.example.invalid", }); // This answer passes both endpoint checks on Ethereum Mainnet. 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 }), }); await page.click(button); expect(page.storage.read("autistmask")[field]).toBe(url); expect(page.text("flash-msg")).toBe("Saved."); }, ); // 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 // 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" }, { button: "btn-save-blockscout", input: "settings-blockscout", field: "blockscoutUrl", }, ])( "$button stores nothing when Settings is drawn again during the check", 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; }), ); page.node(input).value = "https://typed.example.invalid"; const saving = page.node(button).listeners.click.map((fn) => fn()); await page.settle(); // The browser announces this write to this page. await page.storage.local.set({ autistmask: { ...page.storage.read("autistmask"), useBlockscout: true, }, }); await page.settle(); expect(page.node("settings-use-blockscout").checked).toBe(true); // This answer passes both endpoint checks on Ethereum Mainnet. answer({ ok: true, json: async () => ({ result: NETWORKS.mainnet.chainId }), }); await Promise.all(saving); await page.settle(); 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.", ); }, ); // 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.", ); }, );