commit f4b71ececef38778ae813e7557f6ed6b78bfd591 parent 7948aee7d14323946b29ccafc838edeb1190963a Author: Sebastian <sebasjm@gmail.com> Date: Fri, 10 Dec 2021 14:26:24 -0300 reserve hook test Diffstat:
22 files changed, 887 insertions(+), 647 deletions(-)
diff --git a/packages/merchant-backoffice/src/components/exception/QR.tsx b/packages/merchant-backoffice/src/components/exception/QR.tsx @@ -18,18 +18,32 @@ import { h, VNode } from "preact"; import { useEffect, useRef } from "preact/hooks"; import qrcode from "qrcode-generator"; -export function QR({ text }: { text: string; }):VNode { +export function QR({ text }: { text: string }): VNode { const divRef = useRef<HTMLDivElement>(null); useEffect(() => { - const qr = qrcode(0, 'L'); + const qr = qrcode(0, "L"); qr.addData(text); qr.make(); - divRef.current.innerHTML = qr.createSvgTag({ - scalable: true, - }); + if (divRef.current) { + divRef.current.innerHTML = qr.createSvgTag({ + scalable: true, + }); + } }); - return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}> - <div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} /> - </div>; + return ( + <div + style={{ + width: "100%", + display: "flex", + flexDirection: "column", + alignItems: "center", + }} + > + <div + style={{ width: "50%", minWidth: 200, maxWidth: 300 }} + ref={divRef} + /> + </div> + ); } diff --git a/packages/merchant-backoffice/src/hooks/listener.ts b/packages/merchant-backoffice/src/hooks/listener.ts @@ -22,9 +22,22 @@ import { useState } from "preact/hooks"; /** - * returns subscriber and activator - * subscriber will receive a method (listener) that will be call when the activator runs. - * the result of calling the listener will be sent to @action + * This component is used when a component wants one child to have a trigger for + * an action (a button) and other child have the action implemented (like + * gathering information with a form). The difference with other approaches is + * that in this case the parent component is not holding the state. + * + * It will return a subscriber and activator. + * + * The activator may be undefined, if it is undefined it is indicating that the + * subscriber is not ready to be called. + * + * The subscriber will receive a function (the listener) that will be call when the + * activator runs. The listener must return the collected information. + * + * As a result, when the activator is triggered by a child component, the + * @action function is called receives the information from the listener defined by other + * child component * * @param action from <T> to <R> * @returns activator and subscriber, undefined activator means that there is not subscriber diff --git a/packages/merchant-backoffice/src/hooks/notification.ts b/packages/merchant-backoffice/src/hooks/notification.ts @@ -1,43 +0,0 @@ -/* - 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 { useCallback, useState } from "preact/hooks"; -import { Notification } from '../utils/types'; - -interface Result { - notification?: Notification; - pushNotification: (n: Notification) => void; - removeNotification: () => void; -} - -export function useNotification(): Result { - const [notification, setNotifications] = useState<Notification|undefined>(undefined) - - const pushNotification = useCallback((n: Notification): void => { - setNotifications(n) - },[]) - - const removeNotification = useCallback(() => { - setNotifications(undefined) - },[]) - - return { notification, pushNotification, removeNotification } -} diff --git a/packages/merchant-backoffice/src/hooks/order.ts b/packages/merchant-backoffice/src/hooks/order.ts @@ -73,19 +73,18 @@ export function orderFetcher<T>( export function useOrderAPI(): OrderAPI { const mutateAll = useMatchMutate(); - // const { mutate } = useSWRConfig(); const { url: baseUrl, token: adminToken } = useBackendContext(); const { token: instanceToken, id, admin } = useInstanceContext(); const { url, token } = !admin ? { - url: baseUrl, - token: adminToken, - } + url: baseUrl, + token: adminToken, + } : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + url: `${baseUrl}/instances/${id}`, + token: instanceToken, + }; const createOrder = async ( data: MerchantBackend.Orders.PostOrderRequest @@ -170,14 +169,8 @@ export function useOrderDetails( const { token: instanceToken, id, admin } = useInstanceContext(); const { url, token } = !admin - ? { - url: baseUrl, - token: baseToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + ? { url: baseUrl, token: baseToken, } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken, }; const { data, error, isValidating } = useSWR< HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, @@ -211,14 +204,8 @@ export function useInstanceOrders( const { token: instanceToken, id, admin } = useInstanceContext(); const { url, token } = !admin - ? { - url: baseUrl, - token: baseToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + ? { url: baseUrl, token: baseToken, } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken, }; const [pageBefore, setPageBefore] = useState(1); const [pageAfter, setPageAfter] = useState(1); @@ -317,9 +304,9 @@ export function useInstanceOrders( !beforeData || !afterData ? [] : (beforeData || lastBefore).data.orders - .slice() - .reverse() - .concat((afterData || lastAfter).data.orders); + .slice() + .reverse() + .concat((afterData || lastAfter).data.orders); if (loadingAfter || loadingBefore) return { loading: true, data: { orders } }; if (beforeData && afterData) { return { ok: true, data: { orders }, ...pagination }; diff --git a/packages/merchant-backoffice/src/hooks/product.ts b/packages/merchant-backoffice/src/hooks/product.ts @@ -13,11 +13,8 @@ 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 { useEffect } from "preact/hooks"; import useSWR, { useSWRConfig } from "swr"; -import useSWRInfinite, { SWRInfiniteConfiguration } from "swr/infinite"; import { useBackendContext } from "../context/backend"; -// import { useFetchContext } from '../context/fetch'; import { useInstanceContext } from "../context/instance"; import { MerchantBackend, WithId } from "../declaration"; import { @@ -27,7 +24,7 @@ import { HttpResponseOk, multiFetcher, request, - useMatchMutate, + useMatchMutate } from "./backend"; export interface ProductAPI { @@ -48,19 +45,12 @@ export interface ProductAPI { export function useProductAPI(): ProductAPI { const mutateAll = useMatchMutate(); const { mutate } = useSWRConfig(); - // const { mutate } = SWRInfiniteConfiguration(); const { url: baseUrl, token: adminToken } = useBackendContext(); const { token: instanceToken, id, admin } = useInstanceContext(); const { url, token } = !admin - ? { - url: baseUrl, - token: adminToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + ? { url: baseUrl, token: adminToken, } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken, }; const createProduct = async ( data: MerchantBackend.Products.ProductAddDetail @@ -84,51 +74,7 @@ export function useProductAPI(): ProductAPI { data, }); - // await mutate([`/private/products`, token, url]) - // await mutate([`/private/products/${productId}`, token, url], undefined, true); return await mutateAll(/.*"\/private\/products.*/); - return Promise.resolve(); - /** - * There is some inconsistency in how the cache is evicted. - * I'm keeping this for later inspection - */ - - // -- Clear all cache - // -- This seems to work always but is bad - - // const keys = [...cache.keys()] - // console.log(keys) - // cache.clear() - // await Promise.all(keys.map(k => trigger(k))) - - // -- From the keys to the cache trigger - // -- An intermediate step - - // const keys = [ - // [`/private/products`, token, url], - // [`/private/products/${productId}`, token, url], - // ] - // cache.clear() - // const f: string[][] = keys.map(k => cache.serializeKey(k)) - // console.log(f) - // const m = flat(f) - // console.log(m) - // await Promise.all(m.map(k => trigger(k, true))) - - // await Promise.all(keys.map(k => mutate(k))) - - // -- This is how is supposed to be use - - // await mutate([`/private/products`, token, url]) - // await mutate([`/private/products/${productId}`, token, url]) - - // await mutateAll(/@"\/private\/products"@/); - // await mutateAll(/@"\/private\/products\/.*"@/); - // return true - // return r - - // -- FIXME: why this un-break the tests? - // return Promise.resolve() }; const deleteProduct = async (productId: string): Promise<void> => { @@ -160,17 +106,10 @@ export function useInstanceProducts(): HttpResponse< > { const { url: baseUrl, token: baseToken } = useBackendContext(); const { token: instanceToken, id, admin } = useInstanceContext(); - // const { useSWR, useSWRInfinite } = useFetchContext(); const { url, token } = !admin - ? { - url: baseUrl, - token: baseToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + ? { url: baseUrl, token: baseToken, } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken, }; const { data: list, error: listError } = useSWR< HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>, @@ -183,21 +122,6 @@ export function useInstanceProducts(): HttpResponse< refreshWhenOffline: false, }); - // const dataLength = list?.data.products.length || 0 - // const prods: Array<MerchantBackend.Products.InventoryEntry>[] = [] - // prods[0] = list?.data.products || [] - - // const { data: products, error: productError, setSize } = useSWRInfinite<HttpResponseOk<MerchantBackend.Products.ProductDetail>, HttpError>((pageIndex: number) => { - // console.log('fetcher', prods[0], pageIndex) - // if (!list?.data || !prods[0].length || listError) return null - - // console.log(`/private/products/${prods[0][pageIndex].product_id}`) - // return [`/private/products/${prods[0][pageIndex].product_id}`, token, url] - // }, fetcher, { - // initialSize: dataLength, - // revalidateAll: true, - // }) - // [`/private/products`] const paths = (list?.data.products || []).map( (p) => `/private/products/${p.product_id}` ); @@ -212,19 +136,10 @@ export function useInstanceProducts(): HttpResponse< refreshWhenOffline: false, }); - // console.log('data length', dataLength, list?.data) - // useEffect(() => { - // if (dataLength > 0) { - // console.log('set size', dataLength) - // setSize(dataLength) - // } - // }, [dataLength, setSize]) if (listError) return listError; if (productError) return productError; - // if (dataLength === 0) { - // return { ok: true, data: [] } - // } + if (products) { const dataWithId = products.map((d) => { //take the id from the queried url @@ -233,7 +148,6 @@ export function useInstanceProducts(): HttpResponse< id: d.info?.url.replace(/.*\/private\/products\//, "") || "", }; }); - // console.log('data with id', dataWithId) return { ok: true, data: dataWithId }; } return { loading: true }; @@ -247,13 +161,13 @@ export function useProductDetails( const { url, token } = !admin ? { - url: baseUrl, - token: baseToken, - } + url: baseUrl, + token: baseToken, + } : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + url: `${baseUrl}/instances/${id}`, + token: instanceToken, + }; const { data, error, isValidating } = useSWR< HttpResponseOk<MerchantBackend.Products.ProductDetail>, diff --git a/packages/merchant-backoffice/src/hooks/reserves.ts b/packages/merchant-backoffice/src/hooks/reserves.ts @@ -0,0 +1,212 @@ +/* + 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/> + */ +import useSWR from "swr"; +import { useBackendContext } from "../context/backend"; +import { useInstanceContext } from "../context/instance"; +import { MerchantBackend } from "../declaration"; +import { + fetcher, + HttpError, + HttpResponse, + HttpResponseOk, + request, + useMatchMutate, +} from "./backend"; + +export function useReservesAPI(): ReserveMutateAPI { + const mutateAll = useMatchMutate(); + const { url: baseUrl, token: adminToken } = useBackendContext(); + const { token: instanceToken, id, admin } = useInstanceContext(); + + const { url, token } = !admin + ? { url: baseUrl, token: adminToken, } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken, }; + + const createReserve = async ( + data: MerchantBackend.Tips.ReserveCreateRequest + ): Promise< + HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation> + > => { + const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>( + `${url}/private/reserves`, + { + method: "post", + token, + data, + } + ); + + await mutateAll(/.*private\/reserves.*/); + + return res; + }; + + const authorizeTipReserve = async ( + pub: string, + data: MerchantBackend.Tips.TipCreateRequest + ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { + const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( + `${url}/private/reserves/${pub}/authorize-tip`, + { + method: "post", + token, + data, + } + ); + await mutateAll(/@"\/private\/reserves"@/); + + return res; + }; + + const authorizeTip = async ( + data: MerchantBackend.Tips.TipCreateRequest + ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { + const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( + `${url}/private/tips`, + { + method: "post", + token, + data, + } + ); + + await mutateAll(/@"\/private\/reserves"@/); + + return res; + }; + + const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => { + const res = await request<void>(`${url}/private/reserves/${pub}`, { + method: "delete", + token, + }); + + await mutateAll(/@"\/private\/reserves"@/); + + return res; + }; + + return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve }; +} + +export interface ReserveMutateAPI { + createReserve: ( + data: MerchantBackend.Tips.ReserveCreateRequest + ) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>; + authorizeTipReserve: ( + id: string, + data: MerchantBackend.Tips.TipCreateRequest + ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; + authorizeTip: ( + data: MerchantBackend.Tips.TipCreateRequest + ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; + deleteReserve: (id: string) => Promise<HttpResponse<void>>; +} + +export function useInstanceReserves(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> { + const { url: baseUrl, token: baseToken } = useBackendContext(); + const { token: instanceToken, id, admin } = useInstanceContext(); + + const { url, token } = !admin + ? { url: baseUrl, token: baseToken, } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken, }; + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>, + HttpError + >([`/private/reserves`, token, url], fetcher); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error; + return { loading: true }; +} + +export function useReserveDetails( + reserveId: string +): HttpResponse<MerchantBackend.Tips.ReserveDetail> { + const { url: baseUrl } = useBackendContext(); + const { token, id: instanceId, admin } = useInstanceContext(); + + const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`; + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Tips.ReserveDetail>, + HttpError + >([`/private/reserves/${reserveId}`, token, url], reserveDetailFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error; + return { loading: true }; +} + +export function useTipDetails( + tipId: string +): HttpResponse<MerchantBackend.Tips.TipDetails> { + const { url: baseUrl } = useBackendContext(); + const { token, id: instanceId, admin } = useInstanceContext(); + + const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`; + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Tips.TipDetails>, + HttpError + >([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error; + return { loading: true }; +} + +export function reserveDetailFetcher<T>( + url: string, + token: string, + backend: string +): Promise<HttpResponseOk<T>> { + return request<T>(`${backend}${url}`, { + token, + params: { + tips: "yes", + }, + }); +} + +export function tipsDetailFetcher<T>( + url: string, + token: string, + backend: string +): Promise<HttpResponseOk<T>> { + return request<T>(`${backend}${url}`, { + token, + params: { + pickups: "yes", + }, + }); +} diff --git a/packages/merchant-backoffice/src/hooks/tips.ts b/packages/merchant-backoffice/src/hooks/tips.ts @@ -1,224 +0,0 @@ -/* - 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/> - */ -import useSWR from "swr"; -import { useBackendContext } from "../context/backend"; -import { useInstanceContext } from "../context/instance"; -import { MerchantBackend } from "../declaration"; -import { - fetcher, - HttpError, - HttpResponse, - HttpResponseOk, - request, - useMatchMutate, -} from "./backend"; - -export function useReservesAPI(): ReserveMutateAPI { - const mutateAll = useMatchMutate(); - const { url: baseUrl, token: adminToken } = useBackendContext(); - const { token: instanceToken, id, admin } = useInstanceContext(); - - const { url, token } = !admin - ? { - url: baseUrl, - token: adminToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; - - const createReserve = async ( - data: MerchantBackend.Tips.ReserveCreateRequest - ): Promise< - HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation> - > => { - const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>( - `${url}/private/reserves`, - { - method: "post", - token, - data, - } - ); - - await mutateAll(/@"\/private\/reserves"@/); - - return res; - }; - - const authorizeTipReserve = async ( - pub: string, - data: MerchantBackend.Tips.TipCreateRequest - ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { - const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( - `${url}/private/reserves/${pub}/authorize-tip`, - { - method: "post", - token, - data, - } - ); - await mutateAll(/@"\/private\/reserves"@/); - - return res; - }; - - const authorizeTip = async ( - data: MerchantBackend.Tips.TipCreateRequest - ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { - const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( - `${url}/private/tips`, - { - method: "post", - token, - data, - } - ); - - await mutateAll(/@"\/private\/reserves"@/); - - return res; - }; - - const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => { - const res = await request<void>(`${url}/private/reserves/${pub}`, { - method: "delete", - token, - }); - - await mutateAll(/@"\/private\/reserves"@/); - - return res; - }; - - return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve }; -} - -export interface ReserveMutateAPI { - createReserve: ( - data: MerchantBackend.Tips.ReserveCreateRequest - ) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>; - authorizeTipReserve: ( - id: string, - data: MerchantBackend.Tips.TipCreateRequest - ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; - authorizeTip: ( - data: MerchantBackend.Tips.TipCreateRequest - ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; - deleteReserve: (id: string) => Promise<HttpResponse<void>>; -} - -export function useInstanceTips(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> { - const { url: baseUrl, token: baseToken } = useBackendContext(); - const { token: instanceToken, id, admin } = useInstanceContext(); - - const { url, token } = !admin - ? { - url: baseUrl, - token: baseToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>, - HttpError - >([`/private/reserves`, token, url], fetcher); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error; - return { loading: true }; -} - -export function useReserveDetails( - reserveId: string -): HttpResponse<MerchantBackend.Tips.ReserveDetail> { - const { url: baseUrl } = useBackendContext(); - const { token, id: instanceId, admin } = useInstanceContext(); - - const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`; - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.ReserveDetail>, - HttpError - >([`/private/reserves/${reserveId}`, token, url], reserveDetailFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error; - return { loading: true }; -} - -export function useTipDetails( - tipId: string -): HttpResponse<MerchantBackend.Tips.TipDetails> { - const { url: baseUrl } = useBackendContext(); - const { token, id: instanceId, admin } = useInstanceContext(); - - const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`; - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.TipDetails>, - HttpError - >([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error; - return { loading: true }; -} - -export function reserveDetailFetcher<T>( - url: string, - token: string, - backend: string -): Promise<HttpResponseOk<T>> { - return request<T>(`${backend}${url}`, { - token, - params: { - tips: "yes", - }, - }); -} - -export function tipsDetailFetcher<T>( - url: string, - token: string, - backend: string -): Promise<HttpResponseOk<T>> { - return request<T>(`${backend}${url}`, { - token, - params: { - pickups: "yes", - }, - }); -} diff --git a/packages/merchant-backoffice/src/hooks/transfer.ts b/packages/merchant-backoffice/src/hooks/transfer.ts @@ -41,11 +41,6 @@ async function transferFetcher<T>( if (payto_uri !== undefined) params.payto_uri = payto_uri; if (verified !== undefined) params.verified = verified; if (delta !== undefined) { - // if (delta > 0) { - // params.after = searchDate?.getTime() - // } else { - // params.before = searchDate?.getTime() - // } params.limit = delta; } if (position !== undefined) params.offset = position; @@ -60,13 +55,13 @@ export function useTransferAPI(): TransferAPI { const { url, token } = !admin ? { - url: baseUrl, - token: adminToken, - } + url: baseUrl, + token: adminToken, + } : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + url: `${baseUrl}/instances/${id}`, + token: instanceToken, + }; const informTransfer = async ( data: MerchantBackend.Transfers.TransferInformation @@ -110,14 +105,8 @@ export function useInstanceTransfers( const { token: instanceToken, id, admin } = useInstanceContext(); const { url, token } = !admin - ? { - url: baseUrl, - token: baseToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + ? { url: baseUrl, token: baseToken } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; const [pageBefore, setPageBefore] = useState(1); const [pageAfter, setPageAfter] = useState(1); @@ -192,9 +181,9 @@ export function useInstanceTransfers( setPageAfter(pageAfter + 1); } else { const from = - "" + - afterData.data.transfers[afterData.data.transfers.length - 1] - .transfer_serial_id; + `${afterData.data + .transfers[afterData.data.transfers.length - 1] + .transfer_serial_id}`; if (from && updatePosition) updatePosition(from); } }, @@ -204,9 +193,9 @@ export function useInstanceTransfers( setPageBefore(pageBefore + 1); } else if (beforeData) { const from = - "" + - beforeData.data.transfers[beforeData.data.transfers.length - 1] - .transfer_serial_id; + `${beforeData.data + .transfers[beforeData.data.transfers.length - 1] + .transfer_serial_id}`; if (from && updatePosition) updatePosition(from); } }, @@ -216,9 +205,9 @@ export function useInstanceTransfers( !beforeData || !afterData ? [] : (beforeData || lastBefore).data.transfers - .slice() - .reverse() - .concat((afterData || lastAfter).data.transfers); + .slice() + .reverse() + .concat((afterData || lastAfter).data.transfers); if (loadingAfter || loadingBefore) return { loading: true, data: { transfers } }; if (beforeData && afterData) { diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/create/index.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/create/index.tsx @@ -15,49 +15,57 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { Fragment, h, VNode } from 'preact'; -import { useState } from 'preact/hooks'; -import { NotificationCard } from '../../../../components/menu'; -import { MerchantBackend } from '../../../../declaration'; -import { useReservesAPI } from '../../../../hooks/tips'; -import { useTranslator } from '../../../../i18n'; -import { Notification } from '../../../../utils/types'; -import { CreatedSuccessfully } from './CreatedSuccessfully'; -import { CreatePage } from './CreatePage'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu"; +import { MerchantBackend } from "../../../../declaration"; +import { useReservesAPI } from "../../../../hooks/reserves"; +import { useTranslator } from "../../../../i18n"; +import { Notification } from "../../../../utils/types"; +import { CreatedSuccessfully } from "./CreatedSuccessfully"; +import { CreatePage } from "./CreatePage"; interface Props { onBack: () => void; onConfirm: () => void; } export default function CreateReserve({ onBack, onConfirm }: Props): VNode { - const { createReserve } = useReservesAPI() - const [notif, setNotif] = useState<Notification | undefined>(undefined) - const i18n = useTranslator() + const { createReserve } = useReservesAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const i18n = useTranslator(); - const [createdOk, setCreatedOk] = useState<{ - request: MerchantBackend.Tips.ReserveCreateRequest, - response: MerchantBackend.Tips.ReserveCreateConfirmation - } | undefined>(undefined); + const [createdOk, setCreatedOk] = useState< + | { + request: MerchantBackend.Tips.ReserveCreateRequest; + response: MerchantBackend.Tips.ReserveCreateConfirmation; + } + | undefined + >(undefined); if (createdOk) { - return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} /> + return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />; } - return <Fragment> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => { - return createReserve(request).then((r) => setCreatedOk({ request, response: r.data })).catch((error) => { - setNotif({ - message: i18n`could not create reserve`, - type: "ERROR", - description: error.message - }) - }) - }} /> - </Fragment> + return ( + <Fragment> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => { + return createReserve(request) + .then((r) => setCreatedOk({ request, response: r.data })) + .catch((error) => { + setNotif({ + message: i18n`could not create reserve`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); } diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/details/DetailPage.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/details/DetailPage.tsx @@ -29,7 +29,7 @@ import { InputCurrency } from "../../../../components/form/InputCurrency"; import { InputDate } from "../../../../components/form/InputDate"; import { TextField } from "../../../../components/form/TextField"; import { MerchantBackend } from "../../../../declaration"; -import { useTipDetails } from "../../../../hooks/tips"; +import { useTipDetails } from "../../../../hooks/reserves"; import { Translate, useTranslator } from "../../../../i18n"; type Entity = MerchantBackend.Tips.ReserveDetail; diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/details/index.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/details/index.tsx @@ -15,15 +15,15 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { Fragment, h, VNode } from 'preact'; -import { Loading } from '../../../../components/exception/loading'; -import { HttpError } from '../../../../hooks/backend'; -import { useReserveDetails } from '../../../../hooks/tips'; -import { DetailPage } from './DetailPage'; +import { Fragment, h, VNode } from "preact"; +import { Loading } from "../../../../components/exception/loading"; +import { HttpError } from "../../../../hooks/backend"; +import { useReserveDetails } from "../../../../hooks/reserves"; +import { DetailPage } from "./DetailPage"; interface Props { rid: string; @@ -34,14 +34,23 @@ interface Props { onDelete: () => void; onBack: () => void; } -export default function DetailReserve({ rid, onUnauthorized, onLoadError, onNotFound, onBack, onDelete }: Props): VNode { - const result = useReserveDetails(rid) - - if (result.clientError && result.isUnauthorized) return onUnauthorized() - if (result.clientError && result.isNotfound) return onNotFound() - if (result.loading) return <Loading /> - if (!result.ok) return onLoadError(result) - return <Fragment> - <DetailPage selected={result.data} onBack={onBack} id={rid} /> - </Fragment> +export default function DetailReserve({ + rid, + onUnauthorized, + onLoadError, + onNotFound, + onBack, + onDelete, +}: Props): VNode { + const result = useReserveDetails(rid); + + if (result.clientError && result.isUnauthorized) return onUnauthorized(); + if (result.clientError && result.isNotfound) return onNotFound(); + if (result.loading) return <Loading />; + if (!result.ok) return onLoadError(result); + return ( + <Fragment> + <DetailPage selected={result.data} onBack={onBack} id={rid} /> + </Fragment> + ); } diff --git a/packages/merchant-backoffice/src/paths/instance/reserves/list/index.tsx b/packages/merchant-backoffice/src/paths/instance/reserves/list/index.tsx @@ -15,21 +15,24 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { h, VNode } from 'preact'; -import { useState } from 'preact/hooks'; -import { Loading } from '../../../../components/exception/loading'; -import { NotificationCard } from '../../../../components/menu'; -import { MerchantBackend } from '../../../../declaration'; -import { HttpError } from '../../../../hooks/backend'; -import { useInstanceTips, useReservesAPI } from "../../../../hooks/tips"; -import { useTranslator } from '../../../../i18n'; -import { Notification } from '../../../../utils/types'; -import { CardTable } from './Table'; -import { AuthorizeTipModal } from './AutorizeTipModal'; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading"; +import { NotificationCard } from "../../../../components/menu"; +import { MerchantBackend } from "../../../../declaration"; +import { HttpError } from "../../../../hooks/backend"; +import { + useInstanceReserves, + useReservesAPI, +} from "../../../../hooks/reserves"; +import { useTranslator } from "../../../../i18n"; +import { Notification } from "../../../../utils/types"; +import { CardTable } from "./Table"; +import { AuthorizeTipModal } from "./AutorizeTipModal"; interface Props { onUnauthorized: () => VNode; @@ -44,54 +47,71 @@ interface TipConfirmation { request: MerchantBackend.Tips.TipCreateRequest; } -export default function ListTips({ onUnauthorized, onLoadError, onNotFound, onSelect, onCreate }: Props): VNode { - const result = useInstanceTips() - const { deleteReserve, authorizeTipReserve } = useReservesAPI() - const [notif, setNotif] = useState<Notification | undefined>(undefined) - const i18n = useTranslator() - const [reserveForTip, setReserveForTip] = useState<string | undefined>(undefined); - const [tipAuthorized, setTipAuthorized] = useState<TipConfirmation | undefined>(undefined); +export default function ListTips({ + onUnauthorized, + onLoadError, + onNotFound, + onSelect, + onCreate, +}: Props): VNode { + const result = useInstanceReserves(); + const { deleteReserve, authorizeTipReserve } = useReservesAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const i18n = useTranslator(); + const [reserveForTip, setReserveForTip] = useState<string | undefined>( + undefined + ); + const [tipAuthorized, setTipAuthorized] = useState< + TipConfirmation | undefined + >(undefined); - if (result.clientError && result.isUnauthorized) return onUnauthorized() - if (result.clientError && result.isNotfound) return onNotFound() - if (result.loading) return <Loading /> - if (!result.ok) return onLoadError(result) + if (result.clientError && result.isUnauthorized) return onUnauthorized(); + if (result.clientError && result.isNotfound) return onNotFound(); + if (result.loading) return <Loading />; + if (!result.ok) return onLoadError(result); + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> - return <section class="section is-main-section"> - <NotificationCard notification={notif} /> + {reserveForTip && ( + <AuthorizeTipModal + onCancel={() => { + setReserveForTip(undefined); + setTipAuthorized(undefined); + }} + tipAuthorized={tipAuthorized} + onConfirm={async (request) => { + try { + const response = await authorizeTipReserve( + reserveForTip, + request + ); + setTipAuthorized({ + request, + response: response.data, + }); + } catch (error) { + setNotif({ + message: i18n`could not create the tip`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + setReserveForTip(undefined); + } + }} + /> + )} - {reserveForTip && ( - <AuthorizeTipModal - onCancel={() => { - setReserveForTip(undefined) - setTipAuthorized(undefined) - }} - tipAuthorized={tipAuthorized} - onConfirm={async (request) => { - try { - const response = await authorizeTipReserve(reserveForTip, request) - setTipAuthorized({ - request, response: response.data - }) - } catch (error) { - setNotif({ - message: i18n`could not create the tip`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined - }) - setReserveForTip(undefined) - } - } - } + <CardTable + instances={result.data.reserves + .filter((r) => r.active) + .map((o) => ({ ...o, id: o.reserve_pub }))} + onCreate={onCreate} + onDelete={(reserve) => deleteReserve(reserve.reserve_pub)} + onSelect={(reserve) => onSelect(reserve.id)} + onNewTip={(reserve) => setReserveForTip(reserve.id)} /> - )} - - <CardTable instances={result.data.reserves.filter(r => r.active).map(o => ({ ...o, id: o.reserve_pub }))} - onCreate={onCreate} - onDelete={(reserve) => deleteReserve(reserve.reserve_pub)} - onSelect={(reserve) => onSelect(reserve.id)} - onNewTip={(reserve) => setReserveForTip(reserve.id)} - /> - </section> + </section> + ); } diff --git a/packages/merchant-backoffice/tests/axiosMock.ts b/packages/merchant-backoffice/tests/axiosMock.ts @@ -52,7 +52,7 @@ export class AxiosMockEnvironment { this.axiosMock = (axios.default as jest.MockedFunction<axios.AxiosStatic>).mockImplementation(defaultCallback as any) } - addRequestExpectation<RequestType, ResponseType>(expectedQuery?: Query<RequestType, ResponseType> | undefined, params?: { request?: RequestType, qparam?: any, response?: ResponseType }): void { + addRequestExpectation<RequestType, ResponseType>(expectedQuery: Query<RequestType, ResponseType>, params: { request?: RequestType, qparam?: any, response?: ResponseType }): void { this.expectations.push(expectedQuery ? { query: expectedQuery, params } : undefined) this.axiosMock = this.axiosMock.mockImplementationOnce(function (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise { @@ -113,28 +113,27 @@ export class AxiosMockEnvironment { } -export function testAllExpectedRequestAndNoMore(env: AxiosMockEnvironment): void { +export function assertJustExpectedRequestWereMade(env: AxiosMockEnvironment): void { let size = env.expectations.length while (size-- > 0) { - testOneRequestToBackend(env) + assertNextRequest(env) } - testNoOtherRequestWasMade(env) + assertNoMoreRequestWereMade(env) } -export function testNoOtherRequestWasMade(env: AxiosMockEnvironment): void { +export function assertNoMoreRequestWereMade(env: AxiosMockEnvironment): void { const [actualQuery, expectedQuery] = env.getLastTestValues() expect(actualQuery).toBeUndefined(); expect(expectedQuery).toBeUndefined(); } -export function testOneRequestToBackend(env: AxiosMockEnvironment): void { +export function assertNextRequest(env: AxiosMockEnvironment): void { const [actualQuery, expectedQuery] = env.getLastTestValues() if (!actualQuery) { + //expected one query but the tested component didn't execute one expect(actualQuery).toBe(expectedQuery); - - // throw Error('actual query was undefined'); return } @@ -152,7 +151,6 @@ export function testOneRequestToBackend(env: AxiosMockEnvironment): void { if ('post' in expectedQuery.query) { expect(actualQuery.method).toBe('post'); expect(actualQuery.url).toBe(expectedQuery.query.post); - if (actualQuery.method !== 'post') throw Error('tu vieja') } if ('delete' in expectedQuery.query) { expect(actualQuery.method).toBe('delete'); @@ -179,6 +177,13 @@ export const API_LIST_PRODUCTS: Query< get: "http://backend/instances/default/private/products", }; +export const API_LIST_RESERVES: Query< + unknown, + MerchantBackend.Tips.TippingReserveStatus +> = { + get: "http://backend/instances/default/private/reserves", +}; + export const API_LIST_ORDERS: Query< unknown, MerchantBackend.Orders.OrderHistory @@ -186,12 +191,25 @@ export const API_LIST_ORDERS: Query< get: "http://backend/instances/default/private/orders", }; +export const API_LIST_TRANSFERS: Query< + unknown, + MerchantBackend.Transfers.TransferList +> = { + get: "http://backend/instances/default/private/transfers", +}; + export const API_CREATE_PRODUCT: Query< MerchantBackend.Products.ProductAddDetail, unknown > = { post: "http://backend/instances/default/private/products", }; +export const API_CREATE_RESERVE: Query< + MerchantBackend.Tips.ReserveCreateRequest, + MerchantBackend.Tips.ReserveCreateConfirmation +> = { + post: "http://backend/instances/default/private/reserves", +}; export const API_CREATE_ORDER: Query< MerchantBackend.Orders.PostOrderRequest, @@ -202,7 +220,7 @@ export const API_CREATE_ORDER: Query< export const API_GET_PRODUCT_BY_ID = ( id: string -): Query<unknown, MerchantBackend.Products.InventorySummaryResponse> => ({ +): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ get: `http://backend/instances/default/private/products/${id}`, }); @@ -214,3 +232,10 @@ export const API_UPDATE_PRODUCT_BY_ID = ( > => ({ patch: `http://backend/instances/default/private/products/${id}`, }); + +export const API_GET_RESERVE_BY_ID = ( + pub: string +): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({ + get: `http://backend/instances/default/private/reserves/${pub}`, +}); + diff --git a/packages/merchant-backoffice/tests/hooks/listener.test.ts b/packages/merchant-backoffice/tests/hooks/listener.test.ts @@ -0,0 +1,62 @@ +/* + 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 { renderHook, act } from '@testing-library/preact-hooks'; +import { useListener } from '../../src/hooks/listener'; + +// jest.useFakeTimers() + +test('listener', async () => { + + + function createSomeString() { + return "hello" + } + async function addWorldToTheEnd(resultFromComponentB: string) { + return `${resultFromComponentB} world` + } + const expectedResult = "hello world" + + const { result } = renderHook(() => useListener(addWorldToTheEnd)) + + if (!result.current) { + expect(result.current).toBeDefined() + return; + } + + { + const [activator, subscriber] = result.current + expect(activator).toBeUndefined() + + act(() => { + subscriber(createSomeString) + }) + + } + + const [activator] = result.current + expect(activator).toBeDefined() + if (!activator) return; + + const response = await activator() + expect(response).toBe(expectedResult) + +}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/order-create.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/order-create.test.tsx @@ -23,13 +23,14 @@ import { renderHook } from "@testing-library/preact-hooks"; import { act } from "preact/test-utils"; import * as backend from "../../../src/context/backend"; import * as instance from "../../../src/context/instance"; +import { MerchantBackend } from "../../../src/declaration"; import { useInstanceOrders, useOrderAPI } from "../../../src/hooks/order"; import { API_CREATE_ORDER, API_LIST_ORDERS, AxiosMockEnvironment, - testNoOtherRequestWasMade, - testOneRequestToBackend, + assertNoMoreRequestWereMade, + assertNextRequest, } from "../../axiosMock"; jest.mock("axios"); @@ -53,14 +54,14 @@ describe("order api", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: 0, paid: "yes" }, response: { - orders: [{ order_id: "1" }], + orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], }, }); env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, response: { - orders: [{ order_id: "2" }], + orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], }, }); @@ -82,9 +83,9 @@ describe("order api", () => { expect(result.current.query.loading).toBeTruthy(); await waitForNextUpdate(); - testOneRequestToBackend(env); - testOneRequestToBackend(env); - testNoOtherRequestWasMade(env); + assertNextRequest(env); + assertNextRequest(env); + assertNoMoreRequestWereMade(env); expect(result.current.query.loading).toBeFalsy(); expect(result.current?.query.ok).toBeTruthy(); @@ -121,11 +122,11 @@ describe("order api", () => { } as any); }); - testOneRequestToBackend(env); //post + assertNextRequest(env); //post await waitForNextUpdate(); - testOneRequestToBackend(env); //get - testOneRequestToBackend(env); //get - testNoOtherRequestWasMade(env); + assertNextRequest(env); //get + assertNextRequest(env); //get + assertNoMoreRequestWereMade(env); expect(result.current.query.loading).toBeFalsy(); expect(result.current?.query.ok).toBeTruthy(); diff --git a/packages/merchant-backoffice/tests/hooks/swr/order-pagination.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/order-pagination.test.tsx @@ -22,18 +22,14 @@ import { act, renderHook } from "@testing-library/preact-hooks"; import * as backend from "../../../src/context/backend"; import * as instance from "../../../src/context/instance"; -import { MerchantBackend } from "../../../src/declaration"; -import { setAxiosRequestAsTestingEnvironment } from "../../../src/hooks/backend"; import { useInstanceOrders } from "../../../src/hooks/order"; import { API_LIST_ORDERS, AxiosMockEnvironment, - testNoOtherRequestWasMade, - testOneRequestToBackend, + assertNoMoreRequestWereMade, + assertNextRequest, } from "../../axiosMock"; -setAxiosRequestAsTestingEnvironment(); - jest.mock("axios"); describe("order pagination", () => { @@ -73,13 +69,13 @@ describe("order pagination", () => { const date = new Date(12); const { result, waitForNextUpdate } = renderHook(() => useInstanceOrders({ wired: "yes", date }, newDate) - ); // get products -> loading + ); - testOneRequestToBackend(env); - testOneRequestToBackend(env); - testNoOtherRequestWasMade(env); + assertNextRequest(env); + assertNextRequest(env); + assertNoMoreRequestWereMade(env); - await waitForNextUpdate(); // get info of every product, -> loading + await waitForNextUpdate(); expect(result.current?.ok).toBeTruthy(); if (!result.current?.ok) return; @@ -99,10 +95,10 @@ describe("order pagination", () => { if (!result.current?.ok) throw Error("not ok"); result.current.loadMore(); }); - await waitForNextUpdate(); // loading product -> products - // await waitForNextUpdate(); // loading product -> products - testOneRequestToBackend(env); - testNoOtherRequestWasMade(env); + await waitForNextUpdate(); + + assertNextRequest(env); + assertNoMoreRequestWereMade(env); env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: 40, wired: "yes", date_ms: 12 }, @@ -114,9 +110,9 @@ describe("order pagination", () => { if (!result.current?.ok) throw Error("not ok"); result.current.loadMorePrev(); }); - await waitForNextUpdate(); // loading product -> products - testOneRequestToBackend(env); - testNoOtherRequestWasMade(env); + await waitForNextUpdate(); + assertNextRequest(env); + assertNoMoreRequestWereMade(env); expect(result.current.data).toEqual({ orders: [ diff --git a/packages/merchant-backoffice/tests/hooks/swr/product-create.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/product-create.test.tsx @@ -23,13 +23,14 @@ import { renderHook } from "@testing-library/preact-hooks"; import { act } from "preact/test-utils"; import * as backend from "../../../src/context/backend"; import * as instance from "../../../src/context/instance"; +import { MerchantBackend } from "../../../src/declaration"; import { useInstanceProducts, useProductAPI } from "../../../src/hooks/product"; import { API_CREATE_PRODUCT, API_LIST_PRODUCTS, API_GET_PRODUCT_BY_ID, AxiosMockEnvironment, - testAllExpectedRequestAndNoMore, + assertJustExpectedRequestWereMade, } from "../../axiosMock"; jest.mock("axios"); @@ -58,7 +59,7 @@ describe("product create api", () => { }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:33" }, + response: { price: "ARS:33" } as MerchantBackend.Products.ProductDetail, }); const { result, waitForNextUpdate } = renderHook(() => { @@ -66,8 +67,7 @@ describe("product create api", () => { const query = useInstanceProducts(); return { query, api }; - }); // get products -> loading - + }); if (!result.current) { expect(result.current).toBeDefined(); return; @@ -78,7 +78,7 @@ describe("product create api", () => { expect(result.current.query.loading).toBeTruthy(); await waitForNextUpdate(); // second query to get product details - testAllExpectedRequestAndNoMore(env); + assertJustExpectedRequestWereMade(env); expect(result.current.query.loading).toBeFalsy(); expect(result.current?.query.ok).toBeTruthy(); @@ -89,7 +89,9 @@ describe("product create api", () => { ]); env.addRequestExpectation(API_CREATE_PRODUCT, { - request: { price: "ARS:3333" }, + request: { + price: "ARS:3333", + } as MerchantBackend.Products.ProductAddDetail, }); act(async () => { @@ -98,7 +100,7 @@ describe("product create api", () => { } as any); }); - testAllExpectedRequestAndNoMore(env); + assertJustExpectedRequestWereMade(env); env.addRequestExpectation(API_LIST_PRODUCTS, { response: { @@ -107,21 +109,21 @@ describe("product create api", () => { }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:33" }, + response: { price: "ARS:33" } as MerchantBackend.Products.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:33" }, + response: { price: "ARS:33" } as MerchantBackend.Products.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2222"), { - response: { price: "ARS:3333" }, + response: { price: "ARS:3333" } as MerchantBackend.Products.ProductDetail, }); expect(result.current.query.loading).toBeFalsy(); await waitForNextUpdate(); // loading product -> products await waitForNextUpdate(); // loading product -> products - testAllExpectedRequestAndNoMore(env); + assertJustExpectedRequestWereMade(env); expect(result.current.query.loading).toBeFalsy(); expect(result.current.query.ok).toBeTruthy(); diff --git a/packages/merchant-backoffice/tests/hooks/swr/product-delete.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/product-delete.test.tsx @@ -23,13 +23,14 @@ import { renderHook } from "@testing-library/preact-hooks"; import { act } from "preact/test-utils"; import * as backend from "../../../src/context/backend"; import * as instance from "../../../src/context/instance"; +import { MerchantBackend } from "../../../src/declaration"; import { useInstanceProducts, useProductAPI } from "../../../src/hooks/product"; import { API_LIST_PRODUCTS, API_GET_PRODUCT_BY_ID, AxiosMockEnvironment, - testAllExpectedRequestAndNoMore, - testOneRequestToBackend, + assertNextRequest, + assertJustExpectedRequestWereMade, } from "../../axiosMock"; jest.mock("axios"); @@ -46,7 +47,6 @@ describe("product delete api", () => { .mockImplementation( () => ({ token: "token", id: "default", admin: true } as any) ); - // console.log("CLEAR") }); it("should not have problem with cache after a delete", async () => { @@ -59,31 +59,22 @@ describe("product delete api", () => { }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" }, + response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { price: "ARS:23" }, + response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, }); - // env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - // response: { price: "ARS:12" }, - // }); - - // env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - // response: { price: "ARS:23" }, - // }); - const { result, waitForNextUpdate } = renderHook(() => { const query = useInstanceProducts(); const api = useProductAPI(); return { query, api }; - }); // get products -> loading + }); - await waitForNextUpdate(); // get info of every product, -> loading - await waitForNextUpdate(); // loading product -> products - testAllExpectedRequestAndNoMore(env); - // await waitForNextUpdate(); // loading product -> products + await waitForNextUpdate(); + await waitForNextUpdate(); + assertJustExpectedRequestWereMade(env); expect(result.current?.query.ok).toBeTruthy(); if (!result.current?.query.ok) return; @@ -93,9 +84,12 @@ describe("product delete api", () => { { id: "2345", price: "ARS:23" }, ]); - env.addRequestExpectation({ - delete: "http://backend/instances/default/private/products/1234", - }); + env.addRequestExpectation( + { + delete: "http://backend/instances/default/private/products/1234", + }, + {} + ); env.addRequestExpectation(API_LIST_PRODUCTS, { response: { @@ -104,21 +98,17 @@ describe("product delete api", () => { }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { price: "ARS:23" }, + response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, }); - // env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - // response: { price: "ARS:23" }, - // }); - act(async () => { await result.current?.api.deleteProduct("1234"); }); - testOneRequestToBackend(env); + assertNextRequest(env); await waitForNextUpdate(); await waitForNextUpdate(); - testAllExpectedRequestAndNoMore(env); + assertJustExpectedRequestWereMade(env); expect(result.current.query.data).toEqual([ { diff --git a/packages/merchant-backoffice/tests/hooks/swr/product-details-update.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/product-details-update.test.tsx @@ -29,8 +29,8 @@ import { API_GET_PRODUCT_BY_ID, API_UPDATE_PRODUCT_BY_ID, AxiosMockEnvironment, - testAllExpectedRequestAndNoMore, - testOneRequestToBackend, + assertNextRequest, + assertJustExpectedRequestWereMade, } from "../../axiosMock"; jest.mock("axios"); @@ -53,7 +53,9 @@ describe("product details api", () => { const env = new AxiosMockEnvironment(); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { - response: { description: "this is a description" }, + response: { + description: "this is a description", + } as MerchantBackend.Products.ProductDetail, }); const { result, waitForNextUpdate } = renderHook(() => { @@ -69,7 +71,7 @@ describe("product details api", () => { expect(result.current.query.loading).toBeTruthy(); await waitForNextUpdate(); - testAllExpectedRequestAndNoMore(env); + assertJustExpectedRequestWereMade(env); expect(result.current.query.loading).toBeFalsy(); expect(result.current?.query.ok).toBeTruthy(); @@ -84,7 +86,9 @@ describe("product details api", () => { }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { - response: { description: "other description" }, + response: { + description: "other description", + } as MerchantBackend.Products.ProductDetail, }); act(async () => { @@ -93,10 +97,10 @@ describe("product details api", () => { } as any); }); - testOneRequestToBackend(env); + assertNextRequest(env); await waitForNextUpdate(); - testAllExpectedRequestAndNoMore(env); + assertJustExpectedRequestWereMade(env); expect(result.current.query.loading).toBeFalsy(); expect(result.current?.query.ok).toBeTruthy(); diff --git a/packages/merchant-backoffice/tests/hooks/swr/product-update.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/product-update.test.tsx @@ -23,14 +23,15 @@ import { renderHook } from "@testing-library/preact-hooks"; import { act } from "preact/test-utils"; import * as backend from "../../../src/context/backend"; import * as instance from "../../../src/context/instance"; +import { MerchantBackend } from "../../../src/declaration"; import { useInstanceProducts, useProductAPI } from "../../../src/hooks/product"; import { API_GET_PRODUCT_BY_ID, API_LIST_PRODUCTS, API_UPDATE_PRODUCT_BY_ID, AxiosMockEnvironment, - testAllExpectedRequestAndNoMore, - testOneRequestToBackend, + assertJustExpectedRequestWereMade, + assertNextRequest, } from "../../axiosMock"; jest.mock("axios"); @@ -59,7 +60,7 @@ describe("product list api", () => { }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" }, + response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, }); const { result, waitForNextUpdate } = renderHook(() => { @@ -76,7 +77,7 @@ describe("product list api", () => { await waitForNextUpdate(); await waitForNextUpdate(); - testAllExpectedRequestAndNoMore(env); + assertJustExpectedRequestWereMade(env); expect(result.current.query.loading).toBeFalsy(); expect(result.current.query.ok).toBeTruthy(); @@ -96,20 +97,8 @@ describe("product list api", () => { }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:13" }, + response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail, }); - // env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - // request: { price: "ARS:13" }, - // }); - // env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - // request: { price: "ARS:13" }, - // }); - // env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - // request: { price: "ARS:13" }, - // }); - // env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - // request: { price: "ARS:13" }, - // }); act(async () => { await result.current?.api.updateProduct("1234", { @@ -117,11 +106,11 @@ describe("product list api", () => { } as any); }); - testOneRequestToBackend(env); + assertNextRequest(env); await waitForNextUpdate(); // await waitForNextUpdate(); - // testAllExpectedRequestAndNoMore(env); + assertJustExpectedRequestWereMade(env); // await waitForNextUpdate(); expect(result.current.query.loading).toBeFalsy(); diff --git a/packages/merchant-backoffice/tests/hooks/swr/reserve-create.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/reserve-create.test.tsx @@ -0,0 +1,152 @@ +/* + 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 { renderHook } from "@testing-library/preact-hooks"; +import { act } from "preact/test-utils"; +import * as backend from "../../../src/context/backend"; +import * as instance from "../../../src/context/instance"; +import { MerchantBackend } from "../../../src/declaration"; +import { + useInstanceReserves, + useReservesAPI, +} from "../../../src/hooks/reserves"; +import { + API_CREATE_PRODUCT, + API_LIST_PRODUCTS, + API_GET_PRODUCT_BY_ID, + AxiosMockEnvironment, + assertJustExpectedRequestWereMade, + API_LIST_RESERVES, + API_CREATE_RESERVE, + API_GET_RESERVE_BY_ID, +} from "../../axiosMock"; + +jest.mock("axios"); + +describe("reserve create api", () => { + beforeEach(() => { + jest + .spyOn(backend, "useBackendContext") + .mockImplementation( + () => ({ url: "http://backend", token: "token" } as any) + ); + jest + .spyOn(instance, "useInstanceContext") + .mockImplementation( + () => ({ token: "token", id: "default", admin: true } as any) + ); + }); + + it("should mutate list cache when creating new reserve", async () => { + const env = new AxiosMockEnvironment(); + + env.addRequestExpectation(API_LIST_RESERVES, { + response: { + reserves: [ + { + reserve_pub: "11", + } as MerchantBackend.Tips.ReserveStatusEntry, + ], + }, + }); + + const { result, waitForNextUpdate } = renderHook(() => { + const api = useReservesAPI(); + const query = useInstanceReserves(); + + return { query, api }; + }); + + if (!result.current) { + expect(result.current).toBeDefined(); + return; + } + expect(result.current.query.loading).toBeTruthy(); + + await waitForNextUpdate(); + + assertJustExpectedRequestWereMade(env); + + expect(result.current.query.loading).toBeFalsy(); + expect(result.current?.query.ok).toBeTruthy(); + if (!result.current?.query.ok) return; + + expect(result.current.query.data).toEqual({ + reserves: [{ reserve_pub: "11" }], + }); + + env.addRequestExpectation(API_CREATE_RESERVE, { + request: { + initial_balance: "ARS:3333", + exchange_url: "http://url", + wire_method: "iban", + }, + response: { + reserve_pub: "22", + payto_uri: "payto", + }, + }); + + act(async () => { + await result.current?.api.createReserve({ + initial_balance: "ARS:3333", + exchange_url: "http://url", + wire_method: "iban", + }); + return; + }); + + assertJustExpectedRequestWereMade(env); + + env.addRequestExpectation(API_LIST_RESERVES, { + response: { + reserves: [ + { + reserve_pub: "11", + } as MerchantBackend.Tips.ReserveStatusEntry, + { + reserve_pub: "22", + } as MerchantBackend.Tips.ReserveStatusEntry, + ], + }, + }); + + expect(result.current.query.loading).toBeFalsy(); + + await waitForNextUpdate(); + + assertJustExpectedRequestWereMade(env); + + expect(result.current.query.loading).toBeFalsy(); + expect(result.current.query.ok).toBeTruthy(); + + expect(result.current.query.data).toEqual({ + reserves: [ + { + reserve_pub: "11", + } as MerchantBackend.Tips.ReserveStatusEntry, + { + reserve_pub: "22", + } as MerchantBackend.Tips.ReserveStatusEntry, + ], + }); + }); +}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/transfer-pagination.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/transfer-pagination.test.tsx @@ -0,0 +1,120 @@ +/* + 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 { act, renderHook } from "@testing-library/preact-hooks"; +import * as backend from "../../../src/context/backend"; +import * as instance from "../../../src/context/instance"; +import { useInstanceTransfers } from "../../../src/hooks/transfer"; +import { + API_LIST_TRANSFERS, + assertNextRequest, + assertNoMoreRequestWereMade, + AxiosMockEnvironment, +} from "../../axiosMock"; + +jest.mock("axios"); + +describe("transfer pagination", () => { + beforeEach(() => { + jest + .spyOn(backend, "useBackendContext") + .mockImplementation( + () => ({ url: "http://backend", token: "token" } as any) + ); + jest + .spyOn(instance, "useInstanceContext") + .mockImplementation( + () => ({ token: "token", id: "default", admin: true } as any) + ); + }); + + it("should change pagination", async () => { + const env = new AxiosMockEnvironment(); + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: 20, verified: "yes" }, + response: { + transfers: [{ wtid: "1" } as any], + }, + }); + + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: -20, verified: "yes" }, + response: { + transfers: [{ wtid: "3" } as any], + }, + }); + + const updatePosition = (d: string) => { + console.log("updatePosition", d); + }; + + const { result, waitForNextUpdate } = renderHook(() => + useInstanceTransfers({ verified: "yes", position: "" }, updatePosition) + ); + + assertNextRequest(env); + assertNextRequest(env); + assertNoMoreRequestWereMade(env); + + await waitForNextUpdate(); // get info of every product, -> loading + + expect(result.current?.ok).toBeTruthy(); + if (!result.current?.ok) return; + + expect(result.current.data).toEqual({ + transfers: [{ wtid: "1" }, { wtid: "3" }], + }); + + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: -40, verified: "yes" }, + response: { + transfers: [{ wtid: "4" } as any, { wtid: "3" } as any], + }, + }); + + await act(() => { + if (!result.current?.ok) throw Error("not ok"); + result.current.loadMore(); + }); + await waitForNextUpdate(); + assertNextRequest(env); + assertNoMoreRequestWereMade(env); + + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: 40, verified: "yes" }, + response: { + transfers: [{ wtid: "1" } as any, { wtid: "2" } as any], + }, + }); + + await act(() => { + if (!result.current?.ok) throw Error("not ok"); + result.current.loadMorePrev(); + }); + await waitForNextUpdate(); + assertNextRequest(env); + assertNoMoreRequestWereMade(env); + + expect(result.current.data).toEqual({ + transfers: [{ wtid: "2" }, { wtid: "1" }, { wtid: "4" }, { wtid: "3" }], + }); + }); +});