330 lines
11 KiB
JavaScript
330 lines
11 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,
|
|
markViewRendered,
|
|
resetRenderedViews,
|
|
} = 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.
|
|
//
|
|
// A reopen is a fresh page load, so the record of what has been rendered
|
|
// starts empty — that emptiness is what makes the Back path render at all.
|
|
// Returns the view modules so a test can drive forward navigation through
|
|
// the same recorders the router renders through.
|
|
function reopenedOn(view, stack, extra) {
|
|
calls = [];
|
|
resetRenderedViews();
|
|
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 || {});
|
|
// Restoring onto a view renders it, so the reopened popup has that one
|
|
// view on the page and nothing else.
|
|
markViewRendered(view);
|
|
const views = makeViews();
|
|
setBackRenderer(makeBackRenderer(state, views));
|
|
return views;
|
|
}
|
|
|
|
// 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 the success screen renders it", () => {
|
|
reopenedOn("settings", ["main", "success-tx"], {
|
|
viewData: { hash: "0xdead" },
|
|
});
|
|
goBack();
|
|
expect(calls).toEqual(["successTx"]);
|
|
expect(state.currentView).toBe("success-tx");
|
|
});
|
|
|
|
test("Back onto the failure screen renders it", () => {
|
|
reopenedOn("settings", ["main", "error-tx"], {
|
|
viewData: { message: "execution reverted" },
|
|
});
|
|
goBack();
|
|
expect(calls).toEqual(["errorTx"]);
|
|
expect(state.currentView).toBe("error-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("the success screen with no transaction hash falls back to Home", () => {
|
|
reopenedOn("settings", ["main", "success-tx"]);
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
|
|
test("the failure screen with no message falls back to Home", () => {
|
|
reopenedOn("settings", ["main", "error-tx"]);
|
|
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, and a second render would re-fetch
|
|
// and clobber whatever the view holds — an unsaved edit, a request in
|
|
// flight. So the Back path renders only a view this page load has never
|
|
// rendered, and merely unhides every other one: the views it does not
|
|
// render from persisted state, and the views already on the page.
|
|
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);
|
|
});
|
|
|
|
test("Back onto a view this page load already rendered only unhides it", () => {
|
|
const views = reopenedOn("main", []);
|
|
pushCurrentView();
|
|
views.addressDetail.show();
|
|
pushCurrentView();
|
|
views.settings.show();
|
|
calls = [];
|
|
goBack();
|
|
expect(calls).toEqual([]);
|
|
expect(state.currentView).toBe("address");
|
|
});
|
|
|
|
// The unit mirror of the regression the browser suite pins: Settings
|
|
// reassigns its fields from persisted state on every render, so a
|
|
// re-render on the way back discards an edit the user has not saved.
|
|
test("Back onto Settings visited earlier in this page load does not re-render it", () => {
|
|
const views = reopenedOn("main", []);
|
|
pushCurrentView();
|
|
views.settings.show();
|
|
pushCurrentView();
|
|
views.settingsAddToken.show();
|
|
calls = [];
|
|
goBack();
|
|
expect(calls).toEqual([]);
|
|
expect(state.currentView).toBe("settings");
|
|
});
|
|
|
|
// Home is the deliberate exception, unchanged from the popup's
|
|
// behaviour before the router existed: it re-renders on every Back so
|
|
// the wallet list reflects what changed while the user was away.
|
|
test("Back onto Home renders it again even when it is already on the page", () => {
|
|
const views = reopenedOn("main", []);
|
|
pushCurrentView();
|
|
views.addressDetail.show();
|
|
calls = [];
|
|
goBack();
|
|
expect(calls).toEqual(["main"]);
|
|
expect(state.currentView).toBe("main");
|
|
});
|
|
});
|