aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-03-23 17:50:06 -0300
committerSebastian <sebasjm@gmail.com>2022-03-23 17:50:31 -0300
commitcc18751e72435544297de4f5b5a6b318fbba9cd1 (patch)
tree2dacd7fd111f0730816027fa9e49fef7fc704dd6
parentd881f4fd258a27cc765a25c24e5fef9f86b6226f (diff)
some DepositPage unit test
-rw-r--r--packages/taler-wallet-webextension/rollup.config.test.js2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts12
-rw-r--r--packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx18
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts63
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage.tsx258
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>
)}