// 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. 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). A second click on a "Top // tokens:" button tracks nothing // (https://git.eeqj.de/sneak/AutistMask/issues/729). 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", balanceNotFetched: true, }; // 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 `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( () => new Promise((resolve) => { answer = resolve; }), ); const adding = boot.click(button); 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 from Settings onto AddToken after the lookup answered 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("a lookup that fails after AddToken was opened again leaves the status line as it is", async () => { const boot = await openAddToken(); const { lookupTokenInfo } = require("../src/shared/balances"); let failFirstLookup; lookupTokenInfo.mockImplementationOnce( () => new Promise((_, reject) => { failFirstLookup = reject; }), ); const firstAdd = boot.click("btn-add-token-confirm"); 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); failFirstLookup(new Error("lookup failed")); await firstAdd; expect(boot.text("add-token-info")).toBe("Looking up 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]); }); 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]); const { state } = require("../src/shared/state"); expect(state.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]); }); }); test('a double click on a "Top tokens:" button tracks the token once', async () => { const boot = await bootPopup( unversionedValidProfile({ currentView: "address", selectedWallet: 0, selectedAddress: 0, }), ); expect(boot.pageErrors).toEqual([]); // The page stub does not turn the markup the screen draws into elements. // So the list hands the screen a stand-in for TOKEN's button. let press; boot.node("settings-addtoken-top10").querySelectorAll = () => [ { dataset: { address: TOKEN.address, symbol: TOKEN.symbol, decimals: String(TOKEN.decimals), name: TOKEN.name, }, addEventListener: (name, fn) => { press = fn; }, }, ]; await boot.click("btn-settings"); await boot.click("btn-settings-add-token"); const firstClick = press(); const secondClick = press(); const { state } = require("../src/shared/state"); expect(state.trackedTokens).toEqual([TOKEN]); // Read before the first click's save ends, when "Added TST" replaces it. expect(boot.text("flash-msg")).toBe("Already tracked."); await Promise.all([firstClick, secondClick]); await boot.settle(); expect(state.trackedTokens).toEqual([TOKEN]); expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]); });