From cc18751e72435544297de4f5b5a6b318fbba9cd1 Mon Sep 17 00:00:00 2001
From: Sebastian <sebasjm@gmail.com>
Date: Wed, 23 Mar 2022 17:50:06 -0300
Subject: some DepositPage unit test

---
 .../src/wallet/DepositPage.tsx                     | 258 +++++++++++++--------
 1 file changed, 167 insertions(+), 91 deletions(-)

(limited to 'packages/taler-wallet-webextension/src/wallet/DepositPage.tsx')

diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx
index 85541ab23..b420c7ebb 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx
@@ -18,12 +18,15 @@ import {
   AmountJson,
   Amounts,
   AmountString,
+  Balance,
   PaytoUri,
 } from "@gnu-taler/taler-util";
 import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
+import { saturate } from "polished";
 import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
 import { Loading } from "../components/Loading";
+import { LoadingError } from "../components/LoadingError";
 import { SelectList } from "../components/SelectList";
 import {
   Button,
@@ -37,6 +40,7 @@ import {
 import { useTranslationContext } from "../context/translation";
 import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
 import * as wxApi from "../wxApi";
+import { SelectFieldHandler, TextFieldHandler } from "./CreateManualWithdraw";
 
 interface Props {
   currency: string;
@@ -45,45 +49,46 @@ interface Props {
 }
 export function DepositPage({ currency, onCancel, onSuccess }: Props): VNode {
   const state = useAsyncAsHook(async () => {
-    const balance = await wxApi.getBalance();
-    const bs = balance.balances.filter((b) => b.available.startsWith(currency));
-    const currencyBalance =
-      bs.length === 0
-        ? Amounts.getZero(currency)
-        : Amounts.parseOrThrow(bs[0].available);
-    const knownAccounts = await wxApi.listKnownBankAccounts(currency);
-    return { accounts: knownAccounts.accounts, currencyBalance };
+    const { balances } = await wxApi.getBalance();
+    const { accounts } = await wxApi.listKnownBankAccounts(currency);
+    return { accounts, balances };
   });
 
-  const accounts =
-    state === undefined ? [] : state.hasError ? [] : state.response.accounts;
-
-  const currencyBalance =
-    state === undefined
-      ? Amounts.getZero(currency)
-      : state.hasError
-      ? Amounts.getZero(currency)
-      : state.response.currencyBalance;
+  const { i18n } = useTranslationContext();
 
-  async function doSend(account: string, amount: AmountString): Promise<void> {
+  async function doSend(p: PaytoUri, a: AmountJson): Promise<void> {
+    const account = `payto://${p.targetType}/${p.targetPath}`;
+    const amount = Amounts.stringify(a);
     await wxApi.createDepositGroup(account, amount);
     onSuccess(currency);
   }
 
   async function getFeeForAmount(
-    account: string,
-    amount: AmountString,
+    p: PaytoUri,
+    a: AmountJson,
   ): Promise<DepositFee> {
+    const account = `payto://${p.targetType}/${p.targetPath}`;
+    const amount = Amounts.stringify(a);
     return await wxApi.getFeeForDeposit(account, amount);
   }
 
-  if (accounts.length === 0) return <Loading />;
+  if (state === undefined) return <Loading />;
+
+  if (state.hasError) {
+    return (
+      <LoadingError
+        title={<i18n.Translate>Could not load deposit balance</i18n.Translate>}
+        error={state}
+      />
+    );
+  }
 
   return (
     <View
-      onCancel={onCancel}
-      knownBankAccounts={accounts}
-      balance={currencyBalance}
+      onCancel={() => onCancel(currency)}
+      currency={currency}
+      accounts={state.response.accounts}
+      balances={state.response.balances}
       onSend={doSend}
       onCalculateFee={getFeeForAmount}
     />
@@ -91,25 +96,46 @@ export function DepositPage({ currency, onCancel, onSuccess }: Props): VNode {
 }
 
 interface ViewProps {
-  knownBankAccounts: Array<PaytoUri>;
-  balance: AmountJson;
-  onCancel: (currency: string) => void;
-  onSend: (account: string, amount: AmountString) => Promise<void>;
+  accounts: Array<PaytoUri>;
+  currency: string;
+  balances: Balance[];
+  onCancel: () => void;
+  onSend: (account: PaytoUri, amount: AmountJson) => Promise<void>;
   onCalculateFee: (
-    account: string,
-    amount: AmountString,
+    account: PaytoUri,
+    amount: AmountJson,
   ) => Promise<DepositFee>;
 }
 
-export function View({
-  onCancel,
-  knownBankAccounts,
-  balance,
-  onSend,
-  onCalculateFee,
-}: ViewProps): VNode {
-  const { i18n } = useTranslationContext();
-  const accountMap = createLabelsForBankAccount(knownBankAccounts);
+type State = NoBalanceState | NoAccountsState | DepositState;
+
+interface NoBalanceState {
+  status: "no-balance";
+}
+interface NoAccountsState {
+  status: "no-accounts";
+}
+interface DepositState {
+  status: "deposit";
+  amount: TextFieldHandler;
+  account: SelectFieldHandler;
+  totalFee: AmountJson;
+  totalToDeposit: AmountJson;
+  unableToDeposit: boolean;
+  selectedAccount: PaytoUri;
+  parsedAmount: AmountJson | undefined;
+}
+
+export function useComponentState(
+  currency: string,
+  accounts: PaytoUri[],
+  balances: Balance[],
+  onCalculateFee: (
+    account: PaytoUri,
+    amount: AmountJson,
+  ) => Promise<DepositFee>,
+): State {
+  const accountMap = createLabelsForBankAccount(accounts);
   const [accountIdx, setAccountIdx] = useState(0);
   const [amount, setAmount] = useState<number | undefined>(undefined);
   const [fee, setFee] = useState<DepositFee | undefined>(undefined);
@@ -117,35 +143,108 @@ export function View({
     setAmount(num);
     setFee(undefined);
   }
-  const currency = balance.currency;
-  const amountStr: AmountString = `${currency}:${amount}`;
-  const feeSum =
+
+  const selectedAmountSTR: AmountString = `${currency}:${amount}`;
+  const totalFee =
     fee !== undefined
       ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount
       : Amounts.getZero(currency);
 
-  const account = knownBankAccounts.length
-    ? knownBankAccounts[accountIdx]
-    : undefined;
-  const accountURI = !account
-    ? ""
-    : `payto://${account.targetType}/${account.targetPath}`;
+  const selectedAccount = accounts.length ? accounts[accountIdx] : undefined;
+
+  const parsedAmount =
+    amount === undefined ? undefined : Amounts.parse(selectedAmountSTR);
 
   useEffect(() => {
-    if (amount === undefined) return;
-    onCalculateFee(accountURI, amountStr).then((result) => {
+    if (selectedAccount === undefined || parsedAmount === undefined) return;
+    onCalculateFee(selectedAccount, parsedAmount).then((result) => {
       setFee(result);
     });
   }, [amount]);
 
-  if (!balance) {
+  const bs = balances.filter((b) => b.available.startsWith(currency));
+  const balance =
+    bs.length > 0
+      ? Amounts.parseOrThrow(bs[0].available)
+      : Amounts.getZero(currency);
+
+  const isDirty = amount !== 0;
+  const amountError = !isDirty
+    ? undefined
+    : !parsedAmount
+    ? "Invalid amount"
+    : Amounts.cmp(balance, parsedAmount) === -1
+    ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
+    : undefined;
+
+  const totalToDeposit = parsedAmount
+    ? Amounts.sub(parsedAmount, totalFee).amount
+    : Amounts.getZero(currency);
+
+  const unableToDeposit =
+    Amounts.isZero(totalToDeposit) ||
+    fee === undefined ||
+    amountError !== undefined;
+
+  if (Amounts.isZero(balance)) {
+    return {
+      status: "no-balance",
+    };
+  }
+
+  if (!accounts || !accounts.length || !selectedAccount) {
+    return {
+      status: "no-accounts",
+    };
+  }
+
+  return {
+    status: "deposit",
+    amount: {
+      value: String(amount),
+      onInput: (e) => {
+        const num = parseFloat(e);
+        if (!Number.isNaN(num)) {
+          updateAmount(num);
+        } else {
+          updateAmount(undefined);
+          setFee(undefined);
+        }
+      },
+      error: amountError,
+    },
+    account: {
+      list: accountMap,
+      value: String(accountIdx),
+      onChange: (s) => setAccountIdx(parseInt(s, 10)),
+    },
+    totalFee,
+    totalToDeposit,
+    unableToDeposit,
+    selectedAccount,
+    parsedAmount,
+  };
+}
+
+export function View({
+  onCancel,
+  currency,
+  accounts,
+  balances,
+  onSend,
+  onCalculateFee,
+}: ViewProps): VNode {
+  const { i18n } = useTranslationContext();
+  const state = useComponentState(currency, accounts, balances, onCalculateFee);
+
+  if (state.status === "no-balance") {
     return (
       <div>
         <i18n.Translate>no balance</i18n.Translate>
       </div>
     );
   }
-  if (!knownBankAccounts || !knownBankAccounts.length) {
+  if (state.status === "no-accounts") {
     return (
       <Fragment>
         <WarningBox>
@@ -159,30 +258,13 @@ export function View({
           </ButtonBoxWarning>
         </WarningBox>
         <footer>
-          <Button onClick={() => onCancel(currency)}>
+          <Button onClick={onCancel}>
             <i18n.Translate>Cancel</i18n.Translate>
           </Button>
         </footer>
       </Fragment>
     );
   }
-  const parsedAmount =
-    amount === undefined ? undefined : Amounts.parse(amountStr);
-  const isDirty = amount !== 0;
-  const error = !isDirty
-    ? undefined
-    : !parsedAmount
-    ? "Invalid amount"
-    : Amounts.cmp(balance, parsedAmount) === -1
-    ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
-    : undefined;
-
-  const totalToDeposit = parsedAmount
-    ? Amounts.sub(parsedAmount, feeSum).amount
-    : Amounts.getZero(currency);
-
-  const unableToDeposit =
-    Amounts.isZero(totalToDeposit) || fee === undefined || error !== undefined;
 
   return (
     <Fragment>
@@ -193,13 +275,13 @@ export function View({
         <Input>
           <SelectList
             label={<i18n.Translate>Bank account IBAN number</i18n.Translate>}
-            list={accountMap}
+            list={state.account.list}
             name="account"
-            value={String(accountIdx)}
-            onChange={(s) => setAccountIdx(parseInt(s, 10))}
+            value={state.account.value}
+            onChange={state.account.onChange}
           />
         </Input>
-        <InputWithLabel invalid={!!error}>
+        <InputWithLabel invalid={!!state.amount.error}>
           <label>
             <i18n.Translate>Amount</i18n.Translate>
           </label>
@@ -207,19 +289,11 @@ export function View({
             <span>{currency}</span>
             <input
               type="number"
-              value={amount}
-              onInput={(e) => {
-                const num = parseFloat(e.currentTarget.value);
-                if (!Number.isNaN(num)) {
-                  updateAmount(num);
-                } else {
-                  updateAmount(undefined);
-                  setFee(undefined);
-                }
-              }}
+              value={state.amount.value}
+              onInput={(e) => state.amount.onInput(e.currentTarget.value)}
             />
           </div>
-          {error && <ErrorText>{error}</ErrorText>}
+          {state.amount.error && <ErrorText>{state.amount.error}</ErrorText>}
         </InputWithLabel>
         {
           <Fragment>
@@ -232,7 +306,7 @@ export function View({
                 <input
                   type="number"
                   disabled
-                  value={Amounts.stringifyValue(feeSum)}
+                  value={Amounts.stringifyValue(state.totalFee)}
                 />
               </div>
             </InputWithLabel>
@@ -246,7 +320,7 @@ export function View({
                 <input
                   type="number"
                   disabled
-                  value={Amounts.stringifyValue(totalToDeposit)}
+                  value={Amounts.stringifyValue(state.totalToDeposit)}
                 />
               </div>
             </InputWithLabel>
@@ -254,17 +328,19 @@ export function View({
         }
       </section>
       <footer>
-        <Button onClick={() => onCancel(currency)}>
+        <Button onClick={onCancel}>
           <i18n.Translate>Cancel</i18n.Translate>
         </Button>
-        {unableToDeposit ? (
+        {state.unableToDeposit ? (
           <ButtonPrimary disabled>
             <i18n.Translate>Deposit</i18n.Translate>
           </ButtonPrimary>
         ) : (
-          <ButtonPrimary onClick={() => onSend(accountURI, amountStr)}>
+          <ButtonPrimary
+            onClick={() => onSend(state.selectedAccount, state.parsedAmount!)}
+          >
             <i18n.Translate>
-              Deposit {Amounts.stringifyValue(totalToDeposit)} {currency}
+              Deposit {Amounts.stringifyValue(state.totalToDeposit)} {currency}
             </i18n.Translate>
           </ButtonPrimary>
         )}
-- 
cgit v1.2.3

