@ -0,0 +1,3 @@ |
|||
{ |
|||
"extends": ["@commitlint/config-conventional"] |
|||
} |
@ -0,0 +1,12 @@ |
|||
root = true |
|||
|
|||
[*] |
|||
indent_style = space |
|||
indent_size = 2 |
|||
end_of_line = lf |
|||
charset = utf-8 |
|||
trim_trailing_whitespace = true |
|||
insert_final_newline = true |
|||
|
|||
[*.md] |
|||
trim_trailing_whitespace = false |
@ -0,0 +1,56 @@ |
|||
{ |
|||
"appId": "com.lightningditto.app", |
|||
"productName": "Ditto", |
|||
"copyright": "Copyright © 2019 ${author}", |
|||
"artifactName": "ditto-${os}-${arch}-v${version}.${ext}", |
|||
"files": ["build/**/*", "node_modules/**/*"], |
|||
"directories": { |
|||
"buildResources": "assets" |
|||
}, |
|||
"mac": { |
|||
"category": "public.app-category.utilities", |
|||
"target": ["dmg", "zip"] |
|||
}, |
|||
"dmg": { |
|||
"contents": [ |
|||
{ |
|||
"x": 130, |
|||
"y": 220 |
|||
}, |
|||
{ |
|||
"x": 410, |
|||
"y": 220, |
|||
"type": "link", |
|||
"path": "/Applications" |
|||
} |
|||
] |
|||
}, |
|||
"win": { |
|||
"target": [ |
|||
{ |
|||
"target": "nsis", |
|||
"arch": ["x64", "ia32"] |
|||
} |
|||
] |
|||
}, |
|||
"linux": { |
|||
"target": [ |
|||
{ |
|||
"target": "deb", |
|||
"arch": ["x64", "ia32"] |
|||
}, |
|||
{ |
|||
"target": "AppImage", |
|||
"arch": ["x64", "ia32"] |
|||
} |
|||
], |
|||
"category": "Development" |
|||
}, |
|||
"publish": { |
|||
"provider": "github", |
|||
"owner": "jamaljsr", |
|||
"repo": "ditto", |
|||
"private": false |
|||
}, |
|||
"extends": null |
|||
} |
@ -0,0 +1,9 @@ |
|||
# testing |
|||
/coverage |
|||
|
|||
# production |
|||
/build |
|||
/dist |
|||
|
|||
# compiled by tsc from /src/electron/ |
|||
/public/main.js |
@ -0,0 +1,28 @@ |
|||
{ |
|||
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser |
|||
"extends": [ |
|||
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react |
|||
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from @typescript-eslint/eslint-plugin |
|||
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier |
|||
"plugin:prettier/recommended" // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. |
|||
], |
|||
"parserOptions": { |
|||
"ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features |
|||
"sourceType": "module", // Allows for the use of imports |
|||
"ecmaFeatures": { |
|||
"jsx": true // Allows for the parsing of JSX |
|||
} |
|||
}, |
|||
"settings": { |
|||
"react": { |
|||
"version": "detect" // Tells eslint-plugin-react to automatically detect the version of React to use |
|||
} |
|||
}, |
|||
"rules": { |
|||
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs |
|||
"react/prop-types": "off", |
|||
"@typescript-eslint/explicit-function-return-type": "off", |
|||
"@typescript-eslint/explicit-member-accessibility": "off", |
|||
"@typescript-eslint/no-explicit-any": false |
|||
} |
|||
} |
@ -0,0 +1,4 @@ |
|||
* text eol=lf |
|||
*.png binary |
|||
*.ico binary |
|||
*.icns binary |
@ -0,0 +1,32 @@ |
|||
--- |
|||
name: Bug report |
|||
about: Create a report to help us improve |
|||
title: '' |
|||
labels: '' |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Describe the bug** |
|||
A clear and concise description of what the bug is. |
|||
|
|||
**To Reproduce** |
|||
Steps to reproduce the behavior: |
|||
1. Go to '...' |
|||
2. Click on '....' |
|||
3. Scroll down to '....' |
|||
4. See error |
|||
|
|||
**Expected behavior** |
|||
A clear and concise description of what you expected to happen. |
|||
|
|||
**Screenshots** |
|||
If applicable, add screenshots to help explain your problem. |
|||
|
|||
**Desktop (please complete the following information):** |
|||
- OS: [e.g. OSX Majave or Windows 10 v1903] |
|||
- Docker Version: [e.g. 19.03.1] |
|||
- Docker Compose Version: [e.g. 1.24.1] |
|||
|
|||
**Additional context** |
|||
Add any other context about the problem here. |
@ -0,0 +1,15 @@ |
|||
Closes #(issue number goes here) |
|||
|
|||
### Description |
|||
|
|||
[Description of your changes go here. Please provide both casual and technical explanations.] |
|||
|
|||
### Steps to Test |
|||
|
|||
1. Steps |
|||
2. To |
|||
3. Test |
|||
|
|||
### Screenshots |
|||
|
|||
[Only if applicable] |
@ -0,0 +1,89 @@ |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
|
|||
# Runtime data |
|||
pids |
|||
*.pid |
|||
*.seed |
|||
*.pid.lock |
|||
|
|||
# Directory for instrumented libs generated by jscoverage/JSCover |
|||
lib-cov |
|||
|
|||
# Coverage directory used by tools like istanbul |
|||
coverage |
|||
|
|||
# nyc test coverage |
|||
.nyc_output |
|||
|
|||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) |
|||
.grunt |
|||
|
|||
# Bower dependency directory (https://bower.io/) |
|||
bower_components |
|||
|
|||
# node-waf configuration |
|||
.lock-wscript |
|||
|
|||
# Compiled binary addons (https://nodejs.org/api/addons.html) |
|||
build/Release |
|||
|
|||
# Dependency directories |
|||
node_modules/ |
|||
jspm_packages/ |
|||
|
|||
# TypeScript v1 declaration files |
|||
typings/ |
|||
|
|||
# Optional npm cache directory |
|||
.npm |
|||
|
|||
# Optional eslint cache |
|||
.eslintcache |
|||
|
|||
# Optional REPL history |
|||
.node_repl_history |
|||
|
|||
# Output of 'npm pack' |
|||
*.tgz |
|||
|
|||
# Yarn Integrity file |
|||
.yarn-integrity |
|||
|
|||
# dotenv environment variables file |
|||
.env |
|||
|
|||
# next.js build output |
|||
.next |
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
|||
|
|||
# dependencies |
|||
/node_modules |
|||
/.pnp |
|||
.pnp.js |
|||
|
|||
# testing |
|||
/coverage |
|||
|
|||
# production |
|||
/build |
|||
/dist |
|||
|
|||
# compiled by tsc from /src/electron/ |
|||
/public/main.js |
|||
/public/main.js.map |
|||
|
|||
# misc |
|||
.DS_Store |
|||
.env.local |
|||
.env.development.local |
|||
.env.test.local |
|||
.env.production.local |
|||
|
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
@ -0,0 +1,52 @@ |
|||
const typescript = require('typescript'); |
|||
const fs = require('fs'); |
|||
const path = require('path'); |
|||
|
|||
module.exports = { |
|||
input: [ |
|||
'src/**/*.{ts,tsx}', |
|||
// Use ! to filter out files or directories
|
|||
'!src/**/*.spec.{ts,tsx}', |
|||
], |
|||
output: './', |
|||
options: { |
|||
debug: true, |
|||
func: { |
|||
list: ['i18next.t', 'i18n.t', 't'], |
|||
}, |
|||
trans: { |
|||
defaultsKey: 'defaults', |
|||
}, |
|||
lngs: ['en-US', 'es'], |
|||
defaultLng: 'en-US', |
|||
ns: ['translation'], |
|||
defaultNs: 'translation', |
|||
defaultValue: '__MISSING__TRANSLATION__', |
|||
resource: { |
|||
loadPath: 'src/i18n/locales/{{lng}}.json', |
|||
savePath: 'src/i18n/locales/{{lng}}.json', |
|||
}, |
|||
nsSeparator: ':', |
|||
keySeparator: false, |
|||
}, |
|||
transform: function transform(file, enc, done) { |
|||
const { base, ext } = path.parse(file.path); |
|||
// custom transform for typescript files
|
|||
if (['.ts', '.tsx'].includes(ext) && !base.includes('.d.ts')) { |
|||
const content = fs.readFileSync(file.path, enc); |
|||
|
|||
// convert ts code into es2018 code that the parser can injest
|
|||
const { outputText } = typescript.transpileModule(content, { |
|||
compilerOptions: { |
|||
target: 'es2018', |
|||
}, |
|||
fileName: path.basename(file.path), |
|||
}); |
|||
|
|||
this.parser.parseTransFromString(outputText); |
|||
this.parser.parseFuncFromString(outputText); |
|||
} |
|||
|
|||
done(); |
|||
}, |
|||
}; |
@ -0,0 +1,17 @@ |
|||
{ |
|||
"*.{ts,tsx}": [ |
|||
"eslint --ext .ts,.tsx --fix", |
|||
"prettier --single-quote --write", |
|||
"git add" |
|||
], |
|||
"{.{babelrc,eslintrc,prettierrc,stylelintrc}}": [ |
|||
"prettier --parser json --write", |
|||
"git add" |
|||
], |
|||
"*.less": [ |
|||
"stylelint --syntax scss --fix", |
|||
"prettier --single-quote --write", |
|||
"git add" |
|||
], |
|||
"*.{yml,md}": ["prettier --single-quote --write", "git add"] |
|||
} |
@ -0,0 +1,19 @@ |
|||
{ |
|||
"overrides": [ |
|||
{ |
|||
"files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"], |
|||
"options": { |
|||
"parser": "json" |
|||
} |
|||
} |
|||
], |
|||
"printWidth": 90, |
|||
"singleQuote": true, |
|||
"useTabs": false, |
|||
"semi": true, |
|||
"tabWidth": 2, |
|||
"trailingComma": "all", |
|||
"bracketSpacing": true, |
|||
"jsxBracketSameLine": false, |
|||
"arrowParens": "avoid" |
|||
} |
@ -0,0 +1,8 @@ |
|||
{ |
|||
"extends": ["config:base"], |
|||
"rangeStrategy": "bump", |
|||
"automerge": true, |
|||
"major": { |
|||
"automerge": false |
|||
} |
|||
} |
@ -0,0 +1,78 @@ |
|||
// const darkTheme = require('./src/theme');
|
|||
|
|||
const getLessLoader = (test, withModules) => { |
|||
return { |
|||
test, |
|||
use: [ |
|||
{ loader: 'style-loader' }, |
|||
{ |
|||
loader: 'css-loader', |
|||
options: !withModules |
|||
? undefined |
|||
: { |
|||
sourceMap: true, |
|||
modules: true, |
|||
localIdentName: '[local]___[hash:base64:5]', |
|||
}, |
|||
}, |
|||
{ |
|||
loader: 'less-loader', |
|||
options: { |
|||
javascriptEnabled: true, |
|||
modifyVars: { |
|||
'@primary-color': '#fa8c16', |
|||
'@component-background': '#e8e8e8', |
|||
}, |
|||
// modifyVars: darkTheme,
|
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
}; |
|||
|
|||
module.exports = [ |
|||
config => { |
|||
// set target on webpack config to support electrong
|
|||
config.target = 'electron-renderer'; |
|||
// add support for hot reload of hooks
|
|||
config.resolve.alias['react-dom'] = '@hot-loader/react-dom'; |
|||
return config; |
|||
}, |
|||
[ |
|||
'use-babel-config', |
|||
{ |
|||
presets: ['react-app'], |
|||
plugins: [ |
|||
// add babel-plugin-import for antd
|
|||
[ |
|||
'import', |
|||
{ |
|||
libraryName: 'antd', |
|||
libraryDirectory: 'es', |
|||
style: true, |
|||
}, |
|||
], |
|||
// adds support for live hot reload
|
|||
'react-hot-loader/babel', |
|||
], |
|||
}, |
|||
], |
|||
// add less-loader for antd
|
|||
config => { |
|||
const rule = config.module.rules.find(rule => rule.oneOf); |
|||
|
|||
rule.oneOf.unshift(getLessLoader(/\.less$/, false)); |
|||
rule.oneOf.unshift(getLessLoader(/\.module\.less$/, true)); |
|||
|
|||
return config; |
|||
}, |
|||
config => { |
|||
// helper function to troubleshoot webpack config issues
|
|||
RegExp.prototype.toJSON = RegExp.prototype.toString; |
|||
Function.prototype.toJSON = () => 'function() { }'; // Function.prototype.toString;
|
|||
// uncomment the line below to log the webpack config to the console
|
|||
// console.log(JSON.stringify(config.module.rules, null, 2));
|
|||
// process.exit(1);
|
|||
return config; |
|||
}, |
|||
]; |
@ -0,0 +1,3 @@ |
|||
{ |
|||
"extends": ["stylelint-config-standard", "stylelint-config-prettier"] |
|||
} |
@ -0,0 +1,4 @@ |
|||
{ |
|||
"mainWindowUrl": "./build/index.html", |
|||
"appPath": "." |
|||
} |
@ -0,0 +1,63 @@ |
|||
branches: |
|||
only: |
|||
- master |
|||
- /^v\d+\.\d+(\.\d+)?(-\S*)?$/ |
|||
|
|||
matrix: |
|||
include: |
|||
- os: osx |
|||
language: node_js |
|||
node_js: |
|||
- node |
|||
env: |
|||
- ELECTRON_CACHE=$HOME/.cache/electron |
|||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder |
|||
|
|||
- os: linux |
|||
language: node_js |
|||
node_js: |
|||
- node |
|||
dist: xenial |
|||
services: |
|||
- xvfb |
|||
addons: |
|||
apt: |
|||
sources: |
|||
- ubuntu-toolchain-r-test |
|||
packages: |
|||
- gcc-multilib |
|||
- g++-8 |
|||
- g++-multilib |
|||
- icnsutils |
|||
- graphicsmagick |
|||
- xz-utils |
|||
- xorriso |
|||
- rpm |
|||
|
|||
before_cache: |
|||
- rm -rf $HOME/.cache/electron-builder/wine |
|||
|
|||
cache: |
|||
yarn: true |
|||
directories: |
|||
- node_modules |
|||
- $(npm config get prefix)/lib/node_modules |
|||
- $HOME/.cache/electron |
|||
- $HOME/.cache/electron-builder |
|||
|
|||
before_install: |
|||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX="g++-8"; fi |
|||
|
|||
install: |
|||
- yarn --ignore-engines --frozen-lockfile --non-interactive --network-timeout 300000 |
|||
|
|||
script: |
|||
- yarn lint |
|||
- yarn lint:styles |
|||
- yarn tsc |
|||
# ignore snapshot tests on linux due to file incompatibility across different OS's |
|||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then yarn test -u; fi |
|||
- if [[ "$TRAVIS_OS_NAME" != "linux" ]]; then yarn test; fi |
|||
- yarn test:e2e |
|||
- yarn test:codecov |
|||
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]]; then yarn package:ci; fi |
@ -0,0 +1,14 @@ |
|||
{ |
|||
"types": [ |
|||
{ "type": "feat", "section": "Features" }, |
|||
{ "type": "fix", "section": "Bug Fixes" }, |
|||
{ "type": "docs", "section": "Docs" }, |
|||
{ "type": "perf", "section": "Performance" }, |
|||
{ "type": "test", "hidden": true }, |
|||
{ "type": "build", "hidden": true }, |
|||
{ "type": "ci", "hidden": true }, |
|||
{ "type": "chore", "hidden": true }, |
|||
{ "type": "style", "hidden": true }, |
|||
{ "type": "refactor", "hidden": true } |
|||
] |
|||
} |
@ -0,0 +1,19 @@ |
|||
{ |
|||
"files.associations": { |
|||
".commitlintrc": "jsonc", |
|||
".electronbuildrc": "jsonc", |
|||
".eslintrc": "jsonc", |
|||
".lintstagedrc": "jsonc", |
|||
".prettierrc": "jsonc", |
|||
".renovaterc": "jsonc", |
|||
".stylelintrc": "json", |
|||
".versionrc": "json" |
|||
}, |
|||
"eslint.autoFixOnSave": true, |
|||
"eslint.validate": [ |
|||
"javascript", |
|||
"javascriptreact", |
|||
{ "language": "typescript", "autoFix": true }, |
|||
{ "language": "typescriptreact", "autoFix": true } |
|||
] |
|||
} |
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2019 jamaljsr |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
@ -1,2 +1,58 @@ |
|||
# ditto |
|||
One-click Bitcoin Lightning networks for your local app development & testing |
|||
# Lightning Ditto |
|||
|
|||
> One-click Bitcoin Lightning networks for local app development & testing |
|||
|
|||
[](https://travis-ci.org/jamaljsr/ditto) |
|||
[](https://ci.appveyor.com/project/jamaljsr/ditto) |
|||
[](https://codecov.io/gh/jamaljsr/ditto) |
|||
|
|||
## Development |
|||
|
|||
### Dependencies |
|||
|
|||
Ditto requires that you have Docker installed to create the local networks |
|||
|
|||
- On Mac & Windows, you can just install [Docker Desktop](https://www.docker.com/products/docker-desktop) |
|||
- On Linux, you need to install [Docker Server](https://docs.docker.com/install/#server) and [Docker Compose](https://docs.docker.com/compose/install/) separately |
|||
|
|||
### Run the app |
|||
|
|||
`yarn && yarn dev` |
|||
|
|||
### Run Unit Tests |
|||
|
|||
`yarn test` |
|||
|
|||
### Run End-to-end Tests |
|||
|
|||
`yarn test:e2e` |
|||
|
|||
### Run Typescript & Linter |
|||
|
|||
`yarn lint:all` |
|||
|
|||
### Package App for your OS |
|||
|
|||
`yarn package` |
|||
|
|||
### Tech Stack |
|||
|
|||
- [Electron](https://github.com/electron/electron/): cross platform desktop app framework |
|||
- [Typescript](https://github.com/microsoft/TypeScript): increased productivity with a typed language |
|||
- [ReactJS](https://github.com/facebook/react/): declarative UI library for JavaScript |
|||
- [Create React App](https://github.com/facebook/create-react-app): minimize build configuration |
|||
- [easy-peasy](https://github.com/ctrlplusb/easy-peasy): Redux state management without the boilerplate |
|||
- [Ant Design](https://github.com/ant-design/ant-design/): don't reinvent the wheel with UI design |
|||
- [react-i18next](https://github.com/i18next/react-i18next): support for multiple languages (english/spanish included) |
|||
- [electron-log](https://github.com/megahertz/electron-log): multi-level logging to console and file |
|||
- [Prettier](https://github.com/prettier/prettier): keep code format consistent |
|||
- [ESLint](https://github.com/eslint/eslint): follow code quality best practices |
|||
- [Travis](https://travis-ci.org): automate builds and testing on Mac/Linux |
|||
- [AppVeyor](https://appveyor.com): automate builds and testing on Windows |
|||
- [Renevate Bot](https://github.com/renovatebot/renovate): automate dependency upgrades via GitHub bot |
|||
- [Jest](https://github.com/facebook/jest): delightful JavaScript testing |
|||
- [React Testing Library](https://github.com/testing-library/react-testing-library): React specific testing utilities |
|||
- [CodeCov](https://codecov.io/): maintain quality of unit tests |
|||
- [Testcafe](https://github.com/DevExpress/testcafe): End-to-end is important |
|||
- [commitlint](https://github.com/conventional-changelog/commitlint): standardize git commit messages |
|||
- [standard-version](https://github.com/conventional-changelog/commitlint): automate release versioning and changelog generation |
|||
|
@ -0,0 +1,4 @@ |
|||
# TODO List |
|||
|
|||
- update app icon |
|||
- better UI design |
@ -0,0 +1,46 @@ |
|||
image: Visual Studio 2017 |
|||
|
|||
platform: |
|||
- x64 |
|||
|
|||
environment: |
|||
matrix: |
|||
- nodejs_version: 10 |
|||
|
|||
cache: |
|||
- '%LOCALAPPDATA%/Yarn' |
|||
- node_modules |
|||
- '%USERPROFILE%\.electron' |
|||
|
|||
matrix: |
|||
fast_finish: true |
|||
|
|||
branches: |
|||
only: |
|||
- master |
|||
|
|||
build: off |
|||
|
|||
version: '{build}' |
|||
|
|||
shallow_clone: true |
|||
|
|||
clone_depth: 1 |
|||
|
|||
skip_branch_with_pr: true |
|||
|
|||
install: |
|||
- ps: Install-Product node $env:nodejs_version x64 |
|||
- set CI=true |
|||
- yarn |
|||
|
|||
test_script: |
|||
- yarn lint |
|||
- yarn lint:styles |
|||
- yarn tsc |
|||
- yarn test |
|||
- yarn test:e2e |
|||
- yarn test:codecov |
|||
|
|||
deploy_script: |
|||
- cmd: powershell if ($env:appveyor_repo_branch -eq 'master') { yarn package:ci } |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 389 B |
After Width: | Height: | Size: 636 B |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 917 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 4.4 KiB |
@ -0,0 +1,7 @@ |
|||
import { getPageTitle, assertNoConsoleErrors, pageUrl } from './helpers'; |
|||
|
|||
fixture`App`.page(pageUrl).afterEach(assertNoConsoleErrors); |
|||
|
|||
test('should have correct title', async t => { |
|||
await t.expect(getPageTitle()).eql('React App'); |
|||
}); |
@ -0,0 +1,64 @@ |
|||
import { App, Counter } from './pages'; |
|||
import { assertNoConsoleErrors, pageUrl, getPageUrl } from './helpers'; |
|||
|
|||
fixture`Counter` |
|||
.page(pageUrl) |
|||
.beforeEach(App.clickCounterLink) |
|||
.afterEach(assertNoConsoleErrors); |
|||
|
|||
test('should be on the route /counter', async t => { |
|||
await t.expect(getPageUrl()).match(/.*#\/counter$/); |
|||
}); |
|||
|
|||
test('should display updated count after increment button click', async t => { |
|||
await t |
|||
.click(Counter.increment) |
|||
.expect(Counter.getCounterText()) |
|||
.eql('1'); |
|||
}); |
|||
|
|||
test('should display updated count after decrement button click', async t => { |
|||
await t |
|||
.click(Counter.decrement) |
|||
.expect(Counter.getCounterText()) |
|||
.eql('-1'); |
|||
}); |
|||
|
|||
test('should not change when count is even and odd button clicked', async t => { |
|||
await t |
|||
.click(Counter.incrementOdd) |
|||
.expect(Counter.getCounterText()) |
|||
.eql('0'); |
|||
}); |
|||
|
|||
test('should change when count is odd and odd button clicked', async t => { |
|||
await t |
|||
.click(Counter.increment) |
|||
.click(Counter.incrementOdd) |
|||
.expect(Counter.getCounterText()) |
|||
.eql('3'); |
|||
}); |
|||
|
|||
test('should change a second later if async button clicked', async t => { |
|||
await t |
|||
.click(Counter.incrementAsync) |
|||
.expect(Counter.loadingIcon) |
|||
.ok() |
|||
.expect(Counter.getCounterText()) |
|||
.eql('1'); |
|||
}); |
|||
|
|||
test('should show error if count is 3 and async button clicked', async t => { |
|||
await t |
|||
.click(Counter.increment) // increment to 1
|
|||
.click(Counter.incrementOdd) // increment to 3
|
|||
.click(Counter.incrementAsync) |
|||
.expect(Counter.loadingIcon.exists) |
|||
.ok() |
|||
.expect(Counter.getCounterText()) |
|||
.eql('3') |
|||
.expect(Counter.error.exists) |
|||
.ok() |
|||
.expect(Counter.getErrorText()) |
|||
.eql('Increment Async prohibited when count is 3.'); |
|||
}); |
@ -0,0 +1,22 @@ |
|||
import { Home } from './pages'; |
|||
import { getPageUrl, assertNoConsoleErrors, pageUrl } from './helpers'; |
|||
|
|||
fixture`Home`.page(pageUrl).afterEach(assertNoConsoleErrors); |
|||
|
|||
test('should be on the route /', async t => { |
|||
await t.expect(getPageUrl()).match(/.*#\/$/); |
|||
}); |
|||
|
|||
test('should show success alert when "Click Me" button clicked', async t => { |
|||
await t |
|||
.click(Home.clickMeButton) |
|||
.expect(Home.successAlert.exists) |
|||
.ok(); |
|||
}); |
|||
|
|||
test('should navgiate to /counter', async t => { |
|||
await t |
|||
.click(Home.counterLink) |
|||
.expect(getPageUrl()) |
|||
.contains('/counter'); |
|||
}); |
@ -0,0 +1,11 @@ |
|||
import { ClientFunction } from 'testcafe'; |
|||
|
|||
export const pageUrl = '../build/index.html'; |
|||
|
|||
export const getPageUrl = ClientFunction(() => window.location.href); |
|||
export const getPageTitle = ClientFunction(() => document.title); |
|||
|
|||
export const assertNoConsoleErrors = async (t: TestController) => { |
|||
const { error } = await t.getBrowserConsoleMessages(); |
|||
await t.expect(error).eql([]); |
|||
}; |
@ -0,0 +1,11 @@ |
|||
import { Selector } from 'testcafe'; |
|||
|
|||
class App { |
|||
navHomeLink: Selector = Selector('[data-tid=nav-home]'); |
|||
navCounterLink: Selector = Selector('[data-tid=nav-counter]'); |
|||
|
|||
clickHomeLink = async (t: TestController) => t.click(this.navHomeLink); |
|||
clickCounterLink = async (t: TestController) => t.click(this.navCounterLink); |
|||
} |
|||
|
|||
export default new App(); |
@ -0,0 +1,16 @@ |
|||
import { Selector } from 'testcafe'; |
|||
|
|||
class Counter { |
|||
counter: Selector = Selector('[data-tid=counter]'); |
|||
increment: Selector = Selector('[data-tid=incr-btn]'); |
|||
decrement: Selector = Selector('[data-tid=decr-btn]'); |
|||
incrementOdd: Selector = Selector('[data-tid=odd-btn]'); |
|||
incrementAsync: Selector = Selector('[data-tid=async-btn]'); |
|||
loadingIcon: Selector = Selector('[data-tid=async-loader]'); |
|||
error: Selector = Selector('[data-tid=error]'); |
|||
|
|||
getCounterText = () => this.counter.innerText; |
|||
getErrorText = () => this.error.innerText; |
|||
} |
|||
|
|||
export default new Counter(); |
@ -0,0 +1,11 @@ |
|||
import { Selector } from 'testcafe'; |
|||
|
|||
class Home { |
|||
successAlert: Selector = Selector('[data-tid=success]'); |
|||
counterLink: Selector = Selector('[data-tid=counter-link]'); |
|||
clickMeButton: Selector = Selector('[data-tid=me-btn]'); |
|||
|
|||
clickCounterLink = async (t: TestController) => t.click(this.counterLink); |
|||
} |
|||
|
|||
export default new Home(); |
@ -0,0 +1,3 @@ |
|||
export { default as App } from './App'; |
|||
export { default as Home } from './Home'; |
|||
export { default as Counter } from './Counter'; |
@ -0,0 +1,62 @@ |
|||
import path from 'path'; |
|||
import { app, BrowserWindow } from 'electron'; |
|||
import debug from 'electron-debug'; |
|||
import isNotPackaged from 'electron-is-dev'; |
|||
import { warn } from 'electron-log'; |
|||
|
|||
const isDev = isNotPackaged && process.env.NODE_ENV !== 'production'; |
|||
warn(`Starting Electron main process`); |
|||
|
|||
let mainWindow: BrowserWindow | null; |
|||
|
|||
// use dev server for hot reload or file in production
|
|||
const url = isDev |
|||
? 'http://localhost:3000' |
|||
: `file://${path.join(__dirname, '../build/index.html')}`; |
|||
|
|||
// install react & redux chrome dev tools
|
|||
const installExtensions = async () => { |
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|||
const installer = require('electron-devtools-installer'); |
|||
const forceDownload = !!process.env.UPGRADE_EXTENSIONS; |
|||
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; |
|||
|
|||
return Promise.all( |
|||
extensions.map(name => installer.default(installer[name], forceDownload)), |
|||
).catch(console.log); |
|||
}; |
|||
|
|||
const createWindow = async () => { |
|||
mainWindow = new BrowserWindow({ |
|||
width: isDev ? 1536 : 900, |
|||
height: 680, |
|||
webPreferences: { |
|||
nodeIntegration: true, |
|||
}, |
|||
}); |
|||
|
|||
mainWindow.loadURL(url); |
|||
|
|||
if (isDev) { |
|||
debug(); |
|||
await installExtensions(); |
|||
mainWindow.webContents.openDevTools(); |
|||
mainWindow.maximize(); |
|||
} |
|||
|
|||
mainWindow.on('closed', () => (mainWindow = null)); |
|||
}; |
|||
|
|||
app.on('ready', createWindow); |
|||
|
|||
app.on('window-all-closed', () => { |
|||
if (process.platform !== 'darwin') { |
|||
app.quit(); |
|||
} |
|||
}); |
|||
|
|||
app.on('activate', () => { |
|||
if (mainWindow === null) { |
|||
createWindow(); |
|||
} |
|||
}); |
@ -0,0 +1,16 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "es2019", |
|||
"lib": ["esnext"], |
|||
"skipLibCheck": true, |
|||
"esModuleInterop": true, |
|||
"allowSyntheticDefaultImports": true, |
|||
"strict": true, |
|||
"module": "commonjs", |
|||
"moduleResolution": "node", |
|||
"outDir": "../public/", |
|||
"pretty": true, |
|||
"sourceMap": true |
|||
}, |
|||
"include": ["."] |
|||
} |
@ -0,0 +1,144 @@ |
|||
{ |
|||
"name": "lightning-ditto", |
|||
"version": "0.1.0", |
|||
"homepage": "https://lightningditto.com", |
|||
"description": "One-click Bitcoin Lightning networks for local app development & testing", |
|||
"author": { |
|||
"name": "jamaljsr", |
|||
"email": "contact@lightningditto.com", |
|||
"url": "https://lightningditto.com" |
|||
}, |
|||
"main": "public/main.js", |
|||
"scripts": { |
|||
"build": "cross-env PUBLIC_URL=./ rescripts build", |
|||
"cm": "git add . && git cz", |
|||
"dev": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"", |
|||
"eject": "rescripts eject", |
|||
"extract-langs": "i18next-scanner --config ./.i18next-scanner.js", |
|||
"lint": "eslint --ext .ts,.tsx --ignore-path .gitignore .", |
|||
"lint:all": "yarn tsc && yarn lint && yarn lint:styles", |
|||
"lint:fix": "yarn lint --fix", |
|||
"lint:styles": "stylelint **/*.*less", |
|||
"lint:styles:fix": "stylelint **/*.*less --fix", |
|||
"package": "yarn build && yarn package:electron", |
|||
"package:ci": "yarn postinstall && yarn build && yarn package:electron --publish onTagOrDraft", |
|||
"package:electron": "electron-builder build -c.extraMetadata.main=build/main.js --config .electronbuildrc", |
|||
"postinstall": "electron-builder install-app-deps", |
|||
"prestart": "tsc -p electron/tsconfig.json", |
|||
"prebuild": "tsc -p electron/tsconfig.json", |
|||
"release": "standard-version --no-verify", |
|||
"start": "rescripts start", |
|||
"test": "rescripts test --coverage", |
|||
"test:all": "cross-env CI=true yarn test && yarn test:e2e", |
|||
"test:codecov": "codecov", |
|||
"test:e2e": "yarn build && cross-env NODE_ENV=production testcafe electron:./ ./e2e/**/*.e2e.ts", |
|||
"test:e2e:live": "yarn test:e2e -L", |
|||
"tsc": "tsc --noEmit" |
|||
}, |
|||
"dependencies": { |
|||
"electron-debug": "^3.0.1", |
|||
"electron-is-dev": "^1.1.0", |
|||
"electron-log": "^3.0.7" |
|||
}, |
|||
"devDependencies": { |
|||
"@commitlint/cli": "^8.1.0", |
|||
"@commitlint/config-conventional": "^8.1.0", |
|||
"@hot-loader/react-dom": "^16.8.6", |
|||
"@rescripts/cli": "^0.0.11", |
|||
"@rescripts/rescript-use-babel-config": "^0.0.8", |
|||
"@testing-library/jest-dom": "^4.0.0", |
|||
"@testing-library/react": "^8.0.9", |
|||
"@types/jest": "24.0.17", |
|||
"@types/node": "12.7.1", |
|||
"@types/react": "16.9.0", |
|||
"@types/react-dom": "16.8.5", |
|||
"@types/react-redux": "^7.1.1", |
|||
"@types/react-router": "^5.0.3", |
|||
"@types/react-router-dom": "^4.3.4", |
|||
"@types/redux-logger": "^3.0.7", |
|||
"@typescript-eslint/eslint-plugin": "^1.13.0", |
|||
"@typescript-eslint/parser": "^1.13.0", |
|||
"antd": "^3.21.2", |
|||
"babel-plugin-import": "^1.12.0", |
|||
"codecov": "^3.5.0", |
|||
"commitizen": "^4.0.3", |
|||
"concurrently": "^4.1.1", |
|||
"connected-react-router": "^6.5.2", |
|||
"cross-env": "^5.2.0", |
|||
"easy-peasy": "3.0.1", |
|||
"electron": "^5.0.9", |
|||
"electron-builder": "^21.2.0", |
|||
"electron-devtools-installer": "^2.2.4", |
|||
"eslint": "5.16.0", |
|||
"eslint-config-prettier": "^6.0.0", |
|||
"eslint-plugin-prettier": "^3.1.0", |
|||
"eslint-plugin-react": "^7.14.3", |
|||
"history": "^4.9.0", |
|||
"husky": "^3.0.3", |
|||
"i18next": "^17.0.9", |
|||
"i18next-browser-languagedetector": "^3.0.3", |
|||
"i18next-scanner": "^2.10.2", |
|||
"less": "^3.9.0", |
|||
"less-loader": "^5.0.0", |
|||
"lint-staged": "^9.2.1", |
|||
"prettier": "^1.18.2", |
|||
"react": "^16.9.0", |
|||
"react-async-hook": "^3.4.0", |
|||
"react-dom": "^16.9.0", |
|||
"react-hot-loader": "^4.12.10", |
|||
"react-i18next": "^10.11.5", |
|||
"react-redux": "^7.1.0", |
|||
"react-router": "^5.0.1", |
|||
"react-router-dom": "^5.0.1", |
|||
"react-scripts": "3.0.1", |
|||
"redux": "^4.0.4", |
|||
"redux-logger": "^3.0.6", |
|||
"standard-version": "^7.0.0", |
|||
"stylelint": "^10.1.0", |
|||
"stylelint-config-prettier": "^5.2.0", |
|||
"stylelint-config-standard": "^18.3.0", |
|||
"testcafe": "^1.4.0", |
|||
"testcafe-browser-provider-electron": "^0.0.11", |
|||
"testcafe-react-selectors": "^3.1.0", |
|||
"typescript": "3.5.3", |
|||
"wait-on": "^3.3.0", |
|||
"webpack": "4.29.6" |
|||
}, |
|||
"husky": { |
|||
"hooks": { |
|||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS", |
|||
"pre-commit": "lint-staged" |
|||
} |
|||
}, |
|||
"jest": { |
|||
"collectCoverageFrom": [ |
|||
"src/**/*.{js,jsx,ts,tsx}", |
|||
"!<rootDir>/node_modules/", |
|||
"!<rootDir>/src/i18n/index.ts", |
|||
"!<rootDir>/src/index.tsx", |
|||
"!<rootDir>/src/react-app-env.d.ts", |
|||
"!<rootDir>/src/store/index.ts", |
|||
"!<rootDir>/src/theme/index.js" |
|||
] |
|||
}, |
|||
"eslintConfig": { |
|||
"extends": "react-app" |
|||
}, |
|||
"browserslist": { |
|||
"production": [ |
|||
">0.2%", |
|||
"not dead", |
|||
"not op_mini all" |
|||
], |
|||
"development": [ |
|||
"last 1 chrome version", |
|||
"last 1 firefox version", |
|||
"last 1 safari version" |
|||
] |
|||
}, |
|||
"config": { |
|||
"commitizen": { |
|||
"path": "./node_modules/cz-conventional-changelog" |
|||
} |
|||
} |
|||
} |
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,38 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|||
<meta name="theme-color" content="#000000" /> |
|||
<!-- |
|||
manifest.json provides metadata used when your web app is installed on a |
|||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ |
|||
--> |
|||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
|||
<!-- |
|||
Notice the use of %PUBLIC_URL% in the tags above. |
|||
It will be replaced with the URL of the `public` folder during the build. |
|||
Only files inside the `public` folder can be referenced from the HTML. |
|||
|
|||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
|||
work correctly both with client-side routing and a non-root public URL. |
|||
Learn how to configure a non-root public URL by running `npm run build`. |
|||
--> |
|||
<title>React App</title> |
|||
</head> |
|||
<body> |
|||
<noscript>You need to enable JavaScript to run this app.</noscript> |
|||
<div id="root"></div> |
|||
<!-- |
|||
This HTML file is a template. |
|||
If you open it directly in the browser, you will see an empty page. |
|||
|
|||
You can add webfonts, meta tags, or analytics to this file. |
|||
The build step will place the bundled scripts into the <body> tag. |
|||
|
|||
To begin the development, run `npm start` or `yarn start`. |
|||
To create a production bundle, use `npm run build` or `yarn build`. |
|||
--> |
|||
</body> |
|||
</html> |
@ -0,0 +1,15 @@ |
|||
{ |
|||
"short_name": "React App", |
|||
"name": "Create React App Sample", |
|||
"icons": [ |
|||
{ |
|||
"src": "favicon.ico", |
|||
"sizes": "64x64 32x32 24x24 16x16", |
|||
"type": "image/x-icon" |
|||
} |
|||
], |
|||
"start_url": ".", |
|||
"display": "standalone", |
|||
"theme_color": "#000000", |
|||
"background_color": "#ffffff" |
|||
} |
@ -0,0 +1,9 @@ |
|||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
|||
module.exports = { |
|||
error: (...params) => {}, |
|||
warn: (...params) => {}, |
|||
info: (...params) => {}, |
|||
verbose: (...params) => {}, |
|||
debug: (...params) => {}, |
|||
silly: (...params) => {}, |
|||
}; |
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
t: k => k, |
|||
}; |
@ -0,0 +1,62 @@ |
|||
/* eslint-disable @typescript-eslint/no-var-requires */ |
|||
const React = require('react'); |
|||
const reactI18next = require('react-i18next'); |
|||
|
|||
const hasChildren = node => |
|||
node && (node.children || (node.props && node.props.children)); |
|||
|
|||
const getChildren = node => |
|||
node && node.children ? node.children : node.props && node.props.children; |
|||
|
|||
const renderNodes = reactNodes => { |
|||
if (typeof reactNodes === 'string') { |
|||
return reactNodes; |
|||
} |
|||
|
|||
return Object.keys(reactNodes).map((key, i) => { |
|||
const child = reactNodes[key]; |
|||
const isElement = React.isValidElement(child); |
|||
|
|||
if (typeof child === 'string') { |
|||
return child; |
|||
} |
|||
if (hasChildren(child)) { |
|||
const inner = renderNodes(getChildren(child)); |
|||
return React.cloneElement(child, { ...child.props, key: i }, inner); |
|||
} |
|||
if (typeof child === 'object' && !isElement) { |
|||
return Object.keys(child).reduce((str, childKey) => `${str}${child[childKey]}`, ''); |
|||
} |
|||
|
|||
return child; |
|||
}); |
|||
}; |
|||
|
|||
// wrap changeLanguage() in a jest mock to track invocations
|
|||
const i18n = { |
|||
changeLanguage: jest.fn(l => l), |
|||
}; |
|||
// t() should just return the language key
|
|||
const t = k => k; |
|||
|
|||
// mock for the useTranslation hook
|
|||
const useMock = [t, i18n]; |
|||
useMock.t = t; |
|||
useMock.i18n = i18n; |
|||
|
|||
module.exports = { |
|||
// this mock makes sure any components using the translate HoC receive the t function as a prop
|
|||
// eslint-disable-next-line react/display-name
|
|||
withTranslation: () => Component => props => <Component t={k => k} {...props} />, |
|||
Trans: ({ children }) => renderNodes(children), |
|||
Translation: ({ children }) => children(k => k, { i18n: {} }), |
|||
useTranslation: () => useMock, |
|||
|
|||
// mock if needed
|
|||
I18nextProvider: reactI18next.I18nextProvider, |
|||
initReactI18next: reactI18next.initReactI18next, |
|||
setDefaults: reactI18next.setDefaults, |
|||
getDefaults: reactI18next.getDefaults, |
|||
setI18n: reactI18next.setI18n, |
|||
getI18n: reactI18next.getI18n, |
|||
}; |
@ -0,0 +1,9 @@ |
|||
import React from 'react'; |
|||
import ReactDOM from 'react-dom'; |
|||
import App from './App'; |
|||
|
|||
it('renders without crashing', () => { |
|||
const div = document.createElement('div'); |
|||
ReactDOM.render(<App />, div); |
|||
ReactDOM.unmountComponentAtNode(div); |
|||
}); |
@ -0,0 +1,26 @@ |
|||
import React, { useEffect } from 'react'; |
|||
import { hot } from 'react-hot-loader/root'; |
|||
import { Provider } from 'react-redux'; |
|||
import { ConnectedRouter } from 'connected-react-router'; |
|||
import { StoreProvider } from 'easy-peasy'; |
|||
import store, { history } from 'store'; |
|||
import Routes from './Routes'; |
|||
import { warn } from 'electron-log'; |
|||
|
|||
const App: React.FC = () => { |
|||
useEffect(() => warn('Rendering App component'), []); |
|||
return ( |
|||
// store provider for easy-peasy hooks
|
|||
<StoreProvider store={store}> |
|||
{/* react-redux provider for router state */} |
|||
<Provider store={store as any}> |
|||
{/* connected-react-router */} |
|||
<ConnectedRouter history={history}> |
|||
<Routes /> |
|||
</ConnectedRouter> |
|||
</Provider> |
|||
</StoreProvider> |
|||
); |
|||
}; |
|||
|
|||
export default hot(App); |
@ -0,0 +1,36 @@ |
|||
import React from 'react'; |
|||
import { StoreProvider } from 'easy-peasy'; |
|||
import { Provider } from 'react-redux'; |
|||
import { MemoryRouter } from 'react-router'; |
|||
import { render } from '@testing-library/react'; |
|||
import '@testing-library/jest-dom/extend-expect'; |
|||
import '@testing-library/react/cleanup-after-each'; |
|||
import { createReduxStore } from 'store'; |
|||
import Routes, { HOME, COUNTER } from './Routes'; |
|||
|
|||
describe('App container', () => { |
|||
const renderComponent = (route: string) => { |
|||
const store = createReduxStore(); |
|||
const routes = ( |
|||
<StoreProvider store={store}> |
|||
<Provider store={store as any}> |
|||
<MemoryRouter initialEntries={[route]}> |
|||
<Routes /> |
|||
</MemoryRouter> |
|||
</Provider> |
|||
</StoreProvider> |
|||
); |
|||
|
|||
return render(routes); |
|||
}; |
|||
|
|||
it('should render the home page', () => { |
|||
const { getByTestId } = renderComponent(HOME); |
|||
expect(getByTestId('me-btn')).toHaveTextContent('home.me-btn'); |
|||
}); |
|||
|
|||
it('should render the counter page', () => { |
|||
const { getByTestId } = renderComponent(COUNTER); |
|||
expect(getByTestId('counter')).toHaveTextContent('0'); |
|||
}); |
|||
}); |
@ -0,0 +1,19 @@ |
|||
import React from 'react'; |
|||
import { Switch, Route } from 'react-router'; |
|||
import { AppLayout } from './layouts'; |
|||
import { Home } from './home'; |
|||
import { Counter } from './counter'; |
|||
|
|||
export const HOME = '/'; |
|||
export const COUNTER = '/counter'; |
|||
|
|||
const Routes = () => ( |
|||
<AppLayout> |
|||
<Switch> |
|||
<Route path={COUNTER} component={Counter} /> |
|||
<Route path={HOME} component={Home} /> |
|||
</Switch> |
|||
</AppLayout> |
|||
); |
|||
|
|||
export default Routes; |
@ -0,0 +1,12 @@ |
|||
.body { |
|||
padding: 20px; |
|||
text-align: center; |
|||
|
|||
.counter { |
|||
font-size: 5rem; |
|||
} |
|||
|
|||
.btnGroup .btn { |
|||
margin: 5px; |
|||
} |
|||
} |
@ -0,0 +1,112 @@ |
|||
import React from 'react'; |
|||
import { |
|||
render, |
|||
fireEvent, |
|||
waitForElement, |
|||
waitForElementToBeRemoved, |
|||
} from '@testing-library/react'; |
|||
import { StoreProvider } from 'easy-peasy'; |
|||
import { ConnectedRouter } from 'connected-react-router'; |
|||
import { Provider } from 'react-redux'; |
|||
import { createMemoryHistory } from 'history'; |
|||
|
|||
import { createReduxStore } from 'store'; |
|||
import Counter from './Counter'; |
|||
|
|||
describe('Counter component', () => { |
|||
const renderComponent = () => { |
|||
const store = createReduxStore(); |
|||
const app = ( |
|||
<StoreProvider store={store}> |
|||
<Provider store={store as any}> |
|||
<ConnectedRouter history={createMemoryHistory()}> |
|||
<Counter /> |
|||
</ConnectedRouter> |
|||
</Provider> |
|||
</StoreProvider> |
|||
); |
|||
|
|||
return render(app); |
|||
}; |
|||
|
|||
it('should contain default counter value of zero', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
expect(getByTestId('counter')).toHaveTextContent('0'); |
|||
}); |
|||
|
|||
it('should increment by one when plus btn is clicked', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
const btn = getByTestId('incr-btn'); |
|||
fireEvent.click(btn); |
|||
expect(getByTestId('counter')).toHaveTextContent('1'); |
|||
}); |
|||
|
|||
it('should decrement by one when minus btn is clicked', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
const btn = getByTestId('decr-btn'); |
|||
fireEvent.click(btn); |
|||
expect(getByTestId('counter')).toHaveTextContent('-1'); |
|||
}); |
|||
|
|||
it('should not increment when odd btn is clicked and count is even', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
const btn = getByTestId('odd-btn'); |
|||
fireEvent.click(btn); |
|||
expect(getByTestId('counter')).toHaveTextContent('0'); |
|||
}); |
|||
|
|||
it('should increment by two when odd btn is clicked and count is odd', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
|
|||
// first increment to 1 so that the current count is odd
|
|||
const incr = getByTestId('incr-btn'); |
|||
fireEvent.click(incr); |
|||
|
|||
const btn = getByTestId('odd-btn'); |
|||
fireEvent.click(btn); |
|||
expect(getByTestId('counter')).toHaveTextContent('3'); |
|||
}); |
|||
|
|||
it('should show loader when async btn is clicked', async () => { |
|||
const { getByTestId } = renderComponent(); |
|||
const btn = getByTestId('async-btn'); |
|||
fireEvent.click(btn); |
|||
const loader = await waitForElement(() => getByTestId('async-loader')); |
|||
expect(loader).not.toBeNull(); |
|||
}); |
|||
|
|||
it('should increment after some time when async btn is clicked', async () => { |
|||
const { getByTestId } = renderComponent(); |
|||
const btn = getByTestId('async-btn'); |
|||
fireEvent.click(btn); |
|||
|
|||
// wait for loader to show and then be removed
|
|||
await waitForElement(() => getByTestId('async-loader')); |
|||
await waitForElementToBeRemoved(() => getByTestId('async-loader')); |
|||
|
|||
expect(btn).toHaveTextContent('cmps.counter.increment-async'); |
|||
expect(getByTestId('counter')).toHaveTextContent('1'); |
|||
}); |
|||
|
|||
it('should raise error when count is 3 and async btn is clicked', async () => { |
|||
const { getByTestId } = renderComponent(); |
|||
|
|||
// first increment to 3
|
|||
const incr = getByTestId('incr-btn'); |
|||
fireEvent.click(incr); |
|||
fireEvent.click(incr); |
|||
fireEvent.click(incr); |
|||
|
|||
const btn = getByTestId('async-btn'); |
|||
fireEvent.click(btn); |
|||
|
|||
// wait for loader to show and then be removed
|
|||
await waitForElement(() => getByTestId('async-loader')); |
|||
await waitForElementToBeRemoved(() => getByTestId('async-loader')); |
|||
|
|||
expect(btn).toHaveTextContent('cmps.counter.increment-async'); |
|||
expect(getByTestId('error')).toHaveTextContent( |
|||
'models.counter.increment-async.error', |
|||
); |
|||
}); |
|||
}); |
@ -0,0 +1,74 @@ |
|||
import React, { useEffect } from 'react'; |
|||
import { Alert, Button, Icon } from 'antd'; |
|||
import { useStoreState, useStoreActions } from 'store'; |
|||
import { useAsyncCallback } from 'react-async-hook'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import styles from './Counter.module.less'; |
|||
import { info } from 'electron-log'; |
|||
|
|||
const Counter = () => { |
|||
useEffect(() => info('Rendering Counter component'), []); |
|||
const { t } = useTranslation(); |
|||
const { count } = useStoreState(s => s.counter); |
|||
const { increment, decrement, incrementIfOdd, incrementAsync } = useStoreActions( |
|||
a => a.counter, |
|||
); |
|||
const { execute: incrementAsyncCb, loading, error } = useAsyncCallback(() => |
|||
incrementAsync(), |
|||
); |
|||
const [incrementCb, decrementCb, incrementIfOddCb] = [ |
|||
increment, |
|||
decrement, |
|||
incrementIfOdd, |
|||
].map(x => () => x()); |
|||
|
|||
return ( |
|||
<div className={styles.body}> |
|||
{error && <Alert message={error.message} type="error" data-tid="error" />} |
|||
<h1 className={styles.counter} data-tid="counter"> |
|||
{loading ? <Icon type="loading" data-tid="async-loader" /> : count} |
|||
</h1> |
|||
<div className={styles.btnGroup}> |
|||
<Button |
|||
type="primary" |
|||
icon="plus" |
|||
className={styles.btn} |
|||
data-tid="incr-btn" |
|||
onClick={incrementCb} |
|||
> |
|||
{t('cmps.counter.increment', 'Increment')} |
|||
</Button> |
|||
<Button |
|||
type="primary" |
|||
icon="minus" |
|||
className={styles.btn} |
|||
data-tid="decr-btn" |
|||
onClick={decrementCb} |
|||
> |
|||
{t('cmps.counter.decrement', 'Decrement')} |
|||
</Button> |
|||
<Button |
|||
type="primary" |
|||
icon="question" |
|||
className={styles.btn} |
|||
data-tid="odd-btn" |
|||
onClick={incrementIfOddCb} |
|||
> |
|||
{t('cmps.counter.increment-odd', 'Increment Odd')} |
|||
</Button> |
|||
<Button |
|||
type="primary" |
|||
icon="retweet" |
|||
className={styles.btn} |
|||
data-tid="async-btn" |
|||
onClick={incrementAsyncCb} |
|||
loading={loading} |
|||
> |
|||
{t('cmps.counter.increment-async', 'Increment Async')} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default Counter; |
@ -0,0 +1 @@ |
|||
export { default as Counter } from './Counter'; |
@ -0,0 +1,47 @@ |
|||
import React from 'react'; |
|||
import { render, fireEvent } from '@testing-library/react'; |
|||
import { StoreProvider } from 'easy-peasy'; |
|||
import { ConnectedRouter } from 'connected-react-router'; |
|||
import { Provider } from 'react-redux'; |
|||
import { createMemoryHistory } from 'history'; |
|||
import { createReduxStore } from 'store'; |
|||
import Home from './Home'; |
|||
|
|||
describe('Home component', () => { |
|||
const renderComponent = () => { |
|||
const store = createReduxStore(); |
|||
const app = ( |
|||
<StoreProvider store={store}> |
|||
<Provider store={store as any}> |
|||
<ConnectedRouter history={createMemoryHistory()}> |
|||
<Home /> |
|||
</ConnectedRouter> |
|||
</Provider> |
|||
</StoreProvider> |
|||
); |
|||
|
|||
return render(app); |
|||
}; |
|||
|
|||
it('should contain a "Click Me!" button', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
expect(getByTestId('me-btn')).toHaveTextContent('cmps.home.me-btn'); |
|||
}); |
|||
|
|||
it('should contain a link to Counter page', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
expect(getByTestId('counter-link')).toHaveTextContent('Counter'); |
|||
}); |
|||
|
|||
it('should not show alert message', () => { |
|||
const { queryByTestId } = renderComponent(); |
|||
expect(queryByTestId('success')).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should show alert after button is clicked', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
const btn = getByTestId('me-btn'); |
|||
fireEvent.click(btn); |
|||
expect(getByTestId('success')).toHaveTextContent('cmps.home.success-text'); |
|||
}); |
|||
}); |
@ -0,0 +1,46 @@ |
|||
import React, { useState, useEffect } from 'react'; |
|||
import { Card, Button, Alert } from 'antd'; |
|||
import { Link } from 'react-router-dom'; |
|||
import { useTranslation, Trans } from 'react-i18next'; |
|||
import { COUNTER } from 'components/Routes'; |
|||
import { info } from 'electron-log'; |
|||
|
|||
const Home = () => { |
|||
useEffect(() => info('Rendering Home component'), []); |
|||
const { t } = useTranslation(); |
|||
|
|||
const [showAlert, setShowAlert] = useState(false); |
|||
const handleClickMe = () => setShowAlert(true); |
|||
|
|||
return ( |
|||
<div> |
|||
{showAlert && ( |
|||
<Alert |
|||
message={t('cmps.home.success-text')} |
|||
type="success" |
|||
showIcon |
|||
data-tid="success" |
|||
/> |
|||
)} |
|||
<Card title={t('cmps.home.card-title')}> |
|||
<p>{t('cmps.home.card-description')}</p> |
|||
<p> |
|||
<Trans i18nKey="cmps.home.play"> |
|||
Play with the{' '} |
|||
<Link to={COUNTER} data-tid="counter-link"> |
|||
Counter |
|||
</Link>{' '} |
|||
thing |
|||
</Trans> |
|||
</p> |
|||
<p> |
|||
<Button type="primary" data-tid="me-btn" onClick={handleClickMe}> |
|||
{t('cmps.home.me-btn')} |
|||
</Button> |
|||
</p> |
|||
</Card> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default Home; |
@ -0,0 +1 @@ |
|||
export { default as Home } from './Home'; |
@ -0,0 +1,67 @@ |
|||
.logo { |
|||
position: relative; |
|||
height: 64px; |
|||
padding-left: 24px; |
|||
overflow: hidden; |
|||
line-height: 64px; |
|||
background: #002140; |
|||
transition: all 0.3s; |
|||
|
|||
img { |
|||
height: 32px; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
span { |
|||
display: inline-block; |
|||
color: #fff; |
|||
font-weight: 600; |
|||
font-size: 18px; |
|||
vertical-align: middle; |
|||
} |
|||
} |
|||
|
|||
.layout { |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
.header { |
|||
padding: 0; |
|||
|
|||
.trigger { |
|||
color: rgba(255, 255, 255, 0.65); |
|||
font-size: 18px; |
|||
line-height: 64px; |
|||
padding: 0 24px; |
|||
cursor: pointer; |
|||
transition: color 0.3s; |
|||
} |
|||
|
|||
.trigger:hover { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
|
|||
.content { |
|||
margin: 0 16px; |
|||
} |
|||
|
|||
.breadcrumb { |
|||
margin: 16px 0; |
|||
} |
|||
|
|||
.container { |
|||
padding: 24; |
|||
min-height: 360; |
|||
background-color: @component-background; |
|||
} |
|||
|
|||
.footer { |
|||
text-align: center; |
|||
} |
|||
|
|||
// Ant overrides |
|||
.ant-layout-header { |
|||
border-left: 1px solid #000c17; |
|||
} |
@ -0,0 +1,91 @@ |
|||
import React from 'react'; |
|||
import { render, fireEvent } from '@testing-library/react'; |
|||
import { StoreProvider } from 'easy-peasy'; |
|||
import { ConnectedRouter } from 'connected-react-router'; |
|||
import { Provider } from 'react-redux'; |
|||
import { createMemoryHistory, MemoryHistory } from 'history'; |
|||
import { createReduxStore } from 'store'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import AppLayout from './AppLayout'; |
|||
|
|||
describe('AppLayout component', () => { |
|||
let history: MemoryHistory; |
|||
const renderComponent = (path?: string) => { |
|||
history = createMemoryHistory(); |
|||
if (path) history.push(path); |
|||
const store = createReduxStore(); |
|||
const app = ( |
|||
<StoreProvider store={store}> |
|||
<Provider store={store as any}> |
|||
<ConnectedRouter history={history}> |
|||
<AppLayout> |
|||
<p data-tid="hello">Hello World!</p> |
|||
</AppLayout> |
|||
</ConnectedRouter> |
|||
</Provider> |
|||
</StoreProvider> |
|||
); |
|||
|
|||
return render(app); |
|||
}; |
|||
|
|||
const changeLanguageMock = () => { |
|||
// get access to the mocked 'changeLanguage' function
|
|||
const { i18n } = useTranslation(); |
|||
const changeLanguage = (i18n.changeLanguage as unknown) as jest.Mock< |
|||
typeof i18n.changeLanguage |
|||
>; |
|||
return changeLanguage; |
|||
}; |
|||
|
|||
it('should contain a "Hello World!" text', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
expect(getByTestId('hello')).toHaveTextContent('Hello World!'); |
|||
}); |
|||
|
|||
it('should contain a collapse trigger', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
expect(getByTestId('trigger')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should be colappsed by default', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
expect(getByTestId('sider')).toHaveClass('ant-layout-sider-collapsed'); |
|||
}); |
|||
|
|||
it('should expand menu when trigger clicked', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
fireEvent.click(getByTestId('trigger')); |
|||
expect(getByTestId('sider')).not.toHaveClass('ant-layout-sider-collapsed'); |
|||
}); |
|||
|
|||
it('should set language to English', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
const changeLanguage = changeLanguageMock(); |
|||
fireEvent.click(getByTestId('english')); |
|||
expect(changeLanguage.mock.calls.length).toBe(1); |
|||
expect(changeLanguage.mock.calls[0][0]).toBe('en-US'); |
|||
changeLanguage.mockClear(); |
|||
}); |
|||
|
|||
it('should set language to Spanish', async () => { |
|||
const { getByTestId } = renderComponent(); |
|||
const changeLanguage = changeLanguageMock(); |
|||
fireEvent.click(getByTestId('spanish')); |
|||
expect(changeLanguage.mock.calls.length).toBe(1); |
|||
expect(changeLanguage.mock.calls[0][0]).toBe('es'); |
|||
changeLanguage.mockClear(); |
|||
}); |
|||
|
|||
it('should navigate to counter page when Counter link clicked', () => { |
|||
const { getByTestId } = renderComponent(); |
|||
fireEvent.click(getByTestId('nav-counter')); |
|||
expect(history.location.pathname).toEqual('/counter'); |
|||
}); |
|||
|
|||
it('should navigate to home page when logo clicked', () => { |
|||
const { getByTestId } = renderComponent('/counter'); |
|||
fireEvent.click(getByTestId('logo')); |
|||
expect(history.location.pathname).toEqual('/'); |
|||
}); |
|||
}); |
@ -0,0 +1,87 @@ |
|||
import React, { useState } from 'react'; |
|||
import { Layout, Menu, Icon } from 'antd'; |
|||
import { Link } from 'react-router-dom'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { HOME, COUNTER } from 'components/Routes'; |
|||
import logo from 'resources/logo.png'; |
|||
import styles from './AppLayout.module.less'; |
|||
|
|||
const { Header, Content, Footer, Sider } = Layout; |
|||
const { SubMenu } = Menu; |
|||
|
|||
interface Props { |
|||
children: React.ReactNode; |
|||
} |
|||
|
|||
const AppLayout: React.FC<Props> = (props: Props) => { |
|||
const { t, i18n } = useTranslation(); |
|||
const [collapsed, setCollapsed] = useState(true); |
|||
const toggle = () => setCollapsed(!collapsed); |
|||
const setEnglish = () => i18n.changeLanguage('en-US'); |
|||
const setSpanish = () => i18n.changeLanguage('es'); |
|||
|
|||
return ( |
|||
<Layout className={styles.layout}> |
|||
<Sider collapsible collapsed={collapsed} trigger={null} data-tid="sider"> |
|||
<div className={styles.logo}> |
|||
<Link to={HOME} data-tid="logo"> |
|||
<img src={logo} alt="logo" /> |
|||
{!collapsed && <span>Ditto</span>} |
|||
</Link> |
|||
</div> |
|||
<Menu theme="dark" mode="inline" selectable={false}> |
|||
<Menu.Item key="1"> |
|||
<Link to={HOME} data-tid="nav-home"> |
|||
<Icon type="pie-chart" /> |
|||
<span>{t('cmps.app-layout.home', 'Home')}</span> |
|||
</Link> |
|||
</Menu.Item> |
|||
<Menu.Item key="2"> |
|||
<Link to={COUNTER} data-tid="nav-counter"> |
|||
<Icon type="desktop" /> |
|||
<span>{t('cmps.app-layout.counter', 'Counter')}</span> |
|||
</Link> |
|||
</Menu.Item> |
|||
<SubMenu |
|||
key="sub1" |
|||
title={ |
|||
<span> |
|||
<Icon type="user" /> |
|||
<span>{t('cmps.app-layout.menu', 'Menu')}</span> |
|||
</span> |
|||
} |
|||
> |
|||
<Menu.Item key="3">{t('cmps.app-layout.item1', 'Item 1')}</Menu.Item> |
|||
<Menu.Item key="4">{t('cmps.app-layout.item2', 'Item 2')}</Menu.Item> |
|||
<Menu.Item key="5">{t('cmps.app-layout.item3', 'Item 3')}</Menu.Item> |
|||
</SubMenu> |
|||
</Menu> |
|||
</Sider> |
|||
<Layout> |
|||
<Header className={styles.header}> |
|||
<Icon |
|||
className={styles.trigger} |
|||
type={collapsed ? 'menu-unfold' : 'menu-fold'} |
|||
onClick={toggle} |
|||
data-tid="trigger" |
|||
/> |
|||
</Header> |
|||
<Content className={styles.content}> |
|||
<div className={styles.container}>{props.children}</div> |
|||
</Content> |
|||
<Footer className={styles.footer}> |
|||
React App © 2019 Fomo Bros{' '} |
|||
<a href="/#" data-tid="english" onClick={setEnglish}> |
|||
EN |
|||
</a>{' '} |
|||
|{' '} |
|||
<a href="/#" data-tid="spanish" onClick={setSpanish}> |
|||
ES |
|||
</a> |
|||
</Footer> |
|||
</Layout> |
|||
</Layout> |
|||
); |
|||
}; |
|||
|
|||
export default AppLayout; |
@ -0,0 +1 @@ |
|||
export { default as AppLayout } from './AppLayout'; |
@ -0,0 +1,47 @@ |
|||
import i18n from 'i18next'; |
|||
import { app, remote } from 'electron'; |
|||
import { initReactI18next } from 'react-i18next'; |
|||
|
|||
const detectedLang = (app || remote.app).getLocale(); |
|||
|
|||
const config: LocalConfig = { |
|||
fallbackLng: 'en-US', |
|||
languages: { |
|||
'en-US': 'English', |
|||
es: 'Español', |
|||
}, |
|||
}; |
|||
|
|||
const resources = Object.keys(config.languages).reduce( |
|||
(acc: { [key: string]: any }, lang) => { |
|||
acc[lang] = { |
|||
translation: require(`./locales/${lang}.json`), |
|||
}; |
|||
return acc; |
|||
}, |
|||
Object, |
|||
); |
|||
|
|||
const whitelist = Object.keys(config.languages).reduce((acc: string[], lang) => { |
|||
acc.push(lang); |
|||
|
|||
if (lang.includes('-')) { |
|||
acc.push(lang.substring(0, lang.indexOf('-'))); |
|||
} |
|||
|
|||
return acc; |
|||
}, []); |
|||
|
|||
i18n.use(initReactI18next).init({ |
|||
lng: detectedLang || 'en-US', |
|||
resources, |
|||
whitelist, |
|||
fallbackLng: config.fallbackLng, |
|||
debug: process.env.NODE_ENV !== 'production', |
|||
keySeparator: false, |
|||
interpolation: { |
|||
escapeValue: false, |
|||
}, |
|||
}); |
|||
|
|||
export default i18n; |
@ -0,0 +1,18 @@ |
|||
{ |
|||
"cmps.app-layout.home": "Home", |
|||
"cmps.app-layout.counter": "Counter", |
|||
"cmps.app-layout.menu": "Menu", |
|||
"cmps.app-layout.item1": "Item 1", |
|||
"cmps.app-layout.item2": "Item 2", |
|||
"cmps.app-layout.item3": "Item 3", |
|||
"cmps.home.card-title": "Welcome to Ditto", |
|||
"cmps.home.card-description": "Much evil soon high in hope do view. Out may few northward believing attempted. Yet timed being songs marry one defer men our. Although finished blessing do of. Consider speaking me prospect whatever if. Ten nearer rather hunted six parish indeed number. Allowance repulsive sex may contained can set suspected abilities cordially. Do part am he high rest that. So fruit to ready it being views match. Was justice improve age article between. No projection as up preference reasonably delightful celebrated. Preserved and abilities assurance tolerably breakfast use saw. And painted letters forming far village elderly compact. Her rest west each spot his and you knew. Estate gay wooded depart six far her. Of we be have it lose gate bred. Do separate removing or expenses in. Had covered but evident chapter matters anxious.", |
|||
"cmps.home.me-btn": "Click Me!", |
|||
"cmps.home.play": "Play with the <2>Counter</2> thing", |
|||
"cmps.home.success-text": "Success Tips", |
|||
"cmps.counter.increment": "Increment", |
|||
"cmps.counter.decrement": "Decrement", |
|||
"cmps.counter.increment-odd": "Increment Odd", |
|||
"cmps.counter.increment-async": "Increment Async", |
|||
"models.counter.increment-async.error": "Increment Async prohibited when count is 3." |
|||
} |
@ -0,0 +1,18 @@ |
|||
{ |
|||
"cmps.app-layout.home": "Casa", |
|||
"cmps.app-layout.counter": "Mostrador", |
|||
"cmps.app-layout.menu": "Menú", |
|||
"cmps.app-layout.item1": "Objeto 1", |
|||
"cmps.app-layout.item2": "Objeto 2", |
|||
"cmps.app-layout.item3": "Objeto 3", |
|||
"cmps.home.card-title": "Bienvenido a Ditto", |
|||
"cmps.home.card-description": "Mucho mal pronto pronto alto en esperanza ver. Fuera se pueden intentar algunos creyentes hacia el norte. Sin embargo, las canciones son cronometradas y se casan con un hombre diferente. Aunque terminó la bendición de hacer. Considere hablarme prospectar lo que sea si. Diez más cercanos, más bien cazados, seis parroquias de hecho número. Permiso que puede contener el sexo repulsivo puede establecer sospechas habilidades cordialmente. Haz parte, en lo alto, descansa eso. Así que las frutas para que estén listas sean vistas coinciden. Se hizo justicia entre la edad del artículo. Ninguna proyección como preferencia celebrada razonablemente encantadora celebrada. Preservado y garantía de habilidades tolerablemente el uso de desayunos. Y pintó letras formando pueblo lejano compacto de ancianos. Ella descansa al oeste de cada punto suyo y tú lo sabías. Finca gay boscosa partió seis lejos de ella. De nosotros seremos perdidos por la puerta criada. Haga retiros separados o gastos en. Había cubierto, pero el capítulo evidente importa ansioso.", |
|||
"cmps.home.me-btn": "Haz click en mi", |
|||
"cmps.home.play": "Jugar con el <2>contador</2>", |
|||
"cmps.home.success-text": "Consejos de éxito", |
|||
"cmps.counter.increment": "Incremento", |
|||
"cmps.counter.decrement": "Decremento", |
|||
"cmps.counter.increment-odd": "Incremento impar", |
|||
"cmps.counter.increment-async": "Incremento asincrónico", |
|||
"models.counter.increment-async.error": "Incremento de asíncrono prohibido cuando el conteo es 3." |
|||
} |
@ -0,0 +1,11 @@ |
|||
body { |
|||
margin: 0; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', |
|||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
} |
|||
|
|||
code { |
|||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; |
|||
} |
@ -0,0 +1,7 @@ |
|||
import React from 'react'; |
|||
import ReactDOM from 'react-dom'; |
|||
import App from './components/App'; |
|||
import './index.less'; |
|||
import './i18n'; |
|||
|
|||
ReactDOM.render(<App />, document.getElementById('root')); |
@ -0,0 +1,10 @@ |
|||
/// <reference types="react-scripts" />
|
|||
|
|||
declare module '*.module.less'; |
|||
|
|||
interface LocalConfig { |
|||
fallbackLng: string; |
|||
languages: { |
|||
[key: string]: string; |
|||
}; |
|||
} |
After Width: | Height: | Size: 917 B |
@ -0,0 +1,19 @@ |
|||
// react-testing-library renders your components to document.body,
|
|||
// this will ensure they're removed after each test.
|
|||
import '@testing-library/react/cleanup-after-each'; |
|||
// this adds jest-dom's custom assertions
|
|||
import '@testing-library/jest-dom/extend-expect'; |
|||
|
|||
import { configure } from '@testing-library/react'; |
|||
|
|||
configure({ testIdAttribute: 'data-tid' }); |
|||
|
|||
// FIXME Remove when we upgrade to React >= 16.9
|
|||
// see https://github.com/testing-library/react-testing-library/issues/281#issuecomment-507584839
|
|||
const originalConsoleError = console.error; |
|||
console.error = (...args) => { |
|||
if (/Warning.*not wrapped in act/.test(args[0])) { |
|||
return; |
|||
} |
|||
originalConsoleError(...args); |
|||
}; |
@ -0,0 +1,52 @@ |
|||
import { routerMiddleware } from 'connected-react-router'; |
|||
import { createStore, createTypedHooks } from 'easy-peasy'; |
|||
import { createHashHistory } from 'history'; |
|||
import { createLogger } from 'redux-logger'; |
|||
import { createModel, RootModel } from './models'; |
|||
|
|||
export const history = createHashHistory(); |
|||
|
|||
export const createReduxStore = () => { |
|||
// Redux store Configuration
|
|||
const middleware = []; |
|||
|
|||
// Skip redux logs in console during the tests
|
|||
if (process.env.NODE_ENV !== 'test') { |
|||
// Logging Middleware
|
|||
const logger = createLogger({ |
|||
level: 'info', |
|||
collapsed: true, |
|||
}); |
|||
|
|||
middleware.push(logger); |
|||
} |
|||
|
|||
// Router Middleware
|
|||
const router = routerMiddleware(history); |
|||
middleware.push(router); |
|||
|
|||
const models = createModel(history); |
|||
|
|||
// create easy-peasy store
|
|||
return createStore(models, { |
|||
middleware, |
|||
}); |
|||
}; |
|||
|
|||
const store = createReduxStore(); |
|||
|
|||
// export hooks directly from the store to get proper type inference
|
|||
const typedHooks = createTypedHooks<RootModel>(); |
|||
export const { useStoreActions, useStoreDispatch, useStoreState } = typedHooks; |
|||
|
|||
// enable hot reload for models
|
|||
if (process.env.NODE_ENV === 'development') { |
|||
if ((module as any).hot) { |
|||
(module as any).hot.accept('./models', () => { |
|||
const models = createModel(history); |
|||
store.reconfigure(models); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export default store; |
@ -0,0 +1,54 @@ |
|||
import counterModel from './counter'; |
|||
import { createStore } from 'easy-peasy'; |
|||
|
|||
describe('counter model', () => { |
|||
// initialize store for type inference
|
|||
let store = createStore(counterModel); |
|||
|
|||
beforeEach(() => { |
|||
// reset the store before each test run
|
|||
store = createStore(counterModel); |
|||
}); |
|||
|
|||
it('should have a valid initial state', () => { |
|||
expect(store.getState().count).toBe(0); |
|||
}); |
|||
|
|||
it('should increment by one', () => { |
|||
store.getActions().increment(); |
|||
expect(store.getState().count).toBe(1); |
|||
}); |
|||
|
|||
it('should decrement by one', () => { |
|||
store.getActions().decrement(); |
|||
expect(store.getState().count).toBe(-1); |
|||
}); |
|||
|
|||
it('should increment by two if count is odd', () => { |
|||
// initialize state with an odd number
|
|||
store = createStore(counterModel, { initialState: { count: 1 } }); |
|||
store.getActions().incrementIfOdd(); |
|||
expect(store.getState().count).toBe(3); |
|||
}); |
|||
|
|||
it('should not increment by two if count is even', () => { |
|||
store.getActions().incrementIfOdd(); |
|||
expect(store.getState().count).toBe(0); |
|||
}); |
|||
|
|||
it('should increment after a delay', async () => { |
|||
await store.getActions().incrementAsync(); |
|||
expect(store.getState().count).toBe(1); |
|||
}); |
|||
|
|||
it('should fail to increment after a delay if count is three', async () => { |
|||
expect.assertions(1); |
|||
try { |
|||
// initialize state with count set to three
|
|||
store = createStore(counterModel, { initialState: { count: 3 } }); |
|||
await store.getActions().incrementAsync(); |
|||
} catch (e) { |
|||
expect(e.message).toMatch('models.counter.increment-async.error'); |
|||
} |
|||
}); |
|||
}); |
@ -0,0 +1,48 @@ |
|||
import { Action, action, Thunk, thunk } from 'easy-peasy'; |
|||
import i18n from 'i18next'; |
|||
import { info } from 'electron-log'; |
|||
|
|||
export interface CounterModel { |
|||
count: number; |
|||
increment: Action<CounterModel>; |
|||
decrement: Action<CounterModel>; |
|||
incrementIfOdd: Action<CounterModel>; |
|||
incrementAsync: Thunk<CounterModel, number | void>; |
|||
} |
|||
|
|||
const counterModel: CounterModel = { |
|||
// state vars
|
|||
count: 0, |
|||
// reducer actions (mutations allowed thx to immer)
|
|||
increment: action(state => { |
|||
state.count++; |
|||
info(`Incremented count in redux state to ${state.count}`); |
|||
}), |
|||
decrement: action(state => { |
|||
state.count = state.count - 1; |
|||
info(`Decremented count in redux state to ${state.count}`); |
|||
}), |
|||
incrementIfOdd: action(state => { |
|||
info(`Incrementing count in redux state by 2 if "${state.count}" is odd`); |
|||
if (state.count % 2 !== 0) { |
|||
state.count += 2; |
|||
info(`Incremented count in redux state to ${state.count}`); |
|||
} |
|||
}), |
|||
incrementAsync: thunk(async (actions, payload, { getState }) => { |
|||
info(`Incremented count in redux state asynchronously`); |
|||
return new Promise((resolve, reject) => { |
|||
setTimeout(() => { |
|||
if (getState().count !== 3) { |
|||
actions.increment(); |
|||
resolve(); |
|||
} else { |
|||
info(`Failed to increment count because it is currently ${getState().count}`); |
|||
reject(new Error(i18n.t('models.counter.increment-async.error'))); |
|||
} |
|||
}, payload || 1000); |
|||
}); |
|||
}), |
|||
}; |
|||
|
|||
export default counterModel; |
@ -0,0 +1,18 @@ |
|||
import { AnyAction } from 'redux'; |
|||
import { connectRouter, RouterState } from 'connected-react-router'; |
|||
import { History } from 'history'; |
|||
import { reducer, Reducer } from 'easy-peasy'; |
|||
import counterModel, { CounterModel } from './counter'; |
|||
|
|||
export interface RootModel { |
|||
router: Reducer<RouterState, AnyAction>; |
|||
counter: CounterModel; |
|||
} |
|||
|
|||
export const createModel = (history: History<any>): RootModel => { |
|||
const rootModel: RootModel = { |
|||
router: reducer(connectRouter(history) as any), |
|||
counter: counterModel, |
|||
}; |
|||
return rootModel; |
|||
}; |
@ -0,0 +1,40 @@ |
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|||
const { blue, red, gold } = require('@ant-design/colors'); |
|||
|
|||
module.exports = { |
|||
'@body-background': '#000C17', |
|||
'@component-background': '#08121C', |
|||
'@layout-header-background': '#001529', |
|||
'@layout-sider-background': '#001529', |
|||
'@text-color': '#D9D9D9', |
|||
'@text-color-secondary': '#D9D9D9', |
|||
'@heading-color': 'fade(#D9D9D9,85%)', |
|||
'@layout-body-background': '#000C17', |
|||
'@pro-header-box-shadow': '0 1px 4px 0 rgba(0,21,41,0.12)', |
|||
'@btn-default-bg': '#13222E', |
|||
'@btn-primary-bg': '#052F61', |
|||
'@border-color-split': '#172C41', |
|||
'@input-bg': '#08121C', |
|||
'@border-color-base': 'rgba(255, 255, 255, 0.25)', |
|||
'@btn-default-border': '#13222E', |
|||
|
|||
'@table-header-bg': '#0A1A2B', |
|||
'@table-row-hover-bg': '#0F2239', |
|||
'@table-selected-row-bg': '#0F2239', |
|||
|
|||
'@tag-default-bg': '#13222E', |
|||
'@alert-info-bg-color': '#102134', |
|||
'@highlight-color': red[7], |
|||
'@warning-color': gold[9], |
|||
'@card-actions-background': '#08121C', |
|||
'@primary-color': '#FF9500', |
|||
'@item-hover-bg': `fade(${blue[5]}, 20%)`, |
|||
'@item-active-bg': `fade(${blue[5]}, 40%)`, |
|||
'@checkbox-check-color': '#13222E', |
|||
'@disabled-color': '#404C56', |
|||
'@input-disabled-bg': '#404C56', |
|||
|
|||
'@background-color-light': '#001529', |
|||
|
|||
'@popover-bg': '#08121C', |
|||
}; |
@ -0,0 +1,20 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "es5", |
|||
"lib": ["dom", "dom.iterable", "esnext"], |
|||
"allowJs": true, |
|||
"skipLibCheck": true, |
|||
"esModuleInterop": true, |
|||
"allowSyntheticDefaultImports": true, |
|||
"strict": true, |
|||
"forceConsistentCasingInFileNames": true, |
|||
"module": "esnext", |
|||
"moduleResolution": "node", |
|||
"resolveJsonModule": true, |
|||
"isolatedModules": true, |
|||
"noEmit": true, |
|||
"jsx": "preserve", |
|||
"baseUrl": "src" |
|||
}, |
|||
"include": ["src"] |
|||
} |