diff --git a/src/popup/views/addToken.js b/src/popup/views/addToken.js index 5ae5390..793a6d6 100644 --- a/src/popup/views/addToken.js +++ b/src/popup/views/addToken.js @@ -17,6 +17,11 @@ const { log } = require("../../shared/log"); // AddToken was last opened changes neither the screen nor the status line. let addTokenScreenOpenings = 0; +function findTracked(address) { + const lower = address.toLowerCase(); + return state.trackedTokens.find((t) => t.address.toLowerCase() === lower); +} + function show() { addTokenScreenOpenings++; $("add-token-address").value = ""; @@ -50,9 +55,7 @@ function init(ctx) { showFlash("Enter a valid contract address starting with 0x."); return; } - const already = state.trackedTokens.find( - (t) => t.address.toLowerCase() === contractAddr.toLowerCase(), - ); + const already = findTracked(contractAddr); if (already) { showFlash(already.symbol + " is already being tracked."); return; @@ -85,6 +88,18 @@ function init(ctx) { ); return; } + // Another click on Add may have tracked this token while this + // lookup waited. + const trackedMeanwhile = findTracked(contractAddr); + if (trackedMeanwhile) { + showFlash( + trackedMeanwhile.symbol + " is already being tracked.", + ); + if (addTokenScreenOpenings !== opening) return; + infoEl.textContent = ""; + infoEl.style.visibility = "hidden"; + return; + } // The balance list never shows a token that the spoof rule // refuses, so such a token is not tracked. if (isSpoofedSymbol(info.symbol, contractAddr, chainId)) { diff --git a/src/popup/views/settingsAddToken.js b/src/popup/views/settingsAddToken.js index 1c4aa9f..6a67947 100644 --- a/src/popup/views/settingsAddToken.js +++ b/src/popup/views/settingsAddToken.js @@ -163,6 +163,14 @@ function init(_ctx) { ); return; } + // Another click on Add, or a "Top tokens:" button, may have + // tracked this token while this lookup waited. + if (isTracked(addr)) { + showFlash("Already tracked."); + infoEl.textContent = ""; + infoEl.style.visibility = "hidden"; + return; + } // The balance list never shows a token that the spoof rule // refuses, so such a token is not tracked. The rule never refuses // a bundled token, so the "Top tokens:" buttons and the top 100 diff --git a/tests/addTokenLateAnswer.test.js b/tests/addTokenLateAnswer.test.js index 84ae220..b8123a6 100644 --- a/tests/addTokenLateAnswer.test.js +++ b/tests/addTokenLateAnswer.test.js @@ -1,8 +1,10 @@ // 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. +// still shows goes to AddressDetail. On either add-token screen, an answer for a +// token tracked while the lookup waited tracks nothing +// (https://git.eeqj.de/sneak/AutistMask/issues/727). Driven through the real +// popup over a stored profile. const { bootPopup, @@ -43,9 +45,9 @@ async function openAddToken() { 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) { +// Presses `button` with a contract lookup that answers only when the function +// returned here is called. That function waits for the add to finish. +function addWithPendingLookup(boot, button = "btn-add-token-confirm") { const { lookupTokenInfo } = require("../src/shared/balances"); let answer; lookupTokenInfo.mockImplementationOnce( @@ -54,7 +56,7 @@ function addWithPendingLookup(boot) { answer = resolve; }), ); - const adding = boot.click("btn-add-token-confirm"); + const adding = boot.click(button); return async () => { answer({ symbol: TOKEN.symbol, @@ -136,3 +138,78 @@ test("an answer while AddToken still shows goes to AddressDetail with the token expect(boot.node("address-balances").innerHTML).toContain("TST"); expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]); }); + +test("an answer for a token tracked after AddToken was opened again leaves the status line as it is", async () => { + const boot = await openAddToken(); + const answerFirstLookup = addWithPendingLookup(boot); + await boot.click("btn-add-token-back"); + await boot.click("btn-add-token"); + boot.node("add-token-address").value = OTHER_TOKEN_ADDRESS; + // The second lookup is never answered. + addWithPendingLookup(boot); + require("../src/popup/views/helpers").trackToken(TOKEN); + + await answerFirstLookup(); + expect(boot.text("flash-msg")).toBe("TST is already being tracked."); + expect(boot.text("add-token-info")).toBe("Looking up token..."); +}); + +// Each screen is opened from AddressDetail by the buttons named, in order. +describe.each([ + [ + "AddToken", + ["btn-add-token"], + "add-token-address", + "btn-add-token-confirm", + "add-token-info", + "TST is already being tracked.", + ], + [ + "SettingsAddToken", + ["btn-settings", "btn-settings-add-token"], + "settings-addtoken-address", + "btn-settings-addtoken-manual", + "settings-addtoken-info", + "Already tracked.", + ], +])("%s", (_, opening, field, button, statusLine, alreadyTrackedFlash) => { + // The screen open with TOKEN's contract address typed in. + async function openScreen() { + const boot = await bootPopup( + unversionedValidProfile({ + currentView: "address", + selectedWallet: 0, + selectedAddress: 0, + }), + ); + expect(boot.pageErrors).toEqual([]); + for (const id of opening) await boot.click(id); + boot.node(field).value = TOKEN.address; + return boot; + } + + test("a second click on Add while the lookup waits tracks the token once", async () => { + const boot = await openScreen(); + const answerFirstClick = addWithPendingLookup(boot, button); + const answerSecondClick = addWithPendingLookup(boot, button); + + await answerFirstClick(); + await answerSecondClick(); + expect(boot.text("flash-msg")).toBe(alreadyTrackedFlash); + expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]); + }); + + test("an answer for a token tracked while the lookup waited clears the status line", async () => { + const boot = await openScreen(); + const answerLookup = addWithPendingLookup(boot, button); + expect(boot.text(statusLine)).toBe("Looking up token..."); + require("../src/popup/views/helpers").trackToken(TOKEN); + + await answerLookup(); + expect(boot.text("flash-msg")).toBe(alreadyTrackedFlash); + expect(boot.text(statusLine)).toBe(""); + expect(boot.node(statusLine).style.visibility).toBe("hidden"); + const { state } = require("../src/shared/state"); + expect(state.trackedTokens).toEqual([TOKEN]); + }); +});