refactoring transaction component to standard component with test and examples

This commit is contained in:
Sebastian 2022-12-14 15:35:28 -03:00
parent d0dd7a155f
commit 8d8d71807d
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
16 changed files with 587 additions and 127 deletions

View File

@ -44,7 +44,29 @@ const preactCompatPlugin = {
},
};
const entryPoints = ["src/index.tsx", "src/stories.tsx"];
function getFilesInDirectory(startPath, regex) {
if (!fs.existsSync(startPath)) {
return;
}
const files = fs.readdirSync(startPath);
const result = files.flatMap(file => {
const filename = path.join(startPath, file);
const stat = fs.lstatSync(filename);
if (stat.isDirectory()) {
return getFilesInDirectory(filename, regex);
}
else if (regex.test(filename)) {
return filename
}
}).filter(x => !!x)
return result
}
const allTestFiles = getFilesInDirectory(path.join(BASE, 'src'), /.test.ts$/)
const entryPoints = ["src/index.tsx", "src/stories.tsx", ...allTestFiles];
let GIT_ROOT = BASE;
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
@ -128,6 +150,7 @@ export const buildConfig = {
sourcemap: true,
jsxFactory: "h",
jsxFragment: "Fragment",
external: ["async_hooks"],
define: {
__VERSION__: `"${_package.version}"`,
__GIT_HASH__: `"${GIT_HASH}"`,

View File

@ -15,9 +15,9 @@
*/
import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { HookError, utils } from "@gnu-taler/web-util/lib/index.browser";
//import { compose, StateViewMap } from "../../utils/index.js";
//import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js";
@ -47,14 +47,13 @@ export namespace State {
}
}
const viewMapping: StateViewMap<State> = {
const viewMapping: utils.StateViewMap<State> = {
loading: Loading,
"loading-error": LoadingUriView,
ready: ReadyView,
};
export const ComponentName = compose(
"ComponentName",
(p: Props) => useComponentState(p, wxApi),
export const ComponentName = utils.compose(
(p: Props) => useComponentState(p),
viewMapping,
);

View File

@ -14,10 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { wxApi } from "../../wxApi.js";
// import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
export function useComponentState({ p }: Props, api: typeof wxApi): State {
export function useComponentState({ p }: Props): State {
return {
status: "ready",
error: undefined,

View File

@ -19,11 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from "../../test-utils.js";
import { tests } from "@gnu-taler/web-util/lib/index.browser";
import { ReadyView } from "./views.js";
export default {
title: "example",
};
export const Ready = createExample(ReadyView, {});
export const Ready = tests.createExample(ReadyView, {});

View File

@ -15,18 +15,16 @@
*/
import { h, VNode } from "preact";
import { LoadingError } from "../../components/LoadingError.js";
import { useTranslationContext } from "../../context/translation.js";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
return (
<LoadingError
title={<i18n.Translate>Could not load</i18n.Translate>}
error={error}
/>
<div>
<i18n.Translate>Could not load</i18n.Translate>
</div>
);
}

View File

@ -0,0 +1,71 @@
/*
This file is part of GNU Taler
(C) 2022 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/>
*/
import { Loading } from "../Loading.js";
import { HookError, utils } from "@gnu-taler/web-util/lib/index.browser";
// import { compose, StateViewMap } from "../../utils/index.js";
// import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js";
import { AbsoluteTime, AmountJson } from "@gnu-taler/taler-util";
export interface Props {
pageNumber: number;
accountLabel: string;
balanceValue?: string;
}
export type State = State.Loading | State.LoadingUriError | State.Ready;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
}
export interface LoadingUriError {
status: "loading-error";
error: HookError;
}
export interface BaseInfo {
error: undefined;
}
export interface Ready extends BaseInfo {
status: "ready";
error: undefined;
transactions: Transaction[];
}
}
export interface Transaction {
negative: boolean;
counterpart: string;
when: AbsoluteTime;
amount: AmountJson;
subject: string;
}
const viewMapping: utils.StateViewMap<State> = {
loading: Loading,
"loading-error": LoadingUriView,
ready: ReadyView,
};
export const Transactions = utils.compose(
(p: Props) => useComponentState(p),
viewMapping,
);

View File

@ -0,0 +1,133 @@
/*
This file is part of GNU Taler
(C) 2022 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/>
*/
import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
import { parse } from "date-fns";
import { useEffect } from "preact/hooks";
import useSWR from "swr";
import { Props, State } from "./index.js";
export function useComponentState({ accountLabel, pageNumber, balanceValue }: Props): State {
const { data, error, mutate } = useSWR(
`access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`,
);
useEffect(() => {
if (balanceValue) {
mutate();
}
}, [balanceValue ?? ""]);
if (error) {
switch (error.status) {
case 404:
return {
status: "loading-error",
error: {
hasError: true,
operational: false,
message: `Transactions page ${pageNumber} was not found.`
}
}
case 401:
return {
status: "loading-error",
error: {
hasError: true,
operational: false,
message: "Wrong credentials given."
}
}
default:
return {
status: "loading-error",
error: {
hasError: true,
operational: false,
message: `Transaction page ${pageNumber} could not be retrieved.`
} as any
}
}
}
if (!data) {
return {
status: "loading",
error: undefined
}
}
const transactions = data.transactions.map((item: unknown) => {
if (!item || typeof item !== "object" ||
!("direction" in item) ||
!("creditorIban" in item) ||
!("debtorIban" in item) ||
!("date" in item) ||
!("subject" in item) ||
!("currency" in item) ||
!("amount" in item)
) {
//not valid
return;
}
const anyItem = item as any;
if (
!(typeof anyItem.creditorIban === 'string') ||
!(typeof anyItem.debtorIban === 'string') ||
!(typeof anyItem.date === 'string') ||
!(typeof anyItem.subject === 'string') ||
!(typeof anyItem.currency === 'string') ||
!(typeof anyItem.amount === 'string')
) {
return;
}
const negative = anyItem.direction === "DBIT";
const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban;
// Pattern:
//
// DD/MM YYYY subject -5 EUR
// DD/MM YYYY subject 5 EUR
const dateRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{1,2})/;
const dateParse = dateRegex.exec(anyItem.date);
const dateStr =
dateParse !== null
? `${dateParse[3]}/${dateParse[2]} ${dateParse[1]}`
: undefined;
const date = parse(dateStr ?? "", "dd/MM yyyy", new Date())
const when: AbsoluteTime = {
t_ms: date.getTime()
}
const amount = Amounts.parseOrThrow(`${anyItem.currency}:${anyItem.amount}`);
const subject = anyItem.subject;
return {
negative,
counterpart,
when,
amount,
subject,
}
});
return {
status: "ready",
error: undefined,
transactions,
};
}

View File

@ -0,0 +1,45 @@
/*
This file is part of GNU Taler
(C) 2022 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 { tests } from "@gnu-taler/web-util/lib/index.browser";
import { ReadyView } from "./views.js";
export default {
title: "transaction list",
};
export const Ready = tests.createExample(ReadyView, {
transactions: [
{
amount: {
currency: "USD",
fraction: 0,
value: 1,
},
counterpart: "ASD",
negative: false,
subject: "Some",
when: {
t_ms: new Date().getTime(),
},
},
],
});

View File

@ -0,0 +1,174 @@
/*
This file is part of GNU Taler
(C) 2022 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 { tests } from "@gnu-taler/web-util/lib/index.browser";
import { SwrMockEnvironment } from "@gnu-taler/web-util/lib/tests/swr";
import { expect } from "chai";
import { TRANSACTION_API_EXAMPLE } from "../../endpoints.js";
import { Props } from "./index.js";
import { useComponentState } from "./state.js";
describe("Transaction states", () => {
it("should query backend and render transactions", async () => {
const env = new SwrMockEnvironment();
const props: Props = {
accountLabel: "myAccount",
pageNumber: 0
}
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
response: {
"transactions": [
{
"creditorIban": "DE159593",
"creditorBic": "SANDBOXX",
"creditorName": "exchange company",
"debtorIban": "DE118695",
"debtorBic": "SANDBOXX",
"debtorName": "Name unknown",
"amount": "1",
"currency": "KUDOS",
"subject": "Taler Withdrawal N588V8XE9TR49HKAXFQ20P0EQ0EYW2AC9NNANV8ZP5P59N6N0410",
"date": "2022-12-12Z",
"uid": "8PPFR9EM",
"direction": "DBIT",
"pmtInfId": null,
"msgId": null
},
{
"creditorIban": "DE159593",
"creditorBic": "SANDBOXX",
"creditorName": "exchange company",
"debtorIban": "DE118695",
"debtorBic": "SANDBOXX",
"debtorName": "Name unknown",
"amount": "5.00",
"currency": "KUDOS",
"subject": "HNEWWT679TQC5P1BVXJS48FX9NW18FWM6PTK2N80Z8GVT0ACGNK0",
"date": "2022-12-07Z",
"uid": "7FZJC3RJ",
"direction": "DBIT",
"pmtInfId": null,
"msgId": null
},
{
"creditorIban": "DE118695",
"creditorBic": "SANDBOXX",
"creditorName": "Name unknown",
"debtorIban": "DE579516",
"debtorBic": "SANDBOXX",
"debtorName": "The Bank",
"amount": "100",
"currency": "KUDOS",
"subject": "Sign-up bonus",
"date": "2022-12-07Z",
"uid": "I31A06J8",
"direction": "CRDT",
"pmtInfId": null,
"msgId": null
}
]
}
});
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
({ status, error }) => {
expect(status).equals("ready");
expect(error).undefined;
},
], env.buildTestingContext())
expect(hookBehavior).deep.eq({ result: "ok" })
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" })
});
it("should show error message on not found", async () => {
const env = new SwrMockEnvironment();
const props: Props = {
accountLabel: "myAccount",
pageNumber: 0
}
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
({ status, error }) => {
expect(status).equals("loading-error");
expect(error).deep.eq({
hasError: true,
operational: false,
message: "Transactions page 0 was not found."
});
},
], env.buildTestingContext())
expect(hookBehavior).deep.eq({ result: "ok" })
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" })
});
it("should show error message on server error", async () => {
const env = new SwrMockEnvironment(false);
const props: Props = {
accountLabel: "myAccount",
pageNumber: 0
}
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
({ status, error }) => {
expect(status).equals("loading-error");
expect(error).deep.equal({
hasError: true,
operational: false,
message: "Transaction page 0 could not be retrieved."
});
},
], env.buildTestingContext())
expect(hookBehavior).deep.eq({ result: "ok" })
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" })
});
});

View File

@ -0,0 +1,68 @@
/*
This file is part of GNU Taler
(C) 2022 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/>
*/
import { h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { State } from "./index.js";
import { format } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
return (
<div>
<i18n.Translate>Could not load</i18n.Translate>
</div>
);
}
export function ReadyView({ transactions }: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
<div class="results">
<table class="pure-table pure-table-striped">
<thead>
<tr>
<th>{i18n.str`Date`}</th>
<th>{i18n.str`Amount`}</th>
<th>{i18n.str`Counterpart`}</th>
<th>{i18n.str`Subject`}</th>
</tr>
</thead>
<tbody>
{transactions.map((item, idx) => {
return (
<tr key={idx}>
<td>
{item.when.t_ms === "never"
? "never"
: format(item.when.t_ms, "dd/MM/yyyy")}
</td>
<td>
{item.negative ? "-" : ""}
{Amounts.stringifyValue(item.amount)} {item.amount.currency}
</td>
<td>{item.counterpart}</td>
<td>{item.subject}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}

View File

@ -0,0 +1,17 @@
/*
This file is part of GNU Taler
(C) 2022 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/>
*/
export * as tx from "./Transactions/stories.js";

View File

@ -0,0 +1,37 @@
/*
This file is part of GNU Taler
(C) 2022 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)
*/
export const TRANSACTION_API_EXAMPLE = {
LIST_FIRST_PAGE: {
method: "get" as const,
url: "access-api/accounts/myAccount/transactions?page=0",
},
LIST_ERROR: {
method: "get" as const,
url: "access-api/accounts/myAccount/transactions?page=0",
code: 500
},
LIST_NOT_FOUND: {
method: "get" as const,
url: "access-api/accounts/myAccount/transactions?page=0",
code: 404
}
}

View File

@ -27,7 +27,7 @@ import { getIbanFromPayto, prepareHeaders } from "../utils.js";
import { BankFrame } from "./BankFrame.js";
import { LoginForm } from "./LoginForm.js";
import { PaymentOptions } from "./PaymentOptions.js";
import { Transactions } from "./Transactions.js";
import { Transactions } from "../components/Transactions/index.js";
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
export function AccountPage(): VNode {

View File

@ -24,7 +24,7 @@ import { PageStateType, usePageContext } from "../context/pageState.js";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { getBankBackendBaseUrl } from "../utils.js";
import { BankFrame } from "./BankFrame.js";
import { Transactions } from "./Transactions.js";
import { Transactions } from "../components/Transactions/index.js";
const logger = new Logger("PublicHistoriesPage");

View File

@ -1,106 +0,0 @@
/*
This file is part of GNU Taler
(C) 2022 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/>
*/
import { Logger } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useEffect } from "preact/hooks";
import useSWR from "swr";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
const logger = new Logger("Transactions");
/**
* Show one page of transactions.
*/
export function Transactions({
pageNumber,
accountLabel,
balanceValue,
}: {
pageNumber: number;
accountLabel: string;
balanceValue?: string;
}): VNode {
const { i18n } = useTranslationContext();
const { data, error, mutate } = useSWR(
`access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`,
);
useEffect(() => {
if (balanceValue) {
mutate();
}
}, [balanceValue ?? ""]);
if (typeof error !== "undefined") {
logger.error("transactions not found error", error);
switch (error.status) {
case 404: {
return <p>Transactions page {pageNumber} was not found.</p>;
}
case 401: {
return <p>Wrong credentials given.</p>;
}
default: {
return <p>Transaction page {pageNumber} could not be retrieved.</p>;
}
}
}
if (!data) {
logger.trace(`History data of ${accountLabel} not arrived`);
return <p>Transactions page loading...</p>;
}
logger.trace(`History data of ${accountLabel}`, data);
return (
<div class="results">
<table class="pure-table pure-table-striped">
<thead>
<tr>
<th>{i18n.str`Date`}</th>
<th>{i18n.str`Amount`}</th>
<th>{i18n.str`Counterpart`}</th>
<th>{i18n.str`Subject`}</th>
</tr>
</thead>
<tbody>
{data.transactions.map((item: any, idx: number) => {
const sign = item.direction == "DBIT" ? "-" : "";
const counterpart =
item.direction == "DBIT" ? item.creditorIban : item.debtorIban;
// Pattern:
//
// DD/MM YYYY subject -5 EUR
// DD/MM YYYY subject 5 EUR
const dateRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{1,2})/;
const dateParse = dateRegex.exec(item.date);
const date =
dateParse !== null
? `${dateParse[3]}/${dateParse[2]} ${dateParse[1]}`
: "date not found";
return (
<tr key={idx}>
<td>{date}</td>
<td>
{sign}
{item.amount} {item.currency}
</td>
<td>{counterpart}</td>
<td>{item.subject}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}

View File

@ -21,6 +21,7 @@
import { strings } from "./i18n/strings.js";
import * as pages from "./pages/index.stories.js";
import * as components from "./components/index.examples.js";
import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
@ -32,7 +33,7 @@ function SortStories(a: any, b: any): number {
function main(): void {
renderStories(
{ pages },
{ pages, components },
{
strings,
},