splitted rollup config for testing and first component state unit test

This commit is contained in:
Sebastian 2022-03-23 16:20:39 -03:00
parent 136c39ba9f
commit e21c1b3192
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
13 changed files with 349 additions and 91 deletions

View File

@ -0,0 +1,3 @@
import {encodeCrock, stringToBytes} from "../taler-util/lib/talerCrypto.js";
const pepe =process.argv[2]
console.log(pepe, encodeCrock(stringToBytes(pepe)));

View File

@ -12,6 +12,7 @@
"test": "mocha --enable-source-maps 'dist/**/*.test.js'", "test": "mocha --enable-source-maps 'dist/**/*.test.js'",
"test:coverage": "nyc pnpm test", "test:coverage": "nyc pnpm test",
"compile": "rollup -c -m", "compile": "rollup -c -m",
"compile:test": "rollup -c rollup.config.test.js -m",
"prepare": "rollup -c -m", "prepare": "rollup -c -m",
"build-storybook": "build-storybook", "build-storybook": "build-storybook",
"storybook": "start-storybook -s . -p 6006 --no-open", "storybook": "start-storybook -s . -p 6006 --no-open",

View File

@ -10,30 +10,7 @@ import css from 'rollup-plugin-css-only';
import ignore from "rollup-plugin-ignore"; import ignore from "rollup-plugin-ignore";
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import path from 'path'; export const makePlugins = () => [
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 = () => [
typescript({ typescript({
outputToFilesystem: false, outputToFilesystem: false,
}), }),
@ -135,26 +112,10 @@ const webExtensionCryptoWorker = {
plugins: makePlugins(), 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 [ export default [
webExtensionPopupEntryPoint, webExtensionPopupEntryPoint,
webExtensionWalletEntryPoint, webExtensionWalletEntryPoint,
webExtensionBackgroundPageScript, webExtensionBackgroundPageScript,
webExtensionCryptoWorker, webExtensionCryptoWorker,
...tests,
]; ];

View File

@ -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,
];

View File

@ -0,0 +1,14 @@
<html>
<head>
<link rel="manifest" href="./manifest.json" />
</head>
<body>
<iframe src="./static/popup.html" name="popup" width="500" height="400">
algo
</iframe>
<hr />
<iframe src="./static/wallet.html" name="wallet" width="800" height="100%">
otroe
</iframe>
</body>
</html>

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { useTalerActionURL } from "./useTalerActionURL" import { useTalerActionURL } from "./useTalerActionURL"
import { justBrowser_it, mountBrowser } from "../test-utils"; import { mountHook } from "../test-utils";
import { IoCProviderForTesting } from "../context/iocContext"; import { IoCProviderForTesting } from "../context/iocContext";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { act } from "preact/test-utils"; import { act } from "preact/test-utils";
@ -22,7 +22,7 @@ import { act } from "preact/test-utils";
describe('useTalerActionURL hook', () => { describe('useTalerActionURL hook', () => {
// eslint-disable-next-line jest/expect-expect // 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 => { const ctx = ({ children }: { children: any }): VNode => {
return h(IoCProviderForTesting, { 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! const [url] = result.current!

View File

@ -31,7 +31,6 @@ function testThisStory(st: any): any {
const Component = (st as any)[k]; const Component = (st as any)[k];
if (k === "default" || !Component) return; if (k === "default" || !Component) return;
// eslint-disable-next-line jest/expect-expect // eslint-disable-next-line jest/expect-expect
it(`example: ${k}`, () => { it(`example: ${k}`, () => {
renderNodeOrBrowser(Component, Component.args); renderNodeOrBrowser(Component, Component.args);

View File

@ -62,7 +62,9 @@ interface Mounted<T> {
waitNextUpdate: () => Promise<void>; waitNextUpdate: () => Promise<void>;
} }
export function mountBrowser<T>(callback: () => T, Context?: ({ children }: { children: any }) => VNode): Mounted<T> { const isNode = typeof window === "undefined"
export function mountHook<T>(callback: () => T, Context?: ({ children }: { children: any }) => VNode): Mounted<T> {
const result: { current: T | null } = { const result: { current: T | null } = {
current: null current: null
} }
@ -81,23 +83,6 @@ export function mountBrowser<T>(callback: () => T, Context?: ({ children }: { ch
// create the vdom with context if required // create the vdom with context if required
const vdom = !Context ? create(Component, {}) : create(Context, { children: [create(Component, {})] },); 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 // waiter callback
async function waitNextUpdate(): Promise<void> { async function waitNextUpdate(): Promise<void> {
await new Promise((res, rej) => { await new Promise((res, rej) => {
@ -112,11 +97,22 @@ export function mountBrowser<T>(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 { return {
unmount, result, waitNextUpdate unmount, result, waitNextUpdate
} }
} }
const nullTestFunction = {} as TestFunction
export const justBrowser_it: PendingTestFunction | TestFunction =
typeof it === 'undefined' ? nullTestFunction : (typeof window === 'undefined' ? it.skip : it)

View File

@ -29,30 +29,30 @@ export default {
}; };
// , // ,
const exchangeList = { const exchangeUrlWithCurrency = {
"http://exchange.taler:8081": "COL", "http://exchange.taler:8081": "COL",
"http://exchange.tal": "EUR", "http://exchange.tal": "EUR",
}; };
export const WithoutAnyExchangeKnown = createExample(TestedComponent, { export const WithoutAnyExchangeKnown = createExample(TestedComponent, {
exchangeList: {}, exchangeUrlWithCurrency: {},
}); });
export const InitialState = createExample(TestedComponent, { export const InitialState = createExample(TestedComponent, {
exchangeList, exchangeUrlWithCurrency,
}); });
export const WithAmountInitialized = createExample(TestedComponent, { export const WithAmountInitialized = createExample(TestedComponent, {
initialAmount: "10", initialAmount: "10",
exchangeList, exchangeUrlWithCurrency,
}); });
export const WithExchangeError = createExample(TestedComponent, { export const WithExchangeError = createExample(TestedComponent, {
error: "The exchange url seems invalid", error: "The exchange url seems invalid",
exchangeList, exchangeUrlWithCurrency,
}); });
export const WithAmountError = createExample(TestedComponent, { export const WithAmountError = createExample(TestedComponent, {
initialAmount: "e", initialAmount: "e",
exchangeList, exchangeUrlWithCurrency,
}); });

View File

@ -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/>
*/
/**
*
* @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<void>, 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<void>, 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)
}

View File

@ -39,20 +39,39 @@ import { Pages } from "../NavigationBar";
export interface Props { export interface Props {
error: string | undefined; error: string | undefined;
initialAmount?: string; initialAmount?: string;
exchangeList: Record<string, string>; exchangeUrlWithCurrency: Record<string, string>;
onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>; onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
initialCurrency?: string; 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<string, string>;
}
export function useComponentState( export function useComponentState(
exchangeList: Record<string, string>, exchangeUrlWithCurrency: Record<string, string>,
initialAmount: string | undefined, initialAmount: string | undefined,
initialCurrency: string | undefined, initialCurrency: string | undefined,
) { ): State {
const exchangeSelectList = Object.keys(exchangeList); const exchangeSelectList = Object.keys(exchangeUrlWithCurrency);
const currencySelectList = Object.values(exchangeList); const currencySelectList = Object.values(exchangeUrlWithCurrency);
const exchangeMap = exchangeSelectList.reduce( const exchangeMap = exchangeSelectList.reduce(
(p, c) => ({ ...p, [c]: `${c} (${exchangeList[c]})` }), (p, c) => ({ ...p, [c]: `${c} (${exchangeUrlWithCurrency[c]})` }),
{} as Record<string, string>, {} as Record<string, string>,
); );
const currencyMap = currencySelectList.reduce( const currencyMap = currencySelectList.reduce(
@ -61,7 +80,7 @@ export function useComponentState(
); );
const foundExchangeForCurrency = exchangeSelectList.findIndex( const foundExchangeForCurrency = exchangeSelectList.findIndex(
(e) => exchangeList[e] === initialCurrency, (e) => exchangeUrlWithCurrency[e] === initialCurrency,
); );
const initialExchange = const initialExchange =
@ -73,7 +92,7 @@ export function useComponentState(
const [exchange, setExchange] = useState(initialExchange || ""); const [exchange, setExchange] = useState(initialExchange || "");
const [currency, setCurrency] = useState( const [currency, setCurrency] = useState(
initialExchange ? exchangeList[initialExchange] : "", initialExchange ? exchangeUrlWithCurrency[initialExchange] : "",
); );
const [amount, setAmount] = useState(initialAmount || ""); const [amount, setAmount] = useState(initialAmount || "");
@ -81,12 +100,14 @@ export function useComponentState(
function changeExchange(exchange: string): void { function changeExchange(exchange: string): void {
setExchange(exchange); setExchange(exchange);
setCurrency(exchangeList[exchange]); setCurrency(exchangeUrlWithCurrency[exchange]);
} }
function changeCurrency(currency: string): void { function changeCurrency(currency: string): void {
setCurrency(currency); setCurrency(currency);
const found = Object.entries(exchangeList).find((e) => e[1] === currency); const found = Object.entries(exchangeUrlWithCurrency).find(
(e) => e[1] === currency,
);
if (found) { if (found) {
setExchange(found[0]); setExchange(found[0]);
@ -95,7 +116,7 @@ export function useComponentState(
} }
} }
return { return {
initialExchange, noExchangeFound: initialExchange === undefined,
currency: { currency: {
list: currencyMap, list: currencyMap,
value: currency, value: currency,
@ -114,12 +135,12 @@ export function useComponentState(
}; };
} }
interface InputHandler { export interface InputHandler {
value: string; value: string;
onInput: (s: string) => void; onInput: (s: string) => void;
} }
interface SelectInputHandler { export interface SelectInputHandler {
list: Record<string, string>; list: Record<string, string>;
value: string; value: string;
onChange: (s: string) => void; onChange: (s: string) => void;
@ -127,16 +148,20 @@ interface SelectInputHandler {
export function CreateManualWithdraw({ export function CreateManualWithdraw({
initialAmount, initialAmount,
exchangeList, exchangeUrlWithCurrency,
error, error,
initialCurrency, initialCurrency,
onCreate, onCreate,
}: Props): VNode { }: Props): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const state = useComponentState(exchangeList, initialAmount, initialCurrency); const state = useComponentState(
exchangeUrlWithCurrency,
initialAmount,
initialCurrency,
);
if (!state.initialExchange) { if (state.noExchangeFound) {
if (initialCurrency !== undefined) { if (initialCurrency !== undefined) {
return ( return (
<section> <section>

View File

@ -110,7 +110,7 @@ export function ManualWithdrawPage({ currency, onCancel }: Props): VNode {
return ( return (
<CreateManualWithdraw <CreateManualWithdraw
error={error} error={error}
exchangeList={exchangeList} exchangeUrlWithCurrency={exchangeList}
onCreate={doCreate} onCreate={doCreate}
initialCurrency={currency} initialCurrency={currency}
/> />

View File

@ -104,7 +104,7 @@ async function callBackend(operation: string, payload: any): Promise<any> {
} }
console.log("got response", response); console.log("got response", response);
if (response.type === "error") { if (response.type === "error") {
throw new TalerError.fromUncheckedDetail(response.error); throw TalerError.fromUncheckedDetail(response.error);
} }
return response.result; return response.result;
} }