diff --git a/README.md b/README.md index 2ee60cc..6ac6d8b 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 leaving AddToken before the contract lookup answers → 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..539d0c4 100644 --- a/src/popup/views/addToken.js +++ b/src/popup/views/addToken.js @@ -80,6 +80,13 @@ function init(ctx) { }); await saveState(); ctx.doRefreshAndRender(); + // The lookup is over, so a later Back onto AddToken shows no + // status. + infoEl.textContent = ""; + infoEl.style.visibility = "hidden"; + // If the user left AddToken while the lookup ran, 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..2e8b496 --- /dev/null +++ b/tests/addTokenLateAnswer.test.js @@ -0,0 +1,96 @@ +// 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", +}; + +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 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]); +});