diff options
author | Sebastian <sebasjm@gmail.com> | 2022-03-23 17:50:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-03-23 17:50:31 -0300 |
commit | cc18751e72435544297de4f5b5a6b318fbba9cd1 (patch) | |
tree | 2dacd7fd111f0730816027fa9e49fef7fc704dd6 | |
parent | d881f4fd258a27cc765a25c24e5fef9f86b6226f (diff) |
some DepositPage unit test
6 files changed, 250 insertions, 105 deletions
diff --git a/packages/taler-wallet-webextension/rollup.config.test.js b/packages/taler-wallet-webextension/rollup.config.test.js index 387e176bb..9a706fc66 100644 --- a/packages/taler-wallet-webextension/rollup.config.test.js +++ b/packages/taler-wallet-webextension/rollup.config.test.js @@ -25,7 +25,7 @@ function fromDir(startPath, regex) { } const tests = fromDir('./src', /.test.ts$/) - .filter(t => t === 'src/wallet/CreateManualWithdraw.test.ts') + // .filter(t => t === 'src/wallet/DepositPage.test.ts') .map(test => ({ input: test, output: { diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts index a5174bef9..0fb125147 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts @@ -40,8 +40,7 @@ describe("CreateManualWithdraw states", () => { ); if (!result.current) { - expect(result.current).not.to.be.undefined; - return; + expect.fail("hook didn't render"); } expect(result.current.noExchangeFound).equal(true) @@ -53,8 +52,7 @@ describe("CreateManualWithdraw states", () => { ); if (!result.current) { - expect(result.current).not.to.be.undefined; - return; + expect.fail("hook didn't render"); } expect(result.current.noExchangeFound).equal(true) @@ -67,8 +65,7 @@ describe("CreateManualWithdraw states", () => { ); if (!result.current) { - expect(result.current).not.to.be.undefined; - return; + expect.fail("hook didn't render"); } expect(result.current.exchange.value).equal("url1") @@ -80,8 +77,7 @@ describe("CreateManualWithdraw states", () => { ); if (!result.current) { - expect(result.current).not.to.be.undefined; - return; + expect.fail("hook didn't render"); } expect(result.current.exchange.value).equal("url2") diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index 2d5129a3d..bc4b0357a 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -55,10 +55,12 @@ export interface State { export interface TextFieldHandler { onInput: (value: string) => void; value: string; + error?: string; } export interface SelectFieldHandler { onChange: (value: string) => void; + error?: string; value: string; list: Record<string, string>; } diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx index 2e2d4cb3d..ddd4cdc90 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, Balance, parsePaytoUri } from "@gnu-taler/taler-util"; import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import { createExample } from "../test-utils"; import { View as TestedComponent } from "./DepositPage"; @@ -40,13 +40,21 @@ async function alwaysReturnFeeToOne(): Promise<DepositFee> { } export const WithEmptyAccountList = createExample(TestedComponent, { - knownBankAccounts: [], - balance: Amounts.parseOrThrow("USD:10"), + accounts: [], + balances: [ + { + available: "USD:10", + } as Balance, + ], onCalculateFee: alwaysReturnFeeToOne, }); export const WithSomeBankAccounts = createExample(TestedComponent, { - knownBankAccounts: [parsePaytoUri("payto://iban/ES8877998399652238")!], - balance: Amounts.parseOrThrow("EUR:10"), + accounts: [parsePaytoUri("payto://iban/ES8877998399652238")!], + balances: [ + { + available: "USD:10", + } as Balance, + ], onCalculateFee: alwaysReturnFeeToOne, }); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts new file mode 100644 index 000000000..8ff95fdcf --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts @@ -0,0 +1,63 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useComponentState } from "./DepositPage"; +import { expect } from "chai"; +import { mountHook } from "../test-utils"; +import { Amounts, Balance } from "@gnu-taler/taler-util"; + + +const currency = "EUR" +const feeCalculator = async () => ({ + coin: Amounts.parseOrThrow(`${currency}:1`), + wire: Amounts.parseOrThrow(`${currency}:1`), + refresh: Amounts.parseOrThrow(`${currency}:1`) +}) + +const someBalance = [{ + available: 'EUR:10' +} as Balance] + +describe("DepositPage states", () => { + it("should have status 'no-balance' when balance is empty", () => { + const { result } = mountHook(() => + useComponentState(currency, [], [], feeCalculator), + ); + + if (!result.current) { + expect.fail("hook didn't render"); + } + + expect(result.current.status).equal("no-balance") + }); + + it("should have status 'no-accounts' when balance is not empty and accounts is empty", () => { + const { result } = mountHook(() => + useComponentState(currency, [], someBalance, feeCalculator), + ); + + if (!result.current) { + expect.fail("hook didn't render"); + } + + expect(result.current.status).equal("no-accounts") + }); +});
\ No newline at end of file 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> )} |