All checks were successful
check / check (push) Successful in 44s
goBack() only unhid its target. A reopened popup renders the wallet list and the one view restoreView() lands on, so every other view is still the blank static template from index.html: pressing Back from Settings onto an address showed an empty address line and no balances, and the same held for address-token, receive, confirm-tx and transaction. The per-view dispatch and its data guards move out of restoreView() into src/popup/viewRouter.js, and goBack() now routes a popped view through the same code by way of a renderer index.js registers with setBackRenderer(). A view whose backing state is gone falls back to Home the way the restore does, rather than showing an empty template. The renderer declines any view outside RESTORABLE_VIEWS, so goBack() unhides it as before: the restored stack is filtered against that set, so such a view can only be on the stack from the current page load, where forward navigation rendered it on the way in. Forward navigation is untouched and nothing renders twice. tests/backNavigation.test.js drives the real goBack() over the reproduction and over each of address-token, receive, confirm-tx and transaction, with and without their backing state, and pins that a live-session view is still only unhidden. tests/e2e/run.js adds two cases against the real popup in a real browser — a real close and reopen, then Back onto the address screen and onto Receive — because make check cannot see a blank view. Both were demonstrated failing against the unmodified sources: the address line read "" where it should have read the address.
244 lines
8.2 KiB
JavaScript
244 lines
8.2 KiB
JavaScript
// Back after reopening the popup (#268).
|
|
//
|
|
// A reopened popup renders the wallet list and the one view it restores
|
|
// onto; every other view is still the blank static template from
|
|
// index.html. goBack() used to only unhide its target, so Back landed on
|
|
// that blank template for any view the popup had not rendered in this page
|
|
// load. These tests drive the real goBack() with the real router wired to
|
|
// recording view modules, so what is asserted is which view render ran —
|
|
// the thing that was missing.
|
|
//
|
|
// The rendering itself is asserted against the real popup in a real
|
|
// browser by tests/e2e/run.js; here the DOM is a stub, because goBack()
|
|
// only needs showView() to work.
|
|
|
|
const els = new Map();
|
|
|
|
function fakeEl() {
|
|
return {
|
|
textContent: "",
|
|
innerHTML: "",
|
|
classList: {
|
|
toggle() {},
|
|
add() {},
|
|
remove() {},
|
|
contains: () => false,
|
|
},
|
|
remove() {},
|
|
};
|
|
}
|
|
|
|
globalThis.document = {
|
|
getElementById(id) {
|
|
if (!els.has(id)) els.set(id, fakeEl());
|
|
return els.get(id);
|
|
},
|
|
};
|
|
|
|
// helpers.js pulls in state.js, which reads chrome.storage.local at load.
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
|
|
const {
|
|
showView,
|
|
goBack,
|
|
setBackRenderer,
|
|
pushCurrentView,
|
|
} = require("../src/popup/views/helpers");
|
|
const { makeBackRenderer } = require("../src/popup/viewRouter");
|
|
const { state } = require("../src/shared/state");
|
|
|
|
const ADDRESS = "0x1111111111111111111111111111111111111111";
|
|
const TOKEN = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
|
|
|
let calls;
|
|
|
|
// Stand-ins for the view modules. Each records itself and then shows its
|
|
// view, which is what every real view render ends with — so the assertions
|
|
// can tell "rendered and shown" apart from "merely unhidden".
|
|
function recorder(name, view) {
|
|
return () => {
|
|
calls.push(name);
|
|
showView(view);
|
|
};
|
|
}
|
|
|
|
function makeViews() {
|
|
return {
|
|
main: { show: recorder("main", "main") },
|
|
addressDetail: { show: recorder("addressDetail", "address") },
|
|
addressToken: { show: recorder("addressToken", "address-token") },
|
|
receive: { show: recorder("receive", "receive") },
|
|
settings: { show: recorder("settings", "settings") },
|
|
settingsAddToken: {
|
|
show: recorder("settingsAddToken", "settings-addtoken"),
|
|
},
|
|
confirmTx: { restore: recorder("confirmTx", "confirm-tx") },
|
|
transactionDetail: {
|
|
render: recorder("transactionDetail", "transaction"),
|
|
},
|
|
txStatus: {
|
|
restoreWait: () => {
|
|
calls.push("waitTx");
|
|
showView("wait-tx");
|
|
return true;
|
|
},
|
|
renderSuccess: recorder("successTx", "success-tx"),
|
|
renderError: recorder("errorTx", "error-tx"),
|
|
},
|
|
};
|
|
}
|
|
|
|
// The popup as it stands just after a reopen: one wallet with one address,
|
|
// the view the popup restored onto, and the stack behind it.
|
|
function reopenedOn(view, stack, extra) {
|
|
calls = [];
|
|
state.wallets = [
|
|
{
|
|
name: "Wallet 1",
|
|
addresses: [{ address: ADDRESS, balance: "0", tokenBalances: [] }],
|
|
},
|
|
];
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
state.selectedToken = null;
|
|
state.viewData = null;
|
|
state.currentView = view;
|
|
state.viewStack = stack.slice();
|
|
Object.assign(state, extra || {});
|
|
setBackRenderer(makeBackRenderer(state, makeViews()));
|
|
}
|
|
|
|
// The reproduction from the issue, step for step.
|
|
describe("Back onto a view the reopened popup never rendered", () => {
|
|
test("Back from settings renders the address detail underneath", () => {
|
|
reopenedOn("settings", ["main", "address"]);
|
|
goBack();
|
|
expect(calls).toEqual(["addressDetail"]);
|
|
expect(state.currentView).toBe("address");
|
|
expect(state.viewStack).toEqual(["main"]);
|
|
});
|
|
|
|
test("Back onto the token detail renders it", () => {
|
|
reopenedOn("settings", ["main", "address", "address-token"], {
|
|
selectedToken: TOKEN,
|
|
});
|
|
goBack();
|
|
expect(calls).toEqual(["addressToken"]);
|
|
expect(state.currentView).toBe("address-token");
|
|
});
|
|
|
|
test("Back onto Receive renders it", () => {
|
|
reopenedOn("settings", ["main", "address", "receive"]);
|
|
goBack();
|
|
expect(calls).toEqual(["receive"]);
|
|
expect(state.currentView).toBe("receive");
|
|
});
|
|
|
|
test("Back onto the transaction detail renders it", () => {
|
|
reopenedOn("settings", ["main", "transaction"], {
|
|
viewData: { tx: { hash: "0xdead" } },
|
|
});
|
|
goBack();
|
|
expect(calls).toEqual(["transactionDetail"]);
|
|
expect(state.currentView).toBe("transaction");
|
|
});
|
|
|
|
test("Back onto the transaction confirmation restores it", () => {
|
|
reopenedOn("settings", ["main", "confirm-tx"], {
|
|
viewData: { pendingTx: { to: ADDRESS, amount: "1" } },
|
|
});
|
|
goBack();
|
|
expect(calls).toEqual(["confirmTx"]);
|
|
expect(state.currentView).toBe("confirm-tx");
|
|
});
|
|
|
|
test("Back onto Home renders the wallet list", () => {
|
|
reopenedOn("settings", ["main"]);
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
|
|
test("Back with an empty stack renders Home", () => {
|
|
reopenedOn("settings", []);
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
});
|
|
|
|
// The guards are restoreView()'s, so a popped view whose backing data is
|
|
// gone lands on Home rather than on an empty template.
|
|
describe("Back onto a view whose backing data is gone", () => {
|
|
test("the token detail with no token selected falls back to Home", () => {
|
|
reopenedOn("settings", ["main", "address-token"]);
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
|
|
test("the transaction detail with no transaction falls back to Home", () => {
|
|
reopenedOn("settings", ["main", "transaction"]);
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
|
|
test("the confirmation with no pending transaction falls back to Home", () => {
|
|
reopenedOn("settings", ["main", "confirm-tx"]);
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
|
|
test("an address view with no address selected falls back to Home", () => {
|
|
reopenedOn("settings", ["main", "receive"], {
|
|
selectedAddress: null,
|
|
});
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
|
|
test("a wait that can no longer be resumed falls back to Home", () => {
|
|
reopenedOn("settings", ["main", "wait-tx"]);
|
|
const views = makeViews();
|
|
views.txStatus.restoreWait = () => false;
|
|
setBackRenderer(makeBackRenderer(state, views));
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
});
|
|
|
|
// Forward navigation renders as it goes. Re-rendering a second time would
|
|
// re-fetch and clobber whatever the view has in flight, so the Back path is
|
|
// the only place this happens — and it declines any view the popup does not
|
|
// render from persisted state, since the restored stack is filtered against
|
|
// that same set and such a view can only have been rendered in this page
|
|
// load.
|
|
describe("what the Back path leaves alone", () => {
|
|
test("forward navigation renders nothing by itself", () => {
|
|
reopenedOn("address", ["main"]);
|
|
pushCurrentView();
|
|
showView("send");
|
|
expect(calls).toEqual([]);
|
|
expect(state.viewStack).toEqual(["main", "address"]);
|
|
});
|
|
|
|
test("Back onto a live-session view only unhides it", () => {
|
|
reopenedOn("confirm-tx", ["main", "address", "send"]);
|
|
goBack();
|
|
expect(calls).toEqual([]);
|
|
expect(state.currentView).toBe("send");
|
|
});
|
|
|
|
test("Back renders its target exactly once", () => {
|
|
reopenedOn("settings", ["main", "address"]);
|
|
goBack();
|
|
expect(calls.filter((c) => c === "addressDetail")).toHaveLength(1);
|
|
});
|
|
});
|