cherry-pick: demobank-ui: clean up build system

This commit is contained in:
Florian Dold 2022-10-25 01:13:25 +02:00 committed by Sebastian
parent a286649b0a
commit 3685f8cfb8
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
29 changed files with 1430 additions and 6928 deletions

View File

@ -1,19 +1,11 @@
# bank web # Taler Demobank UI
## CLI Commands ## CLI Commands
- `npm install`: Installs dependencies - `pnpm install`: Installs dependencies
- `npm run dev`: Run a development, HMR server - `pnpm run build`: Production-ready build
- `npm run serve`: Run a production-like server - `pnpm run check`: Run type checker
- `npm run build`: Production-ready build - `pnpm run lint`: Pass TypeScript files using ESLint
- `npm run lint`: Pass TypeScript files using ESLint
- `npm run test`: Run Jest and Enzyme with
[`enzyme-adapter-preact-pure`](https://github.com/preactjs/enzyme-adapter-preact-pure) for
your tests
For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md).

160
packages/demobank-ui/build.mjs Executable file
View File

@ -0,0 +1,160 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import esbuild from "esbuild";
import path from "path";
import fs from "fs";
import crypto from "crypto";
import { sassPlugin } from "esbuild-sass-plugin";
const BASE = process.cwd();
const preact = path.join(
BASE,
"node_modules",
"preact",
"compat",
"dist",
"compat.module.js",
);
const preactCompatPlugin = {
name: "preact-compat",
setup(build) {
build.onResolve({ filter: /^(react-dom|react)$/ }, (args) => {
//console.log("onresolve", JSON.stringify(args, undefined, 2));
return {
path: preact,
};
});
},
};
const entryPoints = ["src/index.tsx"];
let GIT_ROOT = BASE;
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
GIT_ROOT = path.join(GIT_ROOT, "../");
}
if (GIT_ROOT === "/") {
console.log("not found");
process.exit(1);
}
const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
function git_hash() {
const rev = fs
.readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
.toString()
.trim()
.split(/.*[: ]/)
.slice(-1)[0];
if (rev.indexOf("/") === -1) {
return rev;
} else {
return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
}
}
// FIXME: Put this into some helper library.
function copyFilesPlugin(options) {
const getDigest = (string) => {
const hash = crypto.createHash("md5");
const data = hash.update(string, "utf-8");
return data.digest("hex");
};
const getFileDigest = (path) => {
if (!fs.existsSync(path)) {
return null;
}
if (fs.statSync(path).isDirectory()) {
return null;
}
return getDigest(fs.readFileSync(path));
};
function filter(src, dest) {
if (!fs.existsSync(dest)) {
return true;
}
if (fs.statSync(dest).isDirectory()) {
return true;
}
return getFileDigest(src) !== getFileDigest(dest);
}
return {
name: "copy-files",
setup(build) {
let src = options.src || "./static";
let dest = options.dest || "./dist";
build.onEnd(() =>
fs.cpSync(src, dest, {
dereference: options.dereference || true,
errorOnExist: options.errorOnExist || false,
filter: options.filter || filter,
force: options.force || true,
preserveTimestamps: options.preserveTimestamps || true,
recursive: options.recursive || true,
}),
);
},
};
}
export const buildConfig = {
entryPoints: [...entryPoints],
bundle: true,
outdir: "dist",
minify: false,
loader: {
".svg": "text",
".png": "dataurl",
".jpeg": "dataurl",
},
target: ["es6"],
format: "esm",
platform: "browser",
sourcemap: true,
jsxFactory: "h",
jsxFragment: "Fragment",
define: {
__VERSION__: `"${_package.version}"`,
__GIT_HASH__: `"${GIT_HASH}"`,
},
plugins: [
preactCompatPlugin,
sassPlugin(),
copyFilesPlugin({
src: "static/index.html",
dest: "dist/index.html",
}),
],
};
esbuild.build(buildConfig).catch((e) => {
console.log(e);
process.exit(1);
});

View File

@ -1,4 +0,0 @@
{
"register": {},
"transactions": {}
}

View File

@ -1,27 +0,0 @@
Object.defineProperty(window, 'requestAnimationFrame', {
value: function(cb) {} // Silence the browser.
})
Object.defineProperty(window, 'localStorage', {
value: {
store: {},
getItem: function(key) {
return this.store[key];
},
setItem: function(key, value) {
return this.store[key] = value;
},
clear: function() {
this.store = {};
}
}
});
Object.defineProperty(window, 'location', {
value: {
origin: "http://localhost:8080", /* where taler-local rev proxy listens to */
search: "",
pathname: "/sandbox/demobanks/default",
}
})
export default window;

View File

@ -4,15 +4,10 @@
"version": "0.1.0", "version": "0.1.0",
"license": "AGPL-3.0-OR-LATER", "license": "AGPL-3.0-OR-LATER",
"scripts": { "scripts": {
"dev": "preact watch --port ${PORT:=9090} --no-sw --no-esm -c preact.mock.js", "build": "./build.mjs",
"build": "preact build --no-sw --no-esm -c preact.single-config.js --dest build && sh remove-link-stylesheet.sh", "check": "tsc",
"serve": "sirv build --port ${PORT:=8080} --cors --single",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"test": "jest ./tests", "pretty": "prettier --write src"
"build-storybook": "build-storybook",
"serve-single": "sirv single --port ${PORT:=8080} --cors --single",
"pretty": "prettier --write src",
"storybook": "start-storybook -p 6006"
}, },
"eslintConfig": { "eslintConfig": {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
@ -24,77 +19,62 @@
"build/" "build/"
], ],
"rules": { "rules": {
"@typescript-eslint/no-explicit-any": [0], "@typescript-eslint/no-explicit-any": [
"@typescript-eslint/ban-ts-comment": [1], 0
"quotes": [2, "single", {"allowTemplateLiterals": true,"avoidEscape": false}], ],
"indent": [2,2], "@typescript-eslint/ban-ts-comment": [
"prefer-arrow-callback": [2, {"allowNamedFunctions": false, "allowUnboundThis": true}], 1
"curly": [2,"multi"], ],
"prefer-template": [1] "quotes": [
2,
"single",
{
"allowTemplateLiterals": true,
"avoidEscape": false
}
],
"indent": [
2,
2
],
"prefer-arrow-callback": [
2,
{
"allowNamedFunctions": false,
"allowUnboundThis": true
}
],
"curly": [
2,
"multi"
],
"prefer-template": [
1
]
} }
}, },
"dependencies": { "dependencies": {
"base64-inline-loader": "1.1.1", "date-fns": "2.29.3",
"date-fns": "2.25.0",
"jed": "1.1.1", "jed": "1.1.1",
"preact": "^10.5.15", "preact-render-to-string": "^5.2.6",
"preact-render-to-string": "^5.1.19", "preact-router": "^4.1.0",
"preact-router": "^3.2.1",
"qrcode-generator": "^1.4.4", "qrcode-generator": "^1.4.4",
"swr": "1.1" "swr": "~1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.18.9",
"@babel/plugin-transform-modules-commonjs": "7.18.6",
"@babel/plugin-transform-react-jsx-source": "7.18.6",
"@babel/preset-env": "7.18.9",
"@creativebulma/bulma-tooltip": "^1.2.0", "@creativebulma/bulma-tooltip": "^1.2.0",
"@gnu-taler/pogen": "^0.0.5", "@gnu-taler/pogen": "^0.0.5",
"@storybook/addon-a11y": "6.2.9", "@typescript-eslint/eslint-plugin": "^5.41.0",
"@storybook/addon-actions": "6.2.9", "@typescript-eslint/parser": "^5.41.0",
"@storybook/addon-essentials": "6.2.9", "bulma": "^0.9.4",
"@storybook/addon-links": "6.2.9",
"@storybook/preact": "6.2.9",
"@storybook/preset-scss": "^1.0.3",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0",
"@types/enzyme": "^3.10.10",
"@types/jest": "^27.0.2",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"babel-loader": "^8.2.2",
"base64-inline-loader": "^1.1.1",
"bulma": "^0.9.3",
"bulma-checkbox": "^1.1.1", "bulma-checkbox": "^1.1.1",
"bulma-radio": "^1.1.1", "bulma-radio": "^1.1.1",
"enzyme": "^3.11.0", "esbuild": "^0.15.12",
"enzyme-adapter-preact-pure": "^3.2.0", "esbuild-sass-plugin": "^2.4.0",
"eslint": "^8.1.0", "eslint": "^8.26.0",
"eslint-config-preact": "^1.2.0", "eslint-config-preact": "^1.2.0",
"html-webpack-inline-chunk-plugin": "^1.1.1",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-skip-assets-plugin": "^1.0.1",
"inline-chunk-html-plugin": "^1.1.1",
"jest": "^27.3.1",
"jest-fetch-mock": "^3.0.3",
"jest-preset-preact": "^4.0.5",
"jest-watch-typeahead": "^1.0.0",
"jest-environment-jsdom": "^27.4.6",
"jssha": "^3.2.0",
"po2json": "^0.4.5", "po2json": "^0.4.5",
"preact-cli": "3.0.5", "preact": "10.11.2",
"sass": "1.32.13",
"sass-loader": "^10",
"script-ext-html-webpack-plugin": "^2.1.5",
"sirv-cli": "^1.0.14",
"typescript": "^4.4.4" "typescript": "^4.4.4"
},
"jest": {
"preset": "jest-preset-preact",
"setupFiles": [
"<rootDir>/tests/__mocks__/browserMocks.ts",
"<rootDir>/tests/__mocks__/setupTests.ts"
]
} }
} }

View File

@ -1,70 +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 { DefinePlugin } from 'webpack';
import pack from './package.json';
import * as cp from 'child_process';
const commitHash = cp.execSync('git rev-parse --short HEAD').toString();
export default {
webpack(config, env, helpers) {
// ensure that process.env will not be undefined on runtime
config.node.process = 'mock'
// add __VERSION__ to be use in the html
config.plugins.push(
new DefinePlugin({
'process.env.__VERSION__': JSON.stringify(env.isProd ? pack.version : `dev-${commitHash}`) ,
}),
);
// suddenly getting out of memory error from build process, error below [1]
// FIXME: remove preact-cli, use rollup
let { index } = helpers.getPluginsByName(config, 'WebpackFixStyleOnlyEntriesPlugin')[0]
config.plugins.splice(index, 1)
}
}
/* [1] from this error decided to remove plugin 'webpack-fix-style-only-entries
leaving this error for future reference
<--- Last few GCs --->
[32479:0x2e01870] 19969 ms: Mark-sweep 1869.4 (1950.2) -> 1443.1 (1504.1) MB, 497.5 / 0.0 ms (average mu = 0.631, current mu = 0.455) allocation failure scavenge might not succeed
[32479:0x2e01870] 21907 ms: Mark-sweep 2016.9 (2077.9) -> 1628.6 (1681.4) MB, 1596.0 / 0.0 ms (average mu = 0.354, current mu = 0.176) allocation failure scavenge might not succeed
<--- JS stacktrace --->
==== JS stack trace =========================================
0: ExitFrame [pc: 0x13cf099]
Security context: 0x2f4ca66c08d1 <JSObject>
1: /* anonymous * / [0x35d05555b4b9] [...path/merchant-backoffice/node_modules/.pnpm/webpack-fix-style-only-entries@0.5.2/node_modules/webpack-fix-style-only-entries/index.js:~80] [pc=0x2145e699d1a4](this=0x1149465410e9 <GlobalObject Object map = 0xff481b5b5f9>,0x047e52e36a49 <Dependency map = 0x1ed1fe41cd19>)
2: arguments adaptor frame: 3...
FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory
*/

View File

@ -1,55 +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 { DefinePlugin, ProvidePlugin } from 'webpack';
import pack from './package.json';
import * as cp from 'child_process';
const commitHash = cp.execSync('git rev-parse --short HEAD').toString();
import path from 'path';
export default {
webpack(config, env, helpers) {
// Ensure that process.env will not be undefined at runtime.
config.node.process = 'mock'
let DEMO_SITES = {
"Blog": process.env.TALER_ENV_URL_MERCHANT_BLOG,
"Donations": process.env.TALER_ENV_URL_MERCHANT_DONATIONS,
"Survey": process.env.TALER_ENV_URL_MERCHANT_SURVEY,
"Landing": process.env.TALER_ENV_URL_INTRO,
"Bank": process.env.TALER_ENV_URL_BANK,
}
console.log("demo links found", DEMO_SITES);
// Add __VERSION__ to be use in the html.
config.plugins.push(
new DefinePlugin({
'process.env.__VERSION__': JSON.stringify(env.isProd ? pack.version : `dev-${commitHash}`) ,
}),
// 'window' gets mocked to point at a running euFin instance.
new ProvidePlugin({window: path.resolve("mocks/window")}),
new DefinePlugin({"DEMO_SITES": JSON.stringify(DEMO_SITES)})
);
let { index } = helpers.getPluginsByName(config, 'WebpackFixStyleOnlyEntriesPlugin')[0]
config.plugins.splice(index, 1)
}
}

View File

@ -1,60 +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 defaultConfig from './preact.config'
export default {
webpack(config, env, helpers, options) {
defaultConfig.webpack(config, env, helpers, options)
//1. check no file is under /routers or /component/{routers,async} to prevent async components
// https://github.com/preactjs/preact-cli#route-based-code-splitting
//2. remove devtools to prevent sourcemaps
config.devtool = false
//3. change assetLoader to load assets inline
const loaders = helpers.getLoaders(config)
const assetsLoader = loaders.find(lo => lo.rule.test.test('something.woff'))
if (assetsLoader) {
assetsLoader.rule.use = 'base64-inline-loader'
assetsLoader.rule.loader = undefined
}
//4. remove critters
//critters remove the css bundle from htmlWebpackPlugin.files.css
//for now, pushing all the content into the html is enough
const crittersWrapper = helpers.getPluginsByName(config, 'Critters')
if (crittersWrapper && crittersWrapper.length > 0) {
const [{ index }] = crittersWrapper
config.plugins.splice(index, 1)
}
//5. remove favicon from src/assets
//6. remove performance hints since we now that this is going to be big
if (config.performance) {
config.performance.hints = false
}
//7. template.html should have a favicon and add js/css content
}
}

View File

@ -1,8 +0,0 @@
# This script has been placed in the public domain.
FILE=$(ls build/bundle.*.css)
BUNDLE=${FILE#build}
grep -q '<link href="'$BUNDLE'" rel="stylesheet">' build/index.html || { echo bundle $BUNDLE not found in index.html; exit 1; }
echo -n Removing link from index.html ...
sed 's_<link href="'$BUNDLE'" rel="stylesheet">__' -i build/index.html
echo done

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0l-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z"/></svg>

Before

Width:  |  Height:  |  Size: 274 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 15h2v2h-2zM17 11h2v2h-2zM17 7h2v2h-2zM13.74 7l1.26.84V7z"/><path d="M10 3v1.51l2 1.33V5h9v14h-4v2h6V3z"/><path d="M8.17 5.7L15 10.25V21H1V10.48L8.17 5.7zM10 19h3v-7.84L8.17 8.09 3 11.38V19h3v-6h4v6z"/></svg>

Before

Width:  |  Height:  |  Size: 359 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 23.59v-3.6c-5.01-.26-9-4.42-9-9.49C2 5.26 6.26 1 11.5 1S21 5.26 21 10.5c0 4.95-3.44 9.93-8.57 12.4l-1.43.69zM11.5 3C7.36 3 4 6.36 4 10.5S7.36 18 11.5 18H13v2.3c3.64-2.3 6-6.08 6-9.8C19 6.36 15.64 3 11.5 3zm-1 11.5h2v2h-2zm2-1.5h-2c0-3.25 3-3 3-5 0-1.1-.9-2-2-2s-2 .9-2 2h-2c0-2.21 1.79-4 4-4s4 1.79 4 4c0 2.5-3 2.75-3 5z"/></svg>

Before

Width:  |  Height:  |  Size: 483 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 1.01L7 1c-1.1 0-1.99.9-1.99 2v18c0 1.1.89 2 1.99 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z"/></svg>

Before

Width:  |  Height:  |  Size: 272 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M18,10.48V6c0-1.1-0.9-2-2-2H4C2.9,4,2,4.9,2,6v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-4.48l4,3.98v-11L18,10.48z M16,9.69V18H4V6h12V9.69z"/><circle cx="10" cy="10" r="2"/><path d="M14,15.43c0-0.81-0.48-1.53-1.22-1.85C11.93,13.21,10.99,13,10,13c-0.99,0-1.93,0.21-2.78,0.58C6.48,13.9,6,14.62,6,15.43 V16h8V15.43z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 525 B

View File

@ -1,7 +1,6 @@
import { FunctionalComponent, h } from 'preact'; import { FunctionalComponent } from "preact";
import { TranslationProvider } from '../context/translation'; import { TranslationProvider } from "../context/translation";
import { BankHome } from '../pages/home/index'; import { BankHome } from "../pages/home/index";
import { Menu } from './menu';
const App: FunctionalComponent = () => { const App: FunctionalComponent = () => {
return ( return (

View File

@ -19,14 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { StateUpdater, useState } from 'preact/hooks'; import { StateUpdater, useState } from "preact/hooks";
export type ValueOrFunction<T> = T | ((p: T) => T); export type ValueOrFunction<T> = T | ((p: T) => T);
const calculateRootPath = () => { const calculateRootPath = () => {
const rootPath = const rootPath =
typeof window !== undefined typeof window !== undefined
? window.location.origin + window.location.pathname ? window.location.origin + window.location.pathname
: '/'; : "/";
return rootPath; return rootPath;
}; };
@ -34,14 +34,14 @@ export function useBackendURL(
url?: string, url?: string,
): [string, boolean, StateUpdater<string>, () => void] { ): [string, boolean, StateUpdater<string>, () => void] {
const [value, setter] = useNotNullLocalStorage( const [value, setter] = useNotNullLocalStorage(
'backend-url', "backend-url",
url || calculateRootPath(), url || calculateRootPath(),
); );
const [triedToLog, setTriedToLog] = useLocalStorage('tried-login'); const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
const checkedSetter = (v: ValueOrFunction<string>) => { const checkedSetter = (v: ValueOrFunction<string>) => {
setTriedToLog('yes'); setTriedToLog("yes");
return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, '')); return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
}; };
const resetBackend = () => { const resetBackend = () => {
@ -53,8 +53,8 @@ export function useBackendURL(
export function useBackendDefaultToken(): [ export function useBackendDefaultToken(): [
string | undefined, string | undefined,
StateUpdater<string | undefined>, StateUpdater<string | undefined>,
] { ] {
return useLocalStorage('backend-token'); return useLocalStorage("backend-token");
} }
export function useBackendInstanceToken( export function useBackendInstanceToken(
@ -64,59 +64,60 @@ export function useBackendInstanceToken(
const [defaultToken, defaultSetToken] = useBackendDefaultToken(); const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token // instance named 'default' use the default token
if (id === 'default') if (id === "default") return [defaultToken, defaultSetToken];
return [defaultToken, defaultSetToken];
return [token, setToken]; return [token, setToken];
} }
export function useLang(initial?: string): [string, StateUpdater<string>] { export function useLang(initial?: string): [string, StateUpdater<string>] {
const browserLang = const browserLang =
typeof window !== 'undefined' typeof window !== "undefined"
? navigator.language || (navigator as any).userLanguage ? navigator.language || (navigator as any).userLanguage
: undefined; : undefined;
const defaultLang = (browserLang || initial || 'en').substring(0, 2); const defaultLang = (browserLang || initial || "en").substring(0, 2);
const [value, setValue] = useNotNullLocalStorage('lang-preference', defaultLang); const [value, setValue] = useNotNullLocalStorage(
function updateValue(newValue: (string | ((v: string) => string))) { "lang-preference",
defaultLang,
);
function updateValue(newValue: string | ((v: string) => string)) {
if (document.body.parentElement) { if (document.body.parentElement) {
const htmlElement = document.body.parentElement const htmlElement = document.body.parentElement;
if (typeof newValue === 'string') { if (typeof newValue === "string") {
htmlElement.lang = newValue; htmlElement.lang = newValue;
setValue(newValue) setValue(newValue);
} else if (typeof newValue === 'function') } else if (typeof newValue === "function")
setValue((old) => { setValue((old) => {
const nv = newValue(old) const nv = newValue(old);
htmlElement.lang = nv; htmlElement.lang = nv;
return nv return nv;
}) });
} else setValue(newValue) } else setValue(newValue);
} }
return [value, updateValue] return [value, updateValue];
} }
export function useLocalStorage( export function useLocalStorage(
key: string, key: string,
initialValue?: string, initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] { ): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>((): const [storedValue, setStoredValue] = useState<string | undefined>(
| string (): string | undefined => {
| undefined => { return typeof window !== "undefined"
return typeof window !== 'undefined'
? window.localStorage.getItem(key) || initialValue ? window.localStorage.getItem(key) || initialValue
: initialValue; : initialValue;
}); },
);
const setValue = ( const setValue = (
value?: string | ((val?: string) => string | undefined), value?: string | ((val?: string) => string | undefined),
) => { ) => {
setStoredValue((p) => { setStoredValue((p) => {
console.log("calling setStoredValue");
console.log(window);
const toStore = value instanceof Function ? value(p) : value; const toStore = value instanceof Function ? value(p) : value;
if (typeof window !== 'undefined') if (typeof window !== "undefined")
if (!toStore) if (!toStore) window.localStorage.removeItem(key);
window.localStorage.removeItem(key); else window.localStorage.setItem(key, toStore);
else
window.localStorage.setItem(key, toStore);
return toStore; return toStore;
}); });
@ -130,7 +131,7 @@ export function useNotNullLocalStorage(
initialValue: string, initialValue: string,
): [string, StateUpdater<string>] { ): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): string => { const [storedValue, setStoredValue] = useState<string>((): string => {
return typeof window !== 'undefined' return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue ? window.localStorage.getItem(key) || initialValue
: initialValue; : initialValue;
}); });
@ -138,13 +139,9 @@ export function useNotNullLocalStorage(
const setValue = (value: string | ((val: string) => string)) => { const setValue = (value: string | ((val: string) => string)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value; const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore); setStoredValue(valueToStore);
if (typeof window !== 'undefined') if (typeof window !== "undefined")
if (!valueToStore) if (!valueToStore) window.localStorage.removeItem(key);
window.localStorage.removeItem(key); else window.localStorage.setItem(key, valueToStore);
else
window.localStorage.setItem(key, valueToStore);
}; };
return [storedValue, setValue]; return [storedValue, setValue];

View File

@ -0,0 +1,34 @@
<!--
This file is part of GNU Taler
(C) 2021--2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
@author Sebastian Javier Marchano
-->
<!DOCTYPE html>
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="icon" href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
</head>
<body>
<div id="app"></div>
<script type="module" src="index.tsx"></script>
</body>
</html>

View File

@ -1,3 +1,7 @@
import App from './components/app'; import App from "./components/app";
export default App; export default App;
import { render, h, Fragment } from "preact";
const app = document.getElementById("app");
render(<App />, app as any);

View File

@ -17,6 +17,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import useSWR, { SWRConfig as _SWRConfig, useSWRConfig } from "swr"; import useSWR, { SWRConfig as _SWRConfig, useSWRConfig } from "swr";
import { h, Fragment, VNode, createContext } from "preact"; import { h, Fragment, VNode, createContext } from "preact";
import { import {
useRef, useRef,
useState, useState,
@ -24,20 +25,37 @@ import {
StateUpdater, StateUpdater,
useContext, useContext,
} from "preact/hooks"; } from "preact/hooks";
import { Buffer } from "buffer"; import { useTranslator, Translate } from "../../i18n/index.js";
import { useTranslator, Translate } from "../../i18n"; import { QR } from "../../components/QR.js";
import { QR } from "../../components/QR"; import { useNotNullLocalStorage, useLocalStorage } from "../../hooks/index.js";
import { useNotNullLocalStorage, useLocalStorage } from "../../hooks";
import "../../scss/main.scss"; import "../../scss/main.scss";
import talerLogo from "../../assets/logo-white.svg"; import talerLogo from "../../assets/logo-white.svg";
import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector"; import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js";
// FIXME: Fix usages of SWRConfig, doing this isn't the best practice (but hey, it works for now) // FIXME: Fix usages of SWRConfig, doing this isn't the best practice (but hey, it works for now)
const SWRConfig = _SWRConfig as any; const SWRConfig = _SWRConfig as any;
const UI_ALLOW_REGISTRATIONS = "__LIBEUFIN_UI_ALLOW_REGISTRATIONS__" ?? 1; /**
const UI_IS_DEMO = "__LIBEUFIN_UI_IS_DEMO__" ?? 0; * If the first argument does not look like a placeholder, return it.
const UI_BANK_NAME = "__LIBEUFIN_UI_BANK_NAME__" ?? "Taler Bank"; * Otherwise, return the default.
*
* Useful for placeholder string replacements optionally
* done as part of the build system.
*/
const replacementOrDefault = (x: string, defaultVal: string) => {
if (x.startsWith("__")) {
return defaultVal;
}
return x;
};
const UI_ALLOW_REGISTRATIONS =
replacementOrDefault("__LIBEUFIN_UI_ALLOW_REGISTRATIONS__", "1") == "1";
const UI_IS_DEMO = replacementOrDefault("__LIBEUFIN_UI_IS_DEMO__", "0") == "1";
const UI_BANK_NAME = replacementOrDefault(
"__LIBEUFIN_UI_BANK_NAME__",
"Taler Bank",
);
/** /**
* FIXME: * FIXME:
@ -156,15 +174,6 @@ function maybeDemoContent(content: VNode) {
if (UI_IS_DEMO) return content; if (UI_IS_DEMO) return content;
} }
async function fetcher(url: string) {
return fetch(url).then((r) => r.json());
}
function genCaptchaNumbers(): string {
return `${Math.floor(Math.random() * 10)} + ${Math.floor(
Math.random() * 10,
)}`;
}
/** /**
* Bring the state to show the public accounts page. * Bring the state to show the public accounts page.
*/ */
@ -276,22 +285,26 @@ function prepareHeaders(username: string, password: string) {
const headers = new Headers(); const headers = new Headers();
headers.append( headers.append(
"Authorization", "Authorization",
`Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`, `Basic ${window.btoa(`${username}:${password}`)}`,
); );
headers.append("Content-Type", "application/json"); headers.append("Content-Type", "application/json");
return headers; return headers;
} }
// Window can be mocked this way: const getBankBackendBaseUrl = () => {
// https://gist.github.com/theKashey/07090691c0a4680ed773375d8dbeebc1#file-webpack-conf-js const overrideUrl = localStorage.getItem("bank-base-url");
// That allows the app to be pointed to a arbitrary if (overrideUrl) {
// euFin backend when launched via "pnpm dev". console.log(
const getRootPath = () => { `using bank base URL ${overrideUrl} (override via bank-base-url localStorage)`,
);
return overrideUrl;
}
const maybeRootPath = const maybeRootPath =
typeof window !== undefined typeof window !== undefined
? window.location.origin + window.location.pathname ? window.location.origin + window.location.pathname
: "/"; : "/";
if (!maybeRootPath.endsWith("/")) return `${maybeRootPath}/`; if (!maybeRootPath.endsWith("/")) return `${maybeRootPath}/`;
console.log(`using bank base URL (${maybeRootPath})`);
return maybeRootPath; return maybeRootPath;
}; };
@ -785,7 +798,7 @@ async function loginCall(
* let the Account component request the balance to check * let the Account component request the balance to check
* whether the credentials are valid. */ * whether the credentials are valid. */
pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true })); pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
let baseUrl = getRootPath(); let baseUrl = getBankBackendBaseUrl();
if (!baseUrl.endsWith("/")) baseUrl += "/"; if (!baseUrl.endsWith("/")) baseUrl += "/";
backendStateSetter((prevState) => ({ backendStateSetter((prevState) => ({
@ -813,7 +826,7 @@ async function registrationCall(
backendStateSetter: StateUpdater<BackendStateTypeOpt>, backendStateSetter: StateUpdater<BackendStateTypeOpt>,
pageStateSetter: StateUpdater<PageStateType>, pageStateSetter: StateUpdater<PageStateType>,
) { ) {
let baseUrl = getRootPath(); let baseUrl = getBankBackendBaseUrl();
/** /**
* If the base URL doesn't end with slash and the path * If the base URL doesn't end with slash and the path
* is not empty, then the concatenation made by URL() * is not empty, then the concatenation made by URL()
@ -873,19 +886,6 @@ async function registrationCall(
* Functional components. * * Functional components. *
*************************/ *************************/
function Currency(): VNode {
const { data, error } = useSWR(
`${getRootPath()}integration-api/config`,
fetcher,
);
if (typeof error !== "undefined")
return <b>error: currency could not be retrieved</b>;
if (typeof data === "undefined") return <Fragment>"..."</Fragment>;
console.log("found bank config", data);
return data.currency;
}
function ErrorBanner(Props: any): VNode | null { function ErrorBanner(Props: any): VNode | null {
const [pageState, pageStateSetter] = Props.pageState; const [pageState, pageStateSetter] = Props.pageState;
const i18n = useTranslator(); const i18n = useTranslator();
@ -2043,10 +2043,7 @@ function Account(Props: any): VNode {
function SWRWithCredentials(props: any): VNode { function SWRWithCredentials(props: any): VNode {
const { username, password, backendUrl } = props; const { username, password, backendUrl } = props;
const headers = new Headers(); const headers = new Headers();
headers.append( headers.append("Authorization", `Basic ${btoa(`${username}:${password}`)}`);
"Authorization",
`Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
);
console.log("Likely backend base URL", backendUrl); console.log("Likely backend base URL", backendUrl);
return ( return (
<SWRConfig <SWRConfig
@ -2179,13 +2176,11 @@ function PublicHistories(Props: any): VNode {
export function BankHome(): VNode { export function BankHome(): VNode {
const [backendState, backendStateSetter] = useBackendState(); const [backendState, backendStateSetter] = useBackendState();
const [pageState, pageStateSetter] = usePageState(); const [pageState, pageStateSetter] = usePageState();
const [accountState, accountStateSetter] = useAccountState();
const setTxPageNumber = useTransactionPageNumber()[1];
const i18n = useTranslator(); const i18n = useTranslator();
if (pageState.showPublicHistories) if (pageState.showPublicHistories)
return ( return (
<SWRWithoutCredentials baseUrl={getRootPath()}> <SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
<PageContext.Provider value={[pageState, pageStateSetter]}> <PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame> <BankFrame>
<PublicHistories pageStateSetter={pageStateSetter}> <PublicHistories pageStateSetter={pageStateSetter}>

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +0,0 @@
<!--
This file is part of GNU Taler
(C) 2021--2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
@author Sebastian Javier Marchano
-->
<!DOCTYPE html>
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="icon" href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
<meta name="theme-color" content="<%= htmlWebpackPlugin.options.manifest.theme_color %>">
<% } %>
<% for (const index in htmlWebpackPlugin.files.css) { %>
<% const file = htmlWebpackPlugin.files.css[index] %>
<style data-href='<%= file %>' >
<%= compilation.assets[file.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</style>
<% } %>
</head>
<body>
<script>
<%= compilation.assets[htmlWebpackPlugin.files.chunks["polyfills"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</script>
<script>
<%= compilation.assets[htmlWebpackPlugin.files.chunks["bundle"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</script>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Demobank</title>
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -1,21 +0,0 @@
// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
/**
* An example how to mock localStorage is given below 👇
*/
/*
// Mocks localStorage
const localStorageMock = (function() {
let store = {};
return {
getItem: (key) => store[key] || null,
setItem: (key, value) => store[key] = value.toString(),
clear: () => store = {}
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
}); */

View File

@ -1,3 +0,0 @@
// This fixed an error related to the CSS and loading gif breaking my Jest test
// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
export default 'test-file-stub';

View File

@ -1,6 +0,0 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-preact-pure';
configure({
adapter: new Adapter() as any
});

View File

@ -1,466 +0,0 @@
import "core-js/stable";
import "regenerator-runtime/runtime";
import "@testing-library/jest-dom";
import { BankHome } from '../../src/pages/home';
import { h } from 'preact';
import { waitFor, cleanup, render, fireEvent, screen } from '@testing-library/preact';
import expect from 'expect';
import fetchMock from "jest-fetch-mock";
/**
* This mock makes the translator always return the
* english string. It didn't work within the 'beforeAll'
* function...
*/
jest.mock("../../src/i18n")
const i18n = require("../../src/i18n")
i18n.useTranslator.mockImplementation(() => function(arg) {return arg})
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: {
origin: "http://localhost",
pathname: "/demobanks/default"
}
})
global.Storage.prototype.setItem = jest.fn((key, value) => {})
})
function fillCredentialsForm() {
const username = Math.random().toString().substring(2);
const u = screen.getByPlaceholderText("username");
const p = screen.getByPlaceholderText("password");
fireEvent.input(u, {target: {value: username}})
fireEvent.input(p, {target: {value: "bar"}})
const signinButton = screen.getByText("Login");
return {
username: username,
signinButton: signinButton
};
}
fetchMock.enableMocks();
function mockSuccessLoginOrRegistration() {
fetch.once("{}", {
status: 200
}).once(JSON.stringify({
balance: {
amount: "EUR:10",
credit_debit_indicator: "credit"
},
paytoUri: "payto://iban/123/ABC"
}))
}
/**
* Render homepage -> navigate to register page -> submit registration.
* 'webMock' is called before submission to mock the server response
*/
function signUp(context, webMock) {
render(<BankHome />);
const registerPage = screen.getByText("Register!");
fireEvent.click(registerPage);
const username = Math.random().toString().substring(2);
const u = screen.getByPlaceholderText("username");
const p = screen.getByPlaceholderText("password");
fireEvent.input(u, {target: {value: username}})
fireEvent.input(p, {target: {value: "bar"}})
const registerButton = screen.getByText("Register");
webMock();
fireEvent.click(registerButton);
context.username = username;
return context;
}
describe("wire transfer", () => {
beforeEach(() => {
signUp({}, mockSuccessLoginOrRegistration); // context unused
})
test("Wire transfer success", async () => {
const transferButton = screen.getByText("Create wire transfer");
const payto = screen.getByPlaceholderText("payto address");
fireEvent.input(payto, {target: {value: "payto://only-checked-by-the-backend!"}})
fetch.once("{}"); // 200 OK
fireEvent.click(transferButton);
await screen.findByText("wire transfer created", {exact: false})
})
test("Wire transfer fail", async () => {
const transferButton = screen.getByText("Create wire transfer");
const payto = screen.getByPlaceholderText("payto address");
fireEvent.input(payto, {target: {value: "payto://only-checked-by-the-backend!"}})
fetch.once("{}", {status: 400});
fireEvent.click(transferButton);
// assert this below does NOT appear.
await waitFor(() => expect(
screen.queryByText("wire transfer created", {exact: false})).not.toBeInTheDocument());
})
})
describe("withdraw", () => {
afterEach(() => {
fetch.resetMocks();
cleanup();
})
let context = {};
// Register and land on the profile page.
beforeEach(() => {
context = signUp(context, mockSuccessLoginOrRegistration);
})
test("network failure before withdrawal creation", async () => {
const a = screen.getAllByPlaceholderText("amount")[0];
fireEvent.input(a, {target: {value: "10"}});
let withdrawButton = screen.getByText("Charge Taler wallet");
// mock network failure.
fetch.mockReject("API is down");
fireEvent.click(withdrawButton);
await screen.findByText("could not create withdrawal operation", {exact: false})
})
test("HTTP response error upon withdrawal creation", async () => {
const a = screen.getAllByPlaceholderText("amount")[0];
fireEvent.input(a, {target: {value: "10,0"}});
let withdrawButton = screen.getByText("Charge Taler wallet");
fetch.once("{}", {status: 404});
fireEvent.click(withdrawButton);
await screen.findByText("gave response error", {exact: false})
})
test("Abort withdrawal", async () => {
const a = screen.getAllByPlaceholderText("amount")[0];
fireEvent.input(a, {target: {value: "10,0"}});
let withdrawButton = screen.getByText("Charge Taler wallet");
fetch.once(JSON.stringify({
taler_withdraw_uri: "taler://withdraw/foo",
withdrawal_id: "foo"
}));
/**
* After triggering a withdrawal, check if the taler://withdraw URI
* rendered, and confirm if so. Lastly, check that a success message
* appeared on the screen.
*/
fireEvent.click(withdrawButton);
const abortButton = await screen.findByText("abort withdrawal", {exact: false})
fireEvent.click(abortButton);
expect(fetch).toHaveBeenLastCalledWith(
`http://localhost/demobanks/default/access-api/accounts/${context.username}/withdrawals/foo/abort`,
expect.anything()
)
await waitFor(() => expect(
screen.queryByText("abort withdrawal", {exact: false})).not.toBeInTheDocument());
})
test("Successful withdrawal creation and confirmation", async () => {
const a = screen.getAllByPlaceholderText("amount")[0];
fireEvent.input(a, {target: {value: "10,0"}});
let withdrawButton = await screen.findByText("Charge Taler wallet");
fetch.once(JSON.stringify({
taler_withdraw_uri: "taler://withdraw/foo",
withdrawal_id: "foo"
}));
/**
* After triggering a withdrawal, check if the taler://withdraw URI
* rendered, and confirm if so. Lastly, check that a success message
* appeared on the screen. */
fireEvent.click(withdrawButton);
expect(fetch).toHaveBeenCalledWith(
`http://localhost/demobanks/default/access-api/accounts/${context.username}/withdrawals`,
expect.objectContaining({body: JSON.stringify({amount: "EUR:10.0"})})
)
// assume wallet POSTed the payment details.
const confirmButton = await screen.findByText("confirm withdrawal", {exact: false})
/**
* Not expecting a new withdrawal possibility while one is being processed.
*/
await waitFor(() => expect(
screen.queryByText("charge taler wallet", {exact: false})).not.toBeInTheDocument());
fetch.once("{}")
// Confirm currently processed withdrawal.
fireEvent.click(confirmButton);
/**
* After having confirmed above, wait that the
* pre-withdrawal elements disappears and a success
* message appears.
*/
await waitFor(() => expect(
screen.queryByText(
"confirm withdrawal",
{exact: false})).not.toBeInTheDocument()
);
await waitFor(() => expect(
screen.queryByText(
"give this address to the taler wallet",
{exact: false})).not.toBeInTheDocument()
);
expect(fetch).toHaveBeenLastCalledWith(
`http://localhost/demobanks/default/access-api/accounts/${context.username}/withdrawals/foo/confirm`,
expect.anything())
// success message
await screen.findByText("withdrawal confirmed", {exact: false})
/**
* Click on a "return to homepage / close" button, and
* check that the withdrawal confirmation is gone, and
* the option to withdraw again reappeared.
*/
const closeButton = await screen.findByText("close", {exact: false})
fireEvent.click(closeButton);
/**
* After closing the operation, the confirmation message is not expected.
*/
await waitFor(() => expect(
screen.queryByText("withdrawal confirmed", {exact: false})).not.toBeInTheDocument()
);
/**
* After closing the operation, the possibility to withdraw again should be offered.
*/
await waitFor(() => expect(
screen.queryByText(
"charge taler wallet",
{exact: false})).toBeInTheDocument()
);
})
})
describe("home page", () => {
afterEach(() => {
fetch.resetMocks();
cleanup();
})
test("public histories", async () => {
render(<BankHome />);
/**
* Mock list of public accounts. 'bar' is
* the shown account, since it occupies the last
* position (and SPA picks it via the 'pop()' method) */
fetch.once(JSON.stringify({
"publicAccounts" : [ {
"balance" : "EUR:1",
"iban" : "XXX",
"accountLabel" : "foo"
}, {
"balance" : "EUR:2",
"iban" : "YYY",
"accountLabel" : "bar"
}]
})).once(JSON.stringify({
transactions: [{
debtorIban: "XXX",
debtorBic: "YYY",
debtorName: "Foo",
creditorIban: "AAA",
creditorBic: "BBB",
creditorName: "Bar",
direction: "DBIT",
amount: "EUR:5",
subject: "Reimbursement",
date: "1970-01-01"
}, {
debtorIban: "XXX",
debtorBic: "YYY",
debtorName: "Foo",
creditorIban: "AAA",
creditorBic: "BBB",
creditorName: "Bar",
direction: "CRDT",
amount: "EUR:5",
subject: "Bonus",
date: "2000-01-01"
}]
})).once(JSON.stringify({
transactions: [{
debtorIban: "XXX",
debtorBic: "YYY",
debtorName: "Foo",
creditorIban: "AAA",
creditorBic: "BBB",
creditorName: "Bar",
direction: "DBIT",
amount: "EUR:5",
subject: "Donation",
date: "1970-01-01"
}, {
debtorIban: "XXX",
debtorBic: "YYY",
debtorName: "Foo",
creditorIban: "AAA",
creditorBic: "BBB",
creditorName: "Bar",
direction: "CRDT",
amount: "EUR:5",
subject: "Refund",
date: "2000-01-01"
}]
}))
// Navigate to dedicate public histories page.
const publicTxsPage = screen.getByText("transactions");
fireEvent.click(publicTxsPage);
/**
* Check that transactions data appears on the page.
*/
await screen.findByText("reimbursement", {exact: false});
await screen.findByText("bonus", {exact: false});
/**
* The transactions below should not appear, because only
* one public account renders.
*/
await waitFor(() => expect(
screen.queryByText("refund", {exact: false})).not.toBeInTheDocument());
await waitFor(() => expect(
screen.queryByText("donation", {exact: false})).not.toBeInTheDocument());
/**
* First HTTP mock:
*/
await expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/public-accounts"
)
/**
* Only expecting this request (second mock), as SWR doesn't let
* the unshown history request to the backend:
*/
await expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/accounts/bar/transactions?page=0"
)
/**
* Switch tab:
*/
let fooTab = await screen.findByText("foo", {exact: false});
fireEvent.click(fooTab);
/**
* Last two HTTP mocks should render now:
*/
await screen.findByText("refund", {exact: false});
await screen.findByText("donation", {exact: false});
// Expect SWR to have requested 'foo' history
// (consuming the last HTTP mock):
await expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/accounts/foo/transactions?page=0"
)
let backButton = await screen.findByText("Go back", {exact: false});
fireEvent.click(backButton);
await waitFor(() => expect(
screen.queryByText("donation", {exact: false})).not.toBeInTheDocument());
await screen.findByText("welcome to eufin bank", {exact: false})
})
// check page informs about the current balance
// after a successful registration.
test("new registration response error 404", async () => {
var context = signUp({}, () => fetch.mockResponseOnce("Not found", {status: 404}));
await screen.findByText("has a problem", {exact: false});
expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/testing/register",
expect.objectContaining(
{body: JSON.stringify({username: context.username, password: "bar"}), method: "POST"},
))
})
test("registration network failure", async () => {
let context = signUp({}, ()=>fetch.mockReject("API is down"));
await screen.findByText("has a problem", {exact: false});
expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/testing/register",
expect.objectContaining(
{body: JSON.stringify({username: context.username, password: "bar"}), method: "POST"}
))
})
test("login non existent user", async () => {
render(<BankHome />);
const { username, signinButton } = fillCredentialsForm();
fetch.once("{}", {status: 404});
fireEvent.click(signinButton);
await screen.findByText("username or account label not found", {exact: false})
})
test("login wrong credentials", async () => {
render(<BankHome />);
const { username, signinButton } = fillCredentialsForm();
fetch.once("{}", {status: 401});
fireEvent.click(signinButton);
await screen.findByText("wrong credentials given", {exact: false})
})
/**
* Test that balance and last transactions get shown
* after a successful login.
*/
test("login success", async () => {
render(<BankHome />);
const { username, signinButton } = fillCredentialsForm();
// Response to balance request.
fetch.once(JSON.stringify({
balance: {
amount: "EUR:10",
credit_debit_indicator: "credit"
},
paytoUri: "payto://iban/123/ABC"
})).once(JSON.stringify({ // Response to history request.
transactions: [{
debtorIban: "XXX",
debtorBic: "YYY",
debtorName: "Foo",
creditorIban: "AAA",
creditorBic: "BBB",
creditorName: "Bar",
direction: "DBIT",
amount: "EUR:5",
subject: "Donation",
date: "01-01-1970"
}, {
debtorIban: "XXX",
debtorBic: "YYY",
debtorName: "Foo",
creditorIban: "AAA",
creditorBic: "BBB",
creditorName: "Bar",
direction: "CRDT",
amount: "EUR:5",
subject: "Refund",
date: "01-01-2000"
}]
}))
fireEvent.click(signinButton);
expect(fetch).toHaveBeenCalledWith(
`http://localhost/demobanks/default/access-api/accounts/${username}`,
expect.anything()
)
await screen.findByText("balance is 10 EUR", {exact: false})
// The two transactions in the history mocked above.
await screen.findByText("refund", {exact: false})
await screen.findByText("donation", {exact: false})
expect(fetch).toHaveBeenCalledWith(
`http://localhost/demobanks/default/access-api/accounts/${username}/transactions?page=0`,
expect.anything()
)
})
test("registration success", async () => {
let context = signUp({}, mockSuccessLoginOrRegistration);
/**
* Tests that a balance is shown after the successful
* registration.
*/
await screen.findByText("balance is 10 EUR", {exact: false})
/**
* The expectation below tests whether the account
* balance was requested after the successful registration.
*/
expect(fetch).toHaveBeenCalledWith(
"http://localhost/demobanks/default/access-api/testing/register",
expect.anything() // no need to match auth headers.
)
expect(fetch).toHaveBeenCalledWith(
`http://localhost/demobanks/default/access-api/accounts/${context.username}`,
expect.anything() // no need to match auth headers.
)
})
})

View File

@ -1,3 +0,0 @@
// Enable enzyme adapter's integration with TypeScript
// See: https://github.com/preactjs/enzyme-adapter-preact-pure#usage-with-typescript
/// <reference types="enzyme-adapter-preact-pure" />

View File

@ -1,19 +1,13 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Basic Options */ /* Basic Options */
"target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, "target": "ES5",
"module": "ESNext" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, "module": "ES6",
// "lib": [], /* Specify library files to be included in the compilation: */ "lib": ["DOM", "ES2016"],
"allowJs": true /* Allow javascript files to be compiled. */, "allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, "jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
"jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */, "jsxImportSource": "preact",
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": true /* Do not emit outputs. */, "noEmit": true /* Do not emit outputs. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
@ -21,11 +15,7 @@
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */, "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* Enable strict null checks. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */ /* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */
@ -33,14 +23,14 @@
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, "moduleResolution": "Node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"esModuleInterop": true /* */, "esModuleInterop": true /* */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */ /* Source Map Options */
@ -56,5 +46,5 @@
/* Advanced Options */ /* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */ "skipLibCheck": true /* Skip type checking of declaration files. */
}, },
"include": ["src/**/*", "tests/**/*"] "include": ["src/**/*"]
} }

File diff suppressed because it is too large Load Diff