const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a"; function oneWallet() { return [{ name: "Wallet 1", type: "hd", addresses: [ADDRESS] }]; } // state.js resolves the storage API at require time, so the stub has to exist // before the module is loaded, and the module registry has to be reset between // cases because `state` is a module-level singleton. function loadModuleWith(persisted) { jest.resetModules(); const set = jest.fn(async () => {}); global.chrome = { storage: { local: { get: jest.fn(async () => persisted ? { autistmask: persisted } : {}, ), set, }, }, }; return { mod: require("../src/shared/state"), set }; } afterEach(() => { delete global.chrome; }); describe("loadState hasWallet reconciliation", () => { // A profile that deleted its last wallet on a build predating the write // path fix keeps hasWallet: true forever. It must load as no wallet, which // is what sends the popup to the welcome view. test("stored hasWallet true with zero wallets loads as no wallet", async () => { const { mod } = loadModuleWith({ hasWallet: true, wallets: [] }); await mod.loadState(); expect(mod.state.hasWallet).toBe(false); }); test("stored hasWallet true with a missing wallets key loads as no wallet", async () => { const { mod } = loadModuleWith({ hasWallet: true }); await mod.loadState(); expect(mod.state.wallets).toEqual([]); expect(mod.state.hasWallet).toBe(false); }); test("stored hasWallet false with one wallet loads as having a wallet", async () => { const { mod } = loadModuleWith({ hasWallet: false, wallets: oneWallet(), }); await mod.loadState(); expect(mod.state.hasWallet).toBe(true); }); test("absent hasWallet with wallets present loads as having a wallet", async () => { const { mod } = loadModuleWith({ wallets: oneWallet() }); await mod.loadState(); expect(mod.state.hasWallet).toBe(true); }); test("consistent stored states are preserved", async () => { const withWallet = loadModuleWith({ hasWallet: true, wallets: oneWallet(), }); await withWallet.mod.loadState(); expect(withWallet.mod.state.hasWallet).toBe(true); const without = loadModuleWith({ hasWallet: false, wallets: [] }); await without.mod.loadState(); expect(without.mod.state.hasWallet).toBe(false); }); test("empty storage leaves the default no-wallet state", async () => { const { mod } = loadModuleWith(null); await mod.loadState(); expect(mod.state.hasWallet).toBe(false); expect(mod.state.wallets).toEqual([]); }); // The correction is derived on every load rather than written back, so a // load never has a storage side effect. test("loadState does not write to storage", async () => { const { mod, set } = loadModuleWith({ hasWallet: true, wallets: [] }); await mod.loadState(); expect(set).not.toHaveBeenCalled(); }); // Deriving must not disturb the rest of the load. test("other persisted fields still load", async () => { const { mod } = loadModuleWith({ hasWallet: false, wallets: oneWallet(), networkId: "sepolia", theme: "dark", activeAddress: ADDRESS, }); await mod.loadState(); expect(mod.state.networkId).toBe("sepolia"); expect(mod.state.theme).toBe("dark"); expect(mod.state.activeAddress).toBe(ADDRESS); }); }); // The known-symbol spoof filter is a safety filter, so an existing profile // stored before the setting existed must load with it on rather than with // undefined, which would read as off. describe("hideSpoofedSymbols persistence", () => { test("defaults to on with empty storage", async () => { const { mod } = loadModuleWith(null); await mod.loadState(); expect(mod.state.hideSpoofedSymbols).toBe(true); }); test("a profile stored without the key loads with it on", async () => { const { mod } = loadModuleWith({ wallets: oneWallet() }); await mod.loadState(); expect(mod.state.hideSpoofedSymbols).toBe(true); }); test("an explicit false survives the load", async () => { const { mod } = loadModuleWith({ wallets: oneWallet(), hideSpoofedSymbols: false, }); await mod.loadState(); expect(mod.state.hideSpoofedSymbols).toBe(false); }); test("saveState persists the flag", async () => { const { mod, set } = loadModuleWith(null); mod.state.hideSpoofedSymbols = false; await mod.saveState(); expect(set).toHaveBeenCalledWith({ autistmask: expect.objectContaining({ hideSpoofedSymbols: false }), }); }); test("the flag round-trips off through save and load", async () => { const first = loadModuleWith(null); first.mod.state.hideSpoofedSymbols = false; await first.mod.saveState(); const persisted = first.set.mock.calls[0][0].autistmask; const second = loadModuleWith(persisted); await second.mod.loadState(); expect(second.mod.state.hideSpoofedSymbols).toBe(false); }); test("the flag round-trips back on through save and load", async () => { const first = loadModuleWith(null); first.mod.state.hideSpoofedSymbols = true; await first.mod.saveState(); const persisted = first.set.mock.calls[0][0].autistmask; const second = loadModuleWith(persisted); await second.mod.loadState(); expect(second.mod.state.hideSpoofedSymbols).toBe(true); }); }); // restoreView() refuses to reopen ONTO a non-restorable view, but the stack // behind it was restored verbatim, so Back could still walk onto a screen // whose content is deliberately never re-rendered — and "show-phrase" has no // Back control of its own to leave by. The stack is filtered on load, at the // first entry the popup would not render, and everything above it goes too: // those entries were reached THROUGH the dropped one. describe("restored viewStack is filtered against RESTORABLE_VIEWS", () => { const NON_RESTORABLE = ["export-privkey", "show-phrase"]; function restoredStack(viewStack, currentView = "settings") { return loadModuleWith({ wallets: oneWallet(), currentView, viewStack, }); } test("a non-restorable view at the top of the stack is dropped", async () => { const { mod } = restoredStack(["main", "address", "export-privkey"]); await mod.loadState(); expect(mod.state.viewStack).toEqual(["main", "address"]); }); test("a non-restorable view in the middle truncates the stack there", async () => { const { mod } = restoredStack(["main", "show-phrase", "address"]); await mod.loadState(); expect(mod.state.viewStack).toEqual(["main"]); }); // Truncating a stack rooted at a non-restorable view leaves nothing, and // the restored view still needs somewhere for Back to go. test("a non-restorable view at the bottom leaves main to go back to", async () => { const { mod } = restoredStack(["export-privkey", "address", "receive"]); await mod.loadState(); expect(mod.state.viewStack).toEqual(["main"]); }); test("no restored stack retains a secret-bearing view", async () => { for (const view of NON_RESTORABLE) { const { mod } = restoredStack(["main", "address", view, "receive"]); await mod.loadState(); expect(mod.state.viewStack).not.toContain(view); } }); // The rule is "views the popup will render", not a blocklist of the two // secret screens: a name no longer in the set (or never a view at all) // has to go the same way. test("a name that is not a restorable view at all is dropped", async () => { const { mod } = restoredStack(["main", "welcome", "address"]); await mod.loadState(); expect(mod.state.viewStack).toEqual(["main"]); }); test("an ordinary restorable stack is restored unchanged", async () => { const stack = ["main", "address", "address-token"]; const { mod } = restoredStack(stack); await mod.loadState(); expect(mod.state.viewStack).toEqual(stack); }); test("restoring onto main keeps the stack empty", async () => { const { mod } = restoredStack(["show-phrase"], "main"); await mod.loadState(); expect(mod.state.viewStack).toEqual([]); }); test("a stack that is not an array still loads as empty", async () => { const { mod } = restoredStack("main"); await mod.loadState(); expect(mod.state.viewStack).toEqual([]); }); // Filtering belongs on load, not on save: the live in-session stack is // legitimate — the user really is one Back away from a screen that is // rendered right now — and only a load-side filter also cleans the // stacks already sitting in storage. test("saveState persists the live stack verbatim", async () => { const { mod, set } = loadModuleWith(null); mod.state.viewStack = ["main", "address", "export-privkey"]; await mod.saveState(); expect(set).toHaveBeenCalledWith({ autistmask: expect.objectContaining({ viewStack: ["main", "address", "export-privkey"], }), }); }); });