diff --git a/README.md b/README.md index 2ee60cc..2fcef1e 100644 --- a/README.md +++ b/README.md @@ -2233,6 +2233,8 @@ view would leave a wallet one click from deletion. **AddressDetail** - "Add" with a token already tracked, a scam-listed address, or a failed contract lookup → flash message, no screen change + - "Add", then the contract lookup answers while another screen shows, or + after AddToken was opened again → tracks the token, no screen change - "Add", then a network switch in Settings before the contract lookup answers → nothing is tracked; "The active network changed before the lookup finished, so the token was not added." flash message, no screen diff --git a/src/popup/views/addToken.js b/src/popup/views/addToken.js index 76c96d0..0082f67 100644 --- a/src/popup/views/addToken.js +++ b/src/popup/views/addToken.js @@ -5,7 +5,12 @@ const { lookupTokenInfo } = require("../../shared/balances"); const { isScamAddress } = require("../../shared/scamlist"); const { log } = require("../../shared/log"); +// Counts the times AddToken has opened. A contract lookup started before +// AddToken was last opened changes neither the screen nor the status line. +let addTokenScreenOpenings = 0; + function show() { + addTokenScreenOpenings++; $("add-token-address").value = ""; $("add-token-info").textContent = ""; $("add-token-info").style.visibility = "hidden"; @@ -55,6 +60,7 @@ function init(ctx) { // The network the lookup asks, which Settings may switch away from // before it answers, as for a transaction in confirmTx.js. const chainId = currentNetwork().chainId; + const opening = addTokenScreenOpenings; try { const info = await lookupTokenInfo( contractAddr, @@ -80,6 +86,17 @@ function init(ctx) { }); await saveState(); ctx.doRefreshAndRender(); + // If the lookup answers after AddToken was opened again, the + // token is still added, but the screen and the status line stay + // as they are. + if (addTokenScreenOpenings !== opening) return; + // The lookup is over, so a later Back onto AddToken shows no + // status. + infoEl.textContent = ""; + infoEl.style.visibility = "hidden"; + // If the lookup answers while another screen shows, the token is + // still added but the screen does not change. + if (state.currentView !== "add-token") return; // Pop the stack (back to address detail) and re-render it // so the newly added token is visible immediately. if (state.viewStack.length > 0) { diff --git a/tests/addTokenLateAnswer.test.js b/tests/addTokenLateAnswer.test.js new file mode 100644 index 0000000..af4ac0a --- /dev/null +++ b/tests/addTokenLateAnswer.test.js @@ -0,0 +1,115 @@ +// A contract lookup on AddToken can answer after the user has left AddToken. +// The token is still added, and the screen the user is on does not change +// (https://git.eeqj.de/sneak/AutistMask/issues/603). An answer while AddToken +// still shows goes to AddressDetail. Driven through the real popup over a +// stored profile. + +const { + bootPopup, + cleanupPopup, + unversionedValidProfile, + TOKEN_ADDRESS, +} = require("./support/popupBoot"); + +const TOKEN = { + address: TOKEN_ADDRESS, + symbol: "TST", + decimals: 18, + name: "Test Token", +}; + +// A second contract address, typed after AddToken was opened again. +const OTHER_TOKEN_ADDRESS = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + +afterEach(() => { + cleanupPopup(); +}); + +// The popup reopened onto AddressDetail, then AddToken opened from it with +// TOKEN's contract address typed in. +async function openAddToken() { + const boot = await bootPopup( + unversionedValidProfile({ + currentView: "address", + selectedWallet: 0, + selectedAddress: 0, + }), + ); + expect(boot.pageErrors).toEqual([]); + await boot.click("btn-add-token"); + expect(boot.visibleViews()).toEqual(["add-token"]); + boot.node("add-token-address").value = TOKEN.address; + return boot; +} + +// Presses Add with a contract lookup that answers only when the function +// returned here is called. That function waits for Add to finish. +function addWithPendingLookup(boot) { + const { lookupTokenInfo } = require("../src/shared/balances"); + let answer; + lookupTokenInfo.mockImplementationOnce( + () => + new Promise((resolve) => { + answer = resolve; + }), + ); + const adding = boot.click("btn-add-token-confirm"); + return async () => { + answer({ + symbol: TOKEN.symbol, + decimals: TOKEN.decimals, + name: TOKEN.name, + }); + await adding; + }; +} + +test("an answer after the user opened Settings adds the token and Settings stays", async () => { + const boot = await openAddToken(); + const answerLookup = addWithPendingLookup(boot); + await boot.click("btn-settings"); + expect(boot.visibleViews()).toEqual(["settings"]); + + await answerLookup(); + expect(boot.visibleViews()).toEqual(["settings"]); + expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]); +}); + +test("Back onto AddToken after that answer shows no lookup status", async () => { + const boot = await openAddToken(); + const answerLookup = addWithPendingLookup(boot); + expect(boot.text("add-token-info")).toBe("Looking up token..."); + await boot.click("btn-settings"); + await answerLookup(); + + await boot.click("btn-settings-back"); + expect(boot.visibleViews()).toEqual(["add-token"]); + expect(boot.text("add-token-info")).toBe(""); + expect(boot.node("add-token-info").style.visibility).toBe("hidden"); +}); + +test("an answer after AddToken was opened again adds the token and leaves AddToken and its status line as they are", async () => { + const boot = await openAddToken(); + const answerFirstLookup = addWithPendingLookup(boot); + await boot.click("btn-add-token-back"); + await boot.click("btn-add-token"); + expect(boot.visibleViews()).toEqual(["add-token"]); + boot.node("add-token-address").value = OTHER_TOKEN_ADDRESS; + // The second lookup is never answered. + addWithPendingLookup(boot); + + await answerFirstLookup(); + expect(boot.visibleViews()).toEqual(["add-token"]); + expect(boot.text("add-token-info")).toBe("Looking up token..."); + expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]); +}); + +test("an answer while AddToken still shows goes to AddressDetail with the token listed", async () => { + const boot = await openAddToken(); + const answerLookup = addWithPendingLookup(boot); + + await answerLookup(); + expect(boot.visibleViews()).toEqual(["address"]); + expect(boot.node("address-balances").innerHTML).toContain("TST"); + expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]); +});