From e21c1b31928cd6bfe90150ea2de19799b6359c40 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 23 Mar 2022 16:20:39 -0300 Subject: [PATCH] splitted rollup config for testing and first component state unit test --- packages/taler-wallet-webextension/encode.mjs | 3 + .../taler-wallet-webextension/package.json | 3 +- .../rollup.config.js | 41 +--- .../rollup.config.test.js | 47 ++++ .../run-wallet-in-browser.html | 14 ++ .../src/hooks/useTalerActionURL.test.ts | 6 +- .../src/stories.test.ts | 1 - .../src/test-utils.ts | 40 ++-- .../wallet/CreateManualWithdraw.stories.tsx | 12 +- .../src/wallet/CreateManualWithdraw.test.ts | 212 ++++++++++++++++++ .../src/wallet/CreateManualWithdraw.tsx | 57 +++-- .../src/wallet/ManualWithdrawPage.tsx | 2 +- .../taler-wallet-webextension/src/wxApi.ts | 2 +- 13 files changed, 349 insertions(+), 91 deletions(-) create mode 100644 packages/taler-wallet-webextension/encode.mjs create mode 100644 packages/taler-wallet-webextension/rollup.config.test.js create mode 100644 packages/taler-wallet-webextension/run-wallet-in-browser.html create mode 100644 packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts diff --git a/packages/taler-wallet-webextension/encode.mjs b/packages/taler-wallet-webextension/encode.mjs new file mode 100644 index 000000000..16de7f642 --- /dev/null +++ b/packages/taler-wallet-webextension/encode.mjs @@ -0,0 +1,3 @@ +import {encodeCrock, stringToBytes} from "../taler-util/lib/talerCrypto.js"; +const pepe =process.argv[2] +console.log(pepe, encodeCrock(stringToBytes(pepe))); diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json index 18563312c..eaad4a4fc 100644 --- a/packages/taler-wallet-webextension/package.json +++ b/packages/taler-wallet-webextension/package.json @@ -12,6 +12,7 @@ "test": "mocha --enable-source-maps 'dist/**/*.test.js'", "test:coverage": "nyc pnpm test", "compile": "rollup -c -m", + "compile:test": "rollup -c rollup.config.test.js -m", "prepare": "rollup -c -m", "build-storybook": "build-storybook", "storybook": "start-storybook -s . -p 6006 --no-open", @@ -84,4 +85,4 @@ "pogen": { "domain": "taler-wallet-webex" } -} +} \ No newline at end of file diff --git a/packages/taler-wallet-webextension/rollup.config.js b/packages/taler-wallet-webextension/rollup.config.js index 8058ce252..8945b48ac 100644 --- a/packages/taler-wallet-webextension/rollup.config.js +++ b/packages/taler-wallet-webextension/rollup.config.js @@ -10,30 +10,7 @@ import css from 'rollup-plugin-css-only'; import ignore from "rollup-plugin-ignore"; import typescript from '@rollup/plugin-typescript'; -import path from 'path'; -import fs from 'fs'; - -function fromDir(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 fromDir(filename, regex); - } - else if (regex.test(filename)) { - return filename - } - }).filter(x => !!x) - - return result -} - -const makePlugins = () => [ +export const makePlugins = () => [ typescript({ outputToFilesystem: false, }), @@ -135,26 +112,10 @@ const webExtensionCryptoWorker = { plugins: makePlugins(), }; -const tests = fromDir('./src', /.test.ts$/).map(test => ({ - input: test, - output: { - file: test.replace(/^src/, 'dist').replace(/\.ts$/, '.js'), - format: "iife", - exports: "none", - name: test, - }, - plugins: [ - ...makePlugins(), - css({ - output: 'walletEntryPoint.css', - }), - ], -})) export default [ webExtensionPopupEntryPoint, webExtensionWalletEntryPoint, webExtensionBackgroundPageScript, webExtensionCryptoWorker, - ...tests, ]; diff --git a/packages/taler-wallet-webextension/rollup.config.test.js b/packages/taler-wallet-webextension/rollup.config.test.js new file mode 100644 index 000000000..387e176bb --- /dev/null +++ b/packages/taler-wallet-webextension/rollup.config.test.js @@ -0,0 +1,47 @@ +// rollup.config.js +import fs from 'fs'; +import path from 'path'; +import css from 'rollup-plugin-css-only'; +import { makePlugins } from "./rollup.config" + +function fromDir(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 fromDir(filename, regex); + } + else if (regex.test(filename)) { + return filename + } + }).filter(x => !!x) + + return result +} + +const tests = fromDir('./src', /.test.ts$/) + .filter(t => t === 'src/wallet/CreateManualWithdraw.test.ts') + .map(test => ({ + input: test, + output: { + file: test.replace(/^src/, 'dist').replace(/\.ts$/, '.js'), + format: "iife", + exports: "none", + name: test, + }, + plugins: [ + ...makePlugins(), + css({ + output: 'walletEntryPoint.css', + }), + ], + })) + +export default [ + ...tests, +]; diff --git a/packages/taler-wallet-webextension/run-wallet-in-browser.html b/packages/taler-wallet-webextension/run-wallet-in-browser.html new file mode 100644 index 000000000..d0ee34ac4 --- /dev/null +++ b/packages/taler-wallet-webextension/run-wallet-in-browser.html @@ -0,0 +1,14 @@ + + + + + + +
+ + + diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts index 99934ef0b..95c77cd76 100644 --- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts +++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ import { useTalerActionURL } from "./useTalerActionURL" -import { justBrowser_it, mountBrowser } from "../test-utils"; +import { mountHook } from "../test-utils"; import { IoCProviderForTesting } from "../context/iocContext"; import { h, VNode } from "preact"; import { act } from "preact/test-utils"; @@ -22,7 +22,7 @@ import { act } from "preact/test-utils"; describe('useTalerActionURL hook', () => { // eslint-disable-next-line jest/expect-expect - justBrowser_it('should be set url to undefined when dismiss', async () => { + it('should be set url to undefined when dismiss', async () => { const ctx = ({ children }: { children: any }): VNode => { return h(IoCProviderForTesting, { @@ -32,7 +32,7 @@ describe('useTalerActionURL hook', () => { }) } - const { result, waitNextUpdate } = mountBrowser(useTalerActionURL, ctx) + const { result, waitNextUpdate } = mountHook(useTalerActionURL, ctx) { const [url] = result.current! diff --git a/packages/taler-wallet-webextension/src/stories.test.ts b/packages/taler-wallet-webextension/src/stories.test.ts index e2d43d5cf..db697460f 100644 --- a/packages/taler-wallet-webextension/src/stories.test.ts +++ b/packages/taler-wallet-webextension/src/stories.test.ts @@ -31,7 +31,6 @@ function testThisStory(st: any): any { const Component = (st as any)[k]; if (k === "default" || !Component) return; - // eslint-disable-next-line jest/expect-expect it(`example: ${k}`, () => { renderNodeOrBrowser(Component, Component.args); diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts index 8c721a9d5..30b37ab8c 100644 --- a/packages/taler-wallet-webextension/src/test-utils.ts +++ b/packages/taler-wallet-webextension/src/test-utils.ts @@ -62,7 +62,9 @@ interface Mounted { waitNextUpdate: () => Promise; } -export function mountBrowser(callback: () => T, Context?: ({ children }: { children: any }) => VNode): Mounted { +const isNode = typeof window === "undefined" + +export function mountHook(callback: () => T, Context?: ({ children }: { children: any }) => VNode): Mounted { const result: { current: T | null } = { current: null } @@ -81,23 +83,6 @@ export function mountBrowser(callback: () => T, Context?: ({ children }: { ch // create the vdom with context if required const vdom = !Context ? create(Component, {}) : create(Context, { children: [create(Component, {})] },); - // in non-browser environment (server side rendering) just serialize to - // string and exit - if (typeof window === "undefined") { - renderToString(vdom); - return { unmount: () => null, result } as any - } - - // do the render into the DOM - const div = document.createElement("div"); - document.body.appendChild(div); - renderIntoDom(vdom, div); - - // clean up callback - function unmount(): any { - document.body.removeChild(div); - } - // waiter callback async function waitNextUpdate(): Promise { await new Promise((res, rej) => { @@ -112,11 +97,22 @@ export function mountBrowser(callback: () => T, Context?: ({ children }: { ch }) } + const customElement = {} as Element + const parentElement = isNode ? customElement : document.createElement("div"); + if (!isNode) { + document.body.appendChild(parentElement); + } + + renderIntoDom(vdom, parentElement); + + // clean up callback + function unmount() { + if (!isNode) { + document.body.removeChild(parentElement); + } + } + return { unmount, result, waitNextUpdate } } - -const nullTestFunction = {} as TestFunction -export const justBrowser_it: PendingTestFunction | TestFunction = - typeof it === 'undefined' ? nullTestFunction : (typeof window === 'undefined' ? it.skip : it) \ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx index e4955e376..407d4ef34 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx @@ -29,30 +29,30 @@ export default { }; // , -const exchangeList = { +const exchangeUrlWithCurrency = { "http://exchange.taler:8081": "COL", "http://exchange.tal": "EUR", }; export const WithoutAnyExchangeKnown = createExample(TestedComponent, { - exchangeList: {}, + exchangeUrlWithCurrency: {}, }); export const InitialState = createExample(TestedComponent, { - exchangeList, + exchangeUrlWithCurrency, }); export const WithAmountInitialized = createExample(TestedComponent, { initialAmount: "10", - exchangeList, + exchangeUrlWithCurrency, }); export const WithExchangeError = createExample(TestedComponent, { error: "The exchange url seems invalid", - exchangeList, + exchangeUrlWithCurrency, }); export const WithAmountError = createExample(TestedComponent, { initialAmount: "e", - exchangeList, + exchangeUrlWithCurrency, }); diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts new file mode 100644 index 000000000..a5174bef9 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { SelectFieldHandler, TextFieldHandler, useComponentState } from "./CreateManualWithdraw"; +import { expect } from "chai"; +import { mountHook } from "../test-utils"; + + +const exchangeListWithARSandUSD = { + "url1": "USD", + "url2": "ARS", + "url3": "ARS", +}; + +const exchangeListEmpty = { +}; + +describe("CreateManualWithdraw states", () => { + it("should set noExchangeFound when exchange list is empty", () => { + const { result } = mountHook(() => + useComponentState(exchangeListEmpty, undefined, undefined), + ); + + if (!result.current) { + expect(result.current).not.to.be.undefined; + return; + } + + expect(result.current.noExchangeFound).equal(true) + }); + + it("should set noExchangeFound when exchange list doesn't include selected currency", () => { + const { result } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, "COL"), + ); + + if (!result.current) { + expect(result.current).not.to.be.undefined; + return; + } + + expect(result.current.noExchangeFound).equal(true) + }); + + + it("should select the first exchange from the list", () => { + const { result } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, undefined), + ); + + if (!result.current) { + expect(result.current).not.to.be.undefined; + return; + } + + expect(result.current.exchange.value).equal("url1") + }); + + it("should select the first exchange with the selected currency", () => { + const { result } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), + ); + + if (!result.current) { + expect(result.current).not.to.be.undefined; + return; + } + + expect(result.current.exchange.value).equal("url2") + }); + + it("should change the exchange when currency change", async () => { + const { result, waitNextUpdate } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), + ); + + if (!result.current) { + expect.fail("hook didn't render"); + } + + expect(result.current.exchange.value).equal("url2") + + result.current.currency.onChange("USD") + + await waitNextUpdate() + + expect(result.current.exchange.value).equal("url1") + + }); + + it("should change the currency when exchange change", async () => { + const { result, waitNextUpdate } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), + ); + + if (!result.current) { + expect.fail("hook didn't render"); + } + + expect(result.current.exchange.value).equal("url2") + expect(result.current.currency.value).equal("ARS") + + result.current.exchange.onChange("url1") + + await waitNextUpdate() + + expect(result.current.exchange.value).equal("url1") + expect(result.current.currency.value).equal("USD") + }); + + it("should update parsed amount when amount change", async () => { + const { result, waitNextUpdate } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), + ); + + if (!result.current) { + expect.fail("hook didn't render"); + } + + expect(result.current.parsedAmount).equal(undefined) + + result.current.amount.onInput("12") + + await waitNextUpdate() + + expect(result.current.parsedAmount).deep.equals({ + value: 12, fraction: 0, currency: "ARS" + }) + }); + + it("should have an amount field", async () => { + const { result, waitNextUpdate } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), + ); + + if (!result.current) { + expect.fail("hook didn't render"); + } + + await defaultTestForInputText(waitNextUpdate, () => result.current!.amount) + }) + + it("should have an exchange selector ", async () => { + const { result, waitNextUpdate } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), + ); + + if (!result.current) { + expect.fail("hook didn't render"); + } + + await defaultTestForInputSelect(waitNextUpdate, () => result.current!.exchange) + }) + + it("should have a currency selector ", async () => { + const { result, waitNextUpdate } = mountHook(() => + useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), + ); + + if (!result.current) { + expect.fail("hook didn't render"); + } + + await defaultTestForInputSelect(waitNextUpdate, () => result.current!.currency) + }) + +}); + + +async function defaultTestForInputText(awaiter: () => Promise, getField: () => TextFieldHandler) { + const initialValue = getField().value; + const otherValue = `${initialValue} something else` + getField().onInput(otherValue) + + await awaiter() + + expect(getField().value).equal(otherValue) +} + + +async function defaultTestForInputSelect(awaiter: () => Promise, getField: () => SelectFieldHandler) { + const initialValue = getField().value; + const keys = Object.keys(getField().list) + const nextIdx = keys.indexOf(initialValue) + 1 + if (keys.length < nextIdx) { + throw new Error('no enough values') + } + const nextValue = keys[nextIdx] + getField().onChange(nextValue) + + await awaiter() + + expect(getField().value).equal(nextValue) +} diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index 068135ae0..2d5129a3d 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -39,20 +39,39 @@ import { Pages } from "../NavigationBar"; export interface Props { error: string | undefined; initialAmount?: string; - exchangeList: Record; + exchangeUrlWithCurrency: Record; onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise; initialCurrency?: string; } +export interface State { + noExchangeFound: boolean; + parsedAmount: AmountJson | undefined; + amount: TextFieldHandler; + currency: SelectFieldHandler; + exchange: SelectFieldHandler; +} + +export interface TextFieldHandler { + onInput: (value: string) => void; + value: string; +} + +export interface SelectFieldHandler { + onChange: (value: string) => void; + value: string; + list: Record; +} + export function useComponentState( - exchangeList: Record, + exchangeUrlWithCurrency: Record, initialAmount: string | undefined, initialCurrency: string | undefined, -) { - const exchangeSelectList = Object.keys(exchangeList); - const currencySelectList = Object.values(exchangeList); +): State { + const exchangeSelectList = Object.keys(exchangeUrlWithCurrency); + const currencySelectList = Object.values(exchangeUrlWithCurrency); const exchangeMap = exchangeSelectList.reduce( - (p, c) => ({ ...p, [c]: `${c} (${exchangeList[c]})` }), + (p, c) => ({ ...p, [c]: `${c} (${exchangeUrlWithCurrency[c]})` }), {} as Record, ); const currencyMap = currencySelectList.reduce( @@ -61,7 +80,7 @@ export function useComponentState( ); const foundExchangeForCurrency = exchangeSelectList.findIndex( - (e) => exchangeList[e] === initialCurrency, + (e) => exchangeUrlWithCurrency[e] === initialCurrency, ); const initialExchange = @@ -73,7 +92,7 @@ export function useComponentState( const [exchange, setExchange] = useState(initialExchange || ""); const [currency, setCurrency] = useState( - initialExchange ? exchangeList[initialExchange] : "", + initialExchange ? exchangeUrlWithCurrency[initialExchange] : "", ); const [amount, setAmount] = useState(initialAmount || ""); @@ -81,12 +100,14 @@ export function useComponentState( function changeExchange(exchange: string): void { setExchange(exchange); - setCurrency(exchangeList[exchange]); + setCurrency(exchangeUrlWithCurrency[exchange]); } function changeCurrency(currency: string): void { setCurrency(currency); - const found = Object.entries(exchangeList).find((e) => e[1] === currency); + const found = Object.entries(exchangeUrlWithCurrency).find( + (e) => e[1] === currency, + ); if (found) { setExchange(found[0]); @@ -95,7 +116,7 @@ export function useComponentState( } } return { - initialExchange, + noExchangeFound: initialExchange === undefined, currency: { list: currencyMap, value: currency, @@ -114,12 +135,12 @@ export function useComponentState( }; } -interface InputHandler { +export interface InputHandler { value: string; onInput: (s: string) => void; } -interface SelectInputHandler { +export interface SelectInputHandler { list: Record; value: string; onChange: (s: string) => void; @@ -127,16 +148,20 @@ interface SelectInputHandler { export function CreateManualWithdraw({ initialAmount, - exchangeList, + exchangeUrlWithCurrency, error, initialCurrency, onCreate, }: Props): VNode { const { i18n } = useTranslationContext(); - const state = useComponentState(exchangeList, initialAmount, initialCurrency); + const state = useComponentState( + exchangeUrlWithCurrency, + initialAmount, + initialCurrency, + ); - if (!state.initialExchange) { + if (state.noExchangeFound) { if (initialCurrency !== undefined) { return (
diff --git a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx index b7e63bbf8..05c518508 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx @@ -110,7 +110,7 @@ export function ManualWithdrawPage({ currency, onCancel }: Props): VNode { return ( diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index ee2a81062..4b0383f88 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -104,7 +104,7 @@ async function callBackend(operation: string, payload: any): Promise { } console.log("got response", response); if (response.type === "error") { - throw new TalerError.fromUncheckedDetail(response.error); + throw TalerError.fromUncheckedDetail(response.error); } return response.result; }