[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] 02/02: Implement public histories tabs.
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] 02/02: Implement public histories tabs. |
Date: |
Sat, 19 Feb 2022 10:53:45 +0100 |
This is an automated email from the git hooks/post-receive script.
ms pushed a commit to branch master
in repository merchant-backoffice.
commit 0a1be293676f0c4a46d81b4f19d0c74ece749045
Author: MS <ms@taler.net>
AuthorDate: Sat Feb 19 10:53:32 2022 +0100
Implement public histories tabs.
---
packages/bank/src/pages/home/index.tsx | 60 +++++-------
packages/bank/tests/__tests__/homepage.js | 152 ++++++++++++++++++------------
2 files changed, 115 insertions(+), 97 deletions(-)
diff --git a/packages/bank/src/pages/home/index.tsx
b/packages/bank/src/pages/home/index.tsx
index 7318cf3..32aed84 100644
--- a/packages/bank/src/pages/home/index.tsx
+++ b/packages/bank/src/pages/home/index.tsx
@@ -509,18 +509,8 @@ async function createWithdrawalCall(
try {
const { username, password } = backendState;
let headers = prepareHeaders(username, password);
- /**
- * NOTE: tests show that when a same object is being
- * POSTed, caching might prevent same requests from being
- * made. Hence, trying to POST twice the same amount might
- * get silently ignored.
- *
- * headers.append("cache-control", "no-store");
- * headers.append("cache-control", "no-cache");
- * headers.append("pragma", "no-cache");
- * */
- // Backend URL must have been stored _with_ a final slash.
+ // Let bank generate withdraw URI:
const url = new URL(
`access-api/accounts/${backendState.username}/withdrawals`,
backendState.url
@@ -562,8 +552,7 @@ async function loginCall(
req: CredentialsRequestType,
/**
* FIXME: figure out if the two following
- * functions can be retrieved somewhat from
- * the state.
+ * functions can be retrieved from the state.
*/
backendStateSetter: StateUpdater<BackendStateTypeOpt>,
pageStateSetter: StateUpdater<PageStateType>
@@ -572,10 +561,7 @@ async function loginCall(
/**
* Optimistically setting the state as 'logged in', and
* let the Account component request the balance to check
- * whether the credentials are valid. If not, then Account
- * will switch the state back to 'logged out' (and the user
- * will see again the login/register form).
- */
+ * whether the credentials are valid. */
pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
let baseUrl = getRootPath();
if (!baseUrl.endsWith('/')) {
@@ -635,7 +621,7 @@ async function registrationCall(
return;
}
if (!res.ok) {
- const errorRaw = JSON.stringify(await res.json());
+ const errorRaw = await res.text();
console.log(`New registration gave response error (${res.status})`,
errorRaw);
pageStateSetter((prevState) => ({
...prevState,
@@ -717,7 +703,7 @@ function TalerWithdrawal(Props: any): VNode {
submitAmount = submitAmount.replace(",", "."); // tolerating comma
instead of point.
const re = RegExp(amountRegex)
if (!re.test(submitAmount)) {
- console.log("Not withdrawing invalid amount", submitAmount);
+ console.log(`Not withdrawing invalid amount '${submitAmount}'.`);
return;
}
console.log("Valid amount", submitAmount);
@@ -842,9 +828,11 @@ function Transactions(Props: any): VNode {
}
}
}
- if (!data) return <p>"Transactions page loading..."</p>;
-
- console.log("History data", data);
+ if (!data) {
+ console.log(`History data of ${accountLabel} not arrived`);
+ return <p>"Transactions page loading..."</p>;
+ }
+ console.log(`History data of ${accountLabel}`, data);
return <ul>{
data.transactions.map(function(item: any) {
const sign = item.direction == "DBIT" ? "-" : "";
@@ -934,6 +922,7 @@ function Account(Props: any): VNode {
* the outcome.
*/
if (talerWithdrawUri) {
+ console.log(`Showing withdraw URI: ${talerWithdrawUri}`);
return (<Fragment>
<p>Scan the following QR code, and then confirm!</p>
<div>{QR({text: talerWithdrawUri})}</div>
@@ -1006,6 +995,7 @@ function SWRWithoutCredentials(Props: any): VNode {
* Show histories of public accounts.
*/
function PublicHistories(Props: any): VNode {
+ const [showAccount, setShowAccount] = useState<string | undefined>();
const { data, error } = useSWR("access-api/public-accounts")
if (typeof error !== "undefined") {
console.log("account error", error);
@@ -1019,14 +1009,14 @@ function PublicHistories(Props: any): VNode {
}
}
if (!data) return <p>Waiting public accounts list...</p>
- var txs = {};
+ var txs: any = {};
var accountsBar = [];
+
+ // Ask first story of all the public accounts.
for (const account of data.publicAccounts) {
console.log("Asking transactions for", account.accountLabel)
accountsBar.push(
- <li>
- <a onClick={() => <PublicAccounts showAccount={account.accountLabel}
/>}>{account.accountLabel}</a>
- </li>
+ <li><a onClick={() =>
setShowAccount(account.accountLabel)}>{account.accountLabel}</a></li>
);
txs[account.accountLabel] =
<div>{account.accountLabel} latest transactions:
@@ -1037,14 +1027,13 @@ function PublicHistories(Props: any): VNode {
* Show the account specified in the props, or just one
* from the list if that's not given.
*/
- var showAccount = Props.showAccount
- if (typeof showAccount === "undefined" && keys(txs).length > 0) {
- showAccount = keys(txs).pop()
- }
-
+ if (typeof showAccount === "undefined" && Object.keys(txs).length > 0)
+ setShowAccount(Object.keys(txs).pop());
+ console.log(`Public history tab: ${showAccount}`);
return <Fragment>
<ul>{accountsBar}</ul>
{typeof showAccount !== "undefined" ? txs[showAccount] : <p>No public
transactions found.</p>}
+ {Props.children}
</Fragment>;
}
@@ -1070,10 +1059,11 @@ export function BankHome(): VNode {
if (pageState.showPublicHistories) {
return (<SWRWithoutCredentials baseUrl={getRootPath()}>
- <PublicHistories />
- <a onClick={() => {
- pageStateSetter((prevState: PageStateType) =>
- ({...prevState, showPublicHistories: false}))}}>Go back</a>
+ <PublicHistories>
+ <a onClick={() => {
+ pageStateSetter((prevState: PageStateType) =>
+ ({...prevState, showPublicHistories: false}))}}>Go back</a>
+ </PublicHistories>
</SWRWithoutCredentials>);
}
diff --git a/packages/bank/tests/__tests__/homepage.js
b/packages/bank/tests/__tests__/homepage.js
index 039bab1..9ea0ed4 100644
--- a/packages/bank/tests/__tests__/homepage.js
+++ b/packages/bank/tests/__tests__/homepage.js
@@ -26,35 +26,21 @@ beforeAll(() => {
global.Storage.prototype.setItem = jest.fn((key, value) => {})
})
-/**
- * Insert username and password into the registration
- * form and returns the submit button. NOTE: the username
- * must be given always fresh, as it acts as a SWR key and
- * therefore might prevent calls from being made, because of
- * caching reasons. That is not a problem per-se but can
- * disrupt ".toHaveLastBeenCalledWith()"-like asserts.
- *
- * Return the username and the submit button.
- */
function fillCredentialsForm() {
const username = Math.random().toString().substring(2);
const u = screen.getByPlaceholderText("username");
const p = screen.getByPlaceholderText("password");
fireEvent.input(u, {target: {value: username}})
fireEvent.input(p, {target: {value: "bar"}})
- const signupButton = screen.getByText("Sign up");
- const signinButton = screen.getByText("Sign in");
+ const signinButton = screen.getByText("Login");
return {
username: username,
- signupButton: signupButton,
signinButton: signinButton
};
}
fetchMock.enableMocks();
-function signUp(context) {
- render(<BankHome />);
- const { username, signupButton } = fillCredentialsForm();
+function mockSuccessLoginOrRegistration() {
fetch.once("{}", {
status: 200
}).once(JSON.stringify({
@@ -64,13 +50,31 @@ function signUp(context) {
},
paytoUri: "payto://iban/123/ABC"
}))
- fireEvent.click(signupButton);
+}
+
+/**
+ * Render homepage -> navigate to register page -> submit registration.
+ * 'webMock' is called before submission to mock the server response
+ */
+function signUp(context, webMock) {
+ render(<BankHome />);
+ const registerPage = screen.getByText("Register!");
+ fireEvent.click(registerPage);
+ const username = Math.random().toString().substring(2);
+ const u = screen.getByPlaceholderText("username");
+ const p = screen.getByPlaceholderText("password");
+ fireEvent.input(u, {target: {value: username}})
+ fireEvent.input(p, {target: {value: "bar"}})
+ const registerButton = screen.getByText("Register");
+ webMock();
+ fireEvent.click(registerButton);
context.username = username;
+ return context;
}
describe("wire transfer", () => {
beforeEach(() => {
- signUp({}); // context unused
+ signUp({}, mockSuccessLoginOrRegistration); // context unused
})
test("Wire transfer success", async () => {
const transferButton = screen.getByText("Create wire transfer");
@@ -98,14 +102,16 @@ describe("withdraw", () => {
cleanup();
})
+
+ let context = {};
// Register and land on the profile page.
beforeEach(() => {
- signUp(context);
+ context = signUp(context, mockSuccessLoginOrRegistration);
})
- let context = {username: null};
-
test("network failure before withdrawal creation", async () => {
+ const a = screen.getAllByPlaceholderText("amount")[0];
+ fireEvent.input(a, {target: {value: "10"}});
let withdrawButton = screen.getByText("Charge Taler wallet");
// mock network failure.
fetch.mockReject("API is down");
@@ -114,6 +120,8 @@ describe("withdraw", () => {
})
test("HTTP response error upon withdrawal creation", async () => {
+ const a = screen.getAllByPlaceholderText("amount")[0];
+ fireEvent.input(a, {target: {value: "10,0"}});
let withdrawButton = screen.getByText("Charge Taler wallet");
fetch.once("{}", {status: 404});
fireEvent.click(withdrawButton);
@@ -121,6 +129,8 @@ describe("withdraw", () => {
})
test("Abort withdrawal", async () => {
+ const a = screen.getAllByPlaceholderText("amount")[0];
+ fireEvent.input(a, {target: {value: "10,0"}});
let withdrawButton = screen.getByText("Charge Taler wallet");
fetch.once(JSON.stringify({
taler_withdraw_uri: "taler://withdraw/foo",
@@ -143,7 +153,9 @@ describe("withdraw", () => {
})
test("Successful withdrawal creation and confirmation", async () => {
- let withdrawButton = screen.getByText("Charge Taler wallet");
+ const a = screen.getAllByPlaceholderText("amount")[0];
+ fireEvent.input(a, {target: {value: "10,0"}});
+ let withdrawButton = await screen.findByText("Charge Taler wallet");
fetch.once(JSON.stringify({
taler_withdraw_uri: "taler://withdraw/foo",
withdrawal_id: "foo"
@@ -151,12 +163,11 @@ describe("withdraw", () => {
/**
* After triggering a withdrawal, check if the taler://withdraw URI
* rendered, and confirm if so. Lastly, check that a success message
- * appeared on the screen.
- */
+ * appeared on the screen. */
fireEvent.click(withdrawButton);
expect(fetch).toHaveBeenCalledWith(
`http://localhost/demobanks/default/access-api/accounts/${context.username}/withdrawals`,
- expect.objectContaining({body: JSON.stringify({amount: "EUR:5"})})
+ expect.objectContaining({body: JSON.stringify({amount: "EUR:10.0"})})
)
// assume wallet POSTed the payment details.
const confirmButton = await screen.findByText("confirm withdrawal",
{exact: false})
@@ -221,7 +232,11 @@ describe("home page", () => {
cleanup();
})
test("public histories", async () => {
- // Mock list of public accounts.
+ render(<BankHome />);
+ /**
+ * Mock list of public accounts. 'bar' is
+ * the shown account, since it occupies the last
+ * position (and SPA picks it via the 'pop()' method) */
fetch.once(JSON.stringify({
"publicAccounts" : [ {
"balance" : "EUR:1",
@@ -281,53 +296,80 @@ describe("home page", () => {
date: "2000-01-01"
}]
}))
- render(<BankHome />);
+
+ // Navigate to dedicate public histories page.
+ const publicTxsPage = screen.getByText("transactions");
+ fireEvent.click(publicTxsPage);
+
/**
- * Check that transacions data appears on the page.
+ * Check that transactions data appears on the page.
*/
await screen.findByText("reimbursement", {exact: false});
- await screen.findByText("refund", {exact: false});
await screen.findByText("bonus", {exact: false});
- await screen.findByText("donation", {exact: false});
-
+ /**
+ * The transactions below should not appear, because only
+ * one public account renders.
+ */
+ await waitFor(() => expect(
+ screen.queryByText("refund", {exact: false})).not.toBeInTheDocument());
+ await waitFor(() => expect(
+ screen.queryByText("donation", {exact: false})).not.toBeInTheDocument());
+ /**
+ * First HTTP mock:
+ */
await expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/public-accounts"
)
+ /**
+ * Only expecting this request (second mock), as SWR doesn't let
+ * the unshown history request to the backend:
+ */
await expect(fetch).toHaveBeenCalledWith(
-
"http://localhost/demobanks/default/access-api/accounts/foo/transactions?page=0"
+
"http://localhost/demobanks/default/access-api/accounts/bar/transactions?page=0"
)
+ /**
+ * Switch tab:
+ */
+ let fooTab = await screen.findByText("foo", {exact: false});
+ fireEvent.click(fooTab);
+ /**
+ * Last two HTTP mocks should render now:
+ */
+ await screen.findByText("refund", {exact: false});
+ await screen.findByText("donation", {exact: false});
+
+ // Expect SWR to have requested 'foo' history
+ // (consuming the last HTTP mock):
await expect(fetch).toHaveBeenCalledWith(
-
"http://localhost/demobanks/default/access-api/accounts/bar/transactions?page=0"
+
"http://localhost/demobanks/default/access-api/accounts/foo/transactions?page=0"
)
+ let backButton = await screen.findByText("Go back", {exact: false});
+ fireEvent.click(backButton);
+ await waitFor(() => expect(
+ screen.queryByText("donation", {exact: false})).not.toBeInTheDocument());
+ await screen.findByText("welcome to eufin bank", {exact: false})
})
// check page informs about the current balance
// after a successful registration.
test("new registration response error 404", async () => {
- render(<BankHome />);
- let { username, signupButton } = fillCredentialsForm();
- fetch.mockResponseOnce("Not found", {status: 404})
- fireEvent.click(signupButton);
+ var context = signUp({}, () => fetch.mockResponseOnce("Not found",
{status: 404}));
await screen.findByText("has a problem", {exact: false});
expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/testing/register",
expect.objectContaining(
- {body: JSON.stringify({username: username, password: "bar"}), method:
"POST"},
+ {body: JSON.stringify({username: context.username, password: "bar"}),
method: "POST"},
))
})
test("registration network failure", async () => {
- render(<BankHome />);
- const { username, signupButton } = fillCredentialsForm();
- // Mocking network failure.
- fetch.mockReject("API is down");
- fireEvent.click(signupButton);
+ let context = signUp({}, ()=>fetch.mockReject("API is down"));
await screen.findByText("has a problem", {exact: false});
expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/testing/register",
expect.objectContaining(
- {body: JSON.stringify({username: username, password: "bar"}), method:
"POST"}
+ {body: JSON.stringify({username: context.username, password: "bar"}),
method: "POST"}
))
})
@@ -391,7 +433,7 @@ describe("home page", () => {
`http://localhost/demobanks/default/access-api/accounts/${username}`,
expect.anything()
)
- await screen.findByText("balance is EUR:10", {exact: false})
+ await screen.findByText("balance is 10 EUR", {exact: false})
// The two transactions in the history mocked above.
await screen.findByText("refund", {exact: false})
await screen.findByText("donation", {exact: false})
@@ -402,26 +444,12 @@ describe("home page", () => {
})
test("registration success", async () => {
- render(<BankHome />);
- const { username, signupButton } = fillCredentialsForm();
- /**
- * Mock successful registration and balance request.
- */
- fetch.once("{}", {
- status: 200
- }).once(JSON.stringify({
- balance: {
- amount: "EUR:10",
- credit_debit_indicator: "credit"
- },
- paytoUri: "payto://iban/123/ABC"
- }))
- fireEvent.click(signupButton);
+ let context = signUp({}, mockSuccessLoginOrRegistration);
/**
* Tests that a balance is shown after the successful
* registration.
*/
- await screen.findByText("balance is EUR:10", {exact: false})
+ await screen.findByText("balance is 10 EUR", {exact: false})
/**
* The expectation below tests whether the account
* balance was requested after the successful registration.
@@ -431,7 +459,7 @@ describe("home page", () => {
expect.anything() // no need to match auth headers.
)
expect(fetch).toHaveBeenCalledWith(
- `http://localhost/demobanks/default/access-api/accounts/${username}`,
+
`http://localhost/demobanks/default/access-api/accounts/${context.username}`,
expect.anything() // no need to match auth headers.
)
})
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.