diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7b5f965..f894c59 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,8 @@ -# These developers will be requested for review when someone opens a pull request. -# Uncomment the line below and mention the reviewers with their github handles -# * @webchapter +# Team Lead +* @carryall + +# Team Members +* @bterone @hanam1ni @hoangmirs @malparty @rosle @tyrro + +# Engineering Leads +CODEOWNERS @nimblehq/engineering-leads diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md index adb94cc..39e870a 100644 --- a/.github/ISSUE_TEMPLATE/bug_template.md +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -7,14 +7,14 @@ labels: "type : bug" ## Issue -Describe the issue you are facing. Show us the implementation: screenshots, gif, etc. - +Describe the issue you are facing. Show us the implementation: screenshots, GIF, etc. + ## Expected -Describe what should be the correct behaviour. - +Describe what should be the correct behavior. + ## Steps to reproduce 1. 2. -3. \ No newline at end of file +3. diff --git a/.github/ISSUE_TEMPLATE/chore_template.md b/.github/ISSUE_TEMPLATE/chore_template.md new file mode 100644 index 0000000..410c900 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/chore_template.md @@ -0,0 +1,14 @@ +--- +name: "Chore" +about: "Open a chore issue for a minor update." +title: "Update " +labels: "type : chore" +--- + +## Why + +Describe the details of the update and why it's needed. + +## Who Benefits? + +Describe who will be the beneficiaries e.g. everyone, specific chapters, clients... diff --git a/.github/ISSUE_TEMPLATE/feature_template.md b/.github/ISSUE_TEMPLATE/feature_template.md index 5a26eb9..4ad1fb4 100644 --- a/.github/ISSUE_TEMPLATE/feature_template.md +++ b/.github/ISSUE_TEMPLATE/feature_template.md @@ -7,8 +7,8 @@ labels: "type : feature" ## Why -Describe the big picture of the feature and why it's needed. - +Describe the big picture of the feature and why it's needed. + ## Who Benefits? Describe who will be the beneficiaries e.g. everyone, specific chapters, clients... diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cef8740..799316f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,11 +1,13 @@ -## What happened +https://github.com/nimblehq/react-templates/issues/? + +## What happened πŸ‘€ Describe the big picture of your changes here to communicate to the team why we should accept this pull request. -## Insight +## Insight πŸ“ -Describe in details how to test the changes. Referenced documentation are welcome as well. +Describe in detail how to test the changes. Referenced documentation is welcome as well. -## Proof Of Work πŸ’ͺ +## Proof Of Work πŸ“Ή -Show us the implementation: screenshots, gif, etc. P.S. Maximum details possible +Show us the implementation: screenshots, GIF, etc. P.S. Maximum details possible diff --git a/.github/PULL_REQUEST_TEMPLATE/release_template.md b/.github/PULL_REQUEST_TEMPLATE/release_template.md new file mode 100644 index 0000000..f4fd62d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/release_template.md @@ -0,0 +1,13 @@ +https://github.com/nimblehq/react-templates/milestone/{ID}?closed=1 + +## Features + +Provide the Pull Request IDs in the section for each type (feature, chore, and bug), e.g. + +- #1234 + +## Chores +- Same structure as in ## Feature + +## Bugs +- Same structure as in ## Feature diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..d9f8800 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.14.2 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..0094556 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 16.14.2 diff --git a/README.md b/README.md index 1e67e91..fb0bff1 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,23 @@ --- -Our templates offer a rich boilerplate to jump start React-based application development for [Create React App](https://github.com/facebook/create-react-app). +

+ + +

+ +Our template offers a rich boilerplate to jump-start React-based application development with [Create React App](https://github.com/facebook/create-react-app). + +## Getting Started -## Get Started +### Prerequisites -### Use the template +[![node-version-image](https://img.shields.io/badge/node-16.14.2-brightgreen.svg)](https://nodejs.org/download/release/v16.14.2/) -To use this template, add `--template nimble` when creating a new app. +### Usage + +To use this template, add `--template nimble` when creating a new app from the `create-react-app` command. -For example: ```sh npx create-react-app my-app --template nimble @@ -27,9 +35,9 @@ npx create-react-app my-app --template nimble yarn create react-app my-app --template nimble ``` -For more information, please refer to: +For more information about `create-react-app`, please refer to: -- [Getting Started](https://create-react-app.dev/docs/getting-started) – How to create a new app. +- [Getting Started](https://create-react-app.dev/docs/getting-started) β€” How to create a new app. - [User Guide](https://create-react-app.dev) – How to develop apps bootstrapped with Create React App. ## Template structure @@ -50,14 +58,25 @@ For more information, please refer to: └── template.json ``` -We use `Typescript` by default for our React applications. Along with the standard files from a `create-react-app` -project, the folder structure in the `src` folder is created as per the -[React Conevention](https://nimblehq.co/compass/development/code-conventions/react/#project-structure). +`Typescript` is used by default for our React applications. +With the standard files from a non-ejected `create-react-app` project, this template adds a folder structure in `/src` that follows our [React Convention](https://nimblehq.co/compass/development/code-conventions/javascript/react/#project-structure). + +## How to contribute + +To test the template locally, simply run the template install command with the path of your local `react-template` repository, prefixed by `file:`: + +```sh +npx create-react-app my-app --template file:{../path/to/your/local/template/repo} + +# or + +yarn create react-app my-app --template file:{../path/to/your/local/template/repo} +``` ## License -This project is Copyright (c) 2014 and onwards. It is free software, -and may be redistributed under the terms specified in the [LICENSE] file. +This project is Copyright (c) 2014 and onwards. +It is free software and may be redistributed under the terms specified in the [LICENSE] file. [LICENSE]: /LICENSE @@ -65,7 +84,7 @@ and may be redistributed under the terms specified in the [LICENSE] file. ![Nimble](https://assets.nimblehq.co/logo/dark/logo-dark-text-160.png) -This project is maintained and funded by Nimble. +This project is maintained and funded by [Nimble](https://nimblehq.co). We love open source and do our part in sharing our work with the community! See [our other projects][community] or [hire our team][hire] to help build your product. diff --git a/package.json b/package.json index 6c9e3f6..315933a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cra-template-nimble", - "version": "2.0.0", + "version": "2.1.0", "keywords": [ "react", "create-react-app", @@ -15,7 +15,7 @@ }, "license": "MIT", "engines": { - "node": ">=10" + "node": "^14.18.2 || ^16 || ^17" }, "bugs": { "url": "https://github.com/nimblehq/react-templates/issues" diff --git a/template.json b/template.json index fc99e2d..bb1e541 100644 --- a/template.json +++ b/template.json @@ -1,40 +1,48 @@ { "package": { "dependencies": { + "@cypress/code-coverage": "3.9.12", + "@nimblehq/eslint-config-nimble": "2.2.1", + "@testing-library/cypress": "8.0.2", "@testing-library/jest-dom": "5.11.4", - "@testing-library/react": "11.1.0", - "@testing-library/user-event": "12.1.10", - "@testing-library/cypress": "7.0.4", - "@types/node": "12.0.0", - "@types/react": "17.0.0", - "@types/react-dom": "17.0.0", - "@types/jest": "26.0.15", - "typescript": "4.1.2", - "web-vitals": "1.0.1", + "@testing-library/react": "12.1.4", + "@testing-library/user-event": "13.5.0", + "@types/jest": "27.4.1", + "@types/node": "17.0.21", + "@types/react": "17.0.40", + "@types/react-dom": "17.0.13", + "@typescript-eslint/eslint-plugin": "5.15.0", + "@typescript-eslint/parser": "5.15.0", "axios": "0.21.1", - "cypress": "7.4.0", - "cypress-react-selector": "2.3.6", - "eslint": "7.25.0", - "prettier": "2.2.1", - "@nimbl3/eslint-config-nimbl3": "2.1.1", - "eslint-plugin-prettier": "3.4.0", - "eslint-config-prettier": "8.3.0", - "eslint-plugin-cypress": "2.11.2", - "eslint-plugin-import": "2.22.1", - "eslint-plugin-jsx-a11y": "6.4.1", - "eslint-plugin-react": "7.23.2", - "eslint-plugin-react-hooks": "4.2.0", - "eslint-plugin-jest": "24.3.6", - "@typescript-eslint/eslint-plugin": "4.22.1", - "@typescript-eslint/parser": "4.22.1", - "node-sass": "4.14.1", - "stylelint": "13.13.1", - "stylelint-config-sass-guidelines": "8.0.0", - "stylelint-scss": "3.19.0", - "stylelint-order": "4.1.0", - "stylelint-config-property-sort-order-smacss": "7.1.0" + "cypress": "9.5.2", + "cypress-react-selector": "2.3.16", + "eslint": "8.11.0", + "eslint-config-prettier": "8.5.0", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-cypress": "2.12.1", + "eslint-plugin-import": "2.25.4", + "eslint-plugin-jest": "26.1.1", + "eslint-plugin-jsx-a11y": "6.5.1", + "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-react": "7.29.4", + "eslint-plugin-react-hooks": "4.3.0", + "i18next": "21.6.14", + "i18next-browser-languagedetector": "6.1.3", + "i18next-http-backend": "1.4.0", + "node-sass": "7.0.1", + "prettier": "2.6.0", + "react-i18next": "11.16.1", + "stylelint": "14.6.0", + "stylelint-config-property-sort-order-smacss": "9.0.0", + "stylelint-config-sass-guidelines": "9.0.1", + "stylelint-order": "5.0.0", + "stylelint-scss": "4.2.0", + "typescript": "4.6.2", + "web-vitals": "2.1.4" }, "scripts": { + "start": "react-scripts -r @cypress/instrument-cra start", + "test:coverage": "react-scripts test --coverage --watchAll=false && yarn cypress:run && node ./scripts/coverage-merge.js && nyc report", "lint": "eslint ./src ./cypress --ext .ts,.tsx", "lint:fix": "eslint ./src ./cypress --ext .ts,.tsx --fix", "stylelint": "stylelint '**/*.scss'", @@ -43,6 +51,18 @@ "codebase:fix": "yarn lint:fix && yarn stylelint:fix", "cypress:run": "cypress run", "cypress:open": "cypress open" + }, + "jest": { + "collectCoverageFrom": ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"], + "coverageReporters": ["json"] + }, + "nyc": { + "report-dir": "coverage/cypress", + "exclude": ["src/reportWebVitals.ts"], + "excludeAfterRemap": true + }, + "devDependencies": { + "@cypress/instrument-cra": "1.4.0" } } } diff --git a/template/.eslintrc.js b/template/.eslintrc.js index f6cb2de..47df027 100644 --- a/template/.eslintrc.js +++ b/template/.eslintrc.js @@ -3,42 +3,36 @@ module.exports = { es6: true, browser: true, node: true, - jest: true + jest: true, }, extends: [ - '@nimbl3/eslint-config-nimbl3', + '@nimblehq/eslint-config-nimble', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:jsx-a11y/recommended', 'plugin:import/errors', - 'plugin:prettier/recommended' + 'plugin:prettier/recommended', ], overrides: [ { files: 'src/tests/**/*.test.ts', - extends: [ - 'plugin:jest/recommended', - 'plugin:jest/style' - ] + extends: ['plugin:jest/recommended', 'plugin:jest/style'], }, { files: 'cypress/**/*.ts', - extends: [ - 'plugin:cypress/recommended' - ] - } + extends: ['plugin:cypress/recommended'], + }, ], parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 6, sourceType: 'module', ecmaFeatures: { - jsx: true - } + jsx: true, + }, }, rules: { - 'max-len': ['error', { code: 120 }], 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'react/jsx-filename-extension': [2, { extensions: ['.tsx'] }], @@ -50,21 +44,21 @@ module.exports = { { pattern: 'react*', group: 'external', - position: 'before' + position: 'before', }, { pattern: 'css/*|*.scss|*.svg|.png', group: 'internal', - position: 'after' - } + position: 'after', + }, ], pathGroupsExcludedImportTypes: ['react'], 'newlines-between': 'always', alphabetize: { order: 'asc', - caseInsensitive: true - } - } + caseInsensitive: true, + }, + }, ], 'import/extensions': [ 'error', @@ -74,8 +68,8 @@ module.exports = { svg: 'always', png: 'always', json: 'always', - spec: 'always' - } + spec: 'always', + }, ], 'no-use-before-define': 'off', 'no-unused-vars': 'off', @@ -83,15 +77,18 @@ module.exports = { '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-use-before-define': ['error'], + 'prettier/prettier': ['error'], }, settings: { react: { - version: 'detect' + version: 'detect', }, 'import/resolver': { + typescript: {}, node: { - extensions: ['.ts', '.tsx'] - } - } - } + extensions: ['.js', '.jsx', '.ts', '.tsx'], + moduleDirectory: ['node_modules', 'src/'], + }, + }, + }, } diff --git a/template/.github/ISSUE_TEMPLATE/bug_template.md b/template/.github/ISSUE_TEMPLATE/bug_template.md new file mode 100644 index 0000000..39e870a --- /dev/null +++ b/template/.github/ISSUE_TEMPLATE/bug_template.md @@ -0,0 +1,20 @@ +--- +name: "Bug Report" +about: "You found something that is not working. Report it so that it can be fixed. πŸ‘·β€" +title: "Fix: " +labels: "type : bug" +--- + +## Issue + +Describe the issue you are facing. Show us the implementation: screenshots, GIF, etc. + +## Expected + +Describe what should be the correct behavior. + +## Steps to reproduce + +1. +2. +3. diff --git a/template/.github/ISSUE_TEMPLATE/chore_template.md b/template/.github/ISSUE_TEMPLATE/chore_template.md new file mode 100644 index 0000000..410c900 --- /dev/null +++ b/template/.github/ISSUE_TEMPLATE/chore_template.md @@ -0,0 +1,14 @@ +--- +name: "Chore" +about: "Open a chore issue for a minor update." +title: "Update " +labels: "type : chore" +--- + +## Why + +Describe the details of the update and why it's needed. + +## Who Benefits? + +Describe who will be the beneficiaries e.g. everyone, specific chapters, clients... diff --git a/template/.github/ISSUE_TEMPLATE/feature_template.md b/template/.github/ISSUE_TEMPLATE/feature_template.md new file mode 100644 index 0000000..4ad1fb4 --- /dev/null +++ b/template/.github/ISSUE_TEMPLATE/feature_template.md @@ -0,0 +1,14 @@ +--- +name: "Feature" +about: "Open a feature issue to add new functionalities." +title: "Add " +labels: "type : feature" +--- + +## Why + +Describe the big picture of the feature and why it's needed. + +## Who Benefits? + +Describe who will be the beneficiaries e.g. everyone, specific chapters, clients... diff --git a/template/.github/PULL_REQUEST_TEMPLATE.md b/template/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..24ee763 --- /dev/null +++ b/template/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +https://github.com/nimblehq/{YOUR_REPOSITORY}/issues/? + +## What happened πŸ‘€ + +Describe the big picture of your changes here to communicate to the team why we should accept this pull request. + +## Insight πŸ“ + +Describe in detail how to test the changes. Referenced documentation is welcome as well. + +## Proof Of Work πŸ“Ή + +Show us the implementation: screenshots, GIF, etc. P.S. Maximum details possible diff --git a/template/.github/PULL_REQUEST_TEMPLATE/release_template.md b/template/.github/PULL_REQUEST_TEMPLATE/release_template.md new file mode 100644 index 0000000..16108d6 --- /dev/null +++ b/template/.github/PULL_REQUEST_TEMPLATE/release_template.md @@ -0,0 +1,13 @@ +https://github.com/nimblehq/{YOUR_REPOSITORY}/milestone/{ID}?closed=1 + +## Features + +Provide the Pull Request IDs in the section for each type (feature, chore, and bug), e.g. + +- #1234 + +## Chores +- Same structure as in ## Feature + +## Bugs +- Same structure as in ## Feature diff --git a/template/.prettierrc b/template/.prettierrc index 10e636b..9ba4a65 100644 --- a/template/.prettierrc +++ b/template/.prettierrc @@ -1,6 +1,7 @@ { - "semi": false, + "semi": true, "singleQuote": true, - "trailingComma": "none", - "printWidth": 120 + "trailingComma": "es5", + "printWidth": 130, + "tabWidth": 2 } diff --git a/template/README.md b/template/README.md index ee7da5d..63ea4b0 100644 --- a/template/README.md +++ b/template/README.md @@ -6,10 +6,13 @@ This project was bootstrapped with [Nimble React template](https://github.com/ni In the project directory, you can run: -`yarn start` : Runs the app in the development mode. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +`yarn start`: Runs the app in the development mode. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. -`yarn test`: Launches the test runner in the interactive watch mode. See the section about -[running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +`yarn test`: Launches the test runner in the interactive watch mode. See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +`yarn test:coverage`: Both Unit Tests (Jest) and UI Tests (cypress) generate test coverage analytics. The below command runs all tests and merges both coverage files into a single report. + +> Use the `.nyc_output/out.json` artefact in your CI/CD pipeline to reuse the code coverage data. `yarn build`: Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes. Your app is ready to be deployed! @@ -17,8 +20,7 @@ optimizes the build for the best performance. The build is minified and the file See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. `yarn eject`: If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This -command will remove the single build dependency from your project. Instead, it will copy all the configuration files -and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. +command will remove the single build dependency from your project. Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. **Note: this is a one-way operation. Once you `eject`, you can’t go back!** @@ -39,6 +41,20 @@ All of the commands except `eject` will still work, but they will point to the c `yarn cypress:open`: Opens the Cypress Test Runner. [Check options](https://docs.cypress.io/guides/guides/command-line#cypress-open) +## Localization + +This project uses the [react-i18next](https://react.i18next.com/) package to handle the project locales. + +To add a new language + +- Add the new language bigram to the `supportedLanguages` array in `src/i18n.ts` β€” use this array to list all available languages in a 'change language' component +- Add the new translation file in `public/locales/{lang_bigram}/translation.json` + +To change the default fallback language + +- Either edit the value of the environment variable `REACT_APP_DEFAULT_LANGUAGE` (cf. the `env.example` file) +- Either directly edit the const `DEFAULT_FALLBACK_LANGUAGE` in `src/i18n.ts` + ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). diff --git a/template/cypress/integration/init.spec.ts b/template/cypress/integration/init.spec.ts index c3d280f..42ef463 100644 --- a/template/cypress/integration/init.spec.ts +++ b/template/cypress/integration/init.spec.ts @@ -1,9 +1,10 @@ describe('Cypress', () => { it('is working', () => { - expect(true).to.equal(true) - }) + expect(true).to.equal(true); + }); it('visits the app', () => { - cy.visit('/') - }) -}) + cy.visit('/'); + cy.findByTestId('app-link').should('be.visible'); + }); +}); diff --git a/template/cypress/plugins/index.ts b/template/cypress/plugins/index.ts index 9f41d00..4c53539 100644 --- a/template/cypress/plugins/index.ts +++ b/template/cypress/plugins/index.ts @@ -1,3 +1,15 @@ -/* eslint-disable */ +import browserify from '@cypress/browserify-preprocessor'; +import task from '@cypress/code-coverage/task'; -module.exports = (on, config) => {}; +module.exports = (on, config) => { + task(on, config); + + on( + 'file:preprocessor', + browserify({ + typescript: require.resolve('typescript'), + }) + ); + + return config; +}; diff --git a/template/cypress/support/commands.ts b/template/cypress/support/commands.ts index 88c99aa..331779a 100644 --- a/template/cypress/support/commands.ts +++ b/template/cypress/support/commands.ts @@ -1 +1,3 @@ +import '@testing-library/cypress/add-commands'; + // Import commands from the commands folder. diff --git a/template/cypress/support/component-selector.ts b/template/cypress/support/component-selector.ts index c16172e..2642f38 100644 --- a/template/cypress/support/component-selector.ts +++ b/template/cypress/support/component-selector.ts @@ -1 +1 @@ -import 'cypress-react-selector' +import 'cypress-react-selector'; diff --git a/template/cypress/support/configure-testing-library.ts b/template/cypress/support/configure-testing-library.ts index 7dee8bd..6d5925f 100644 --- a/template/cypress/support/configure-testing-library.ts +++ b/template/cypress/support/configure-testing-library.ts @@ -1,4 +1,4 @@ // we are configuring the default lookout of data attribute of // cypress-testing-library(https://github.com/testing-library/cypress-testing-library). -import { configure } from '@testing-library/cypress' -configure({ testIdAttribute: 'data-test-id' }) +import { configure } from '@testing-library/cypress'; +configure({ testIdAttribute: 'data-test-id' }); diff --git a/template/cypress/support/index.ts b/template/cypress/support/index.ts index 6b33e3a..9277a39 100644 --- a/template/cypress/support/index.ts +++ b/template/cypress/support/index.ts @@ -1,5 +1,6 @@ /* eslint-disable */ +import '@cypress/code-coverage/support' import './commands' import './configure-testing-library' import './component-selector' diff --git a/template/env.example b/template/env.example new file mode 100644 index 0000000..f2c3eea --- /dev/null +++ b/template/env.example @@ -0,0 +1 @@ +REACT_APP_DEFAULT_LANGUAGE=en diff --git a/template/gitignore b/template/gitignore index 4d29575..8f34940 100644 --- a/template/gitignore +++ b/template/gitignore @@ -6,7 +6,10 @@ .pnp.js # testing +/cypress/screenshots +/cypress/videos /coverage +.nyc_output # production /build @@ -21,3 +24,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# editors +.vscode diff --git a/template/public/locales/en/translation.json b/template/public/locales/en/translation.json new file mode 100644 index 0000000..bab8636 --- /dev/null +++ b/template/public/locales/en/translation.json @@ -0,0 +1,6 @@ +{ + "sample_page": { + "message": "Edit {{codeSample}} and save to reload", + "learn_react": "Learn React" + } +} diff --git a/template/scripts/coverage-merge.js b/template/scripts/coverage-merge.js new file mode 100644 index 0000000..c5c6f1b --- /dev/null +++ b/template/scripts/coverage-merge.js @@ -0,0 +1,28 @@ +/** + * This script merges the coverage reports from Cypress and Jest into a single one, + * inside the "coverage" folder + */ +const { execSync } = require('child_process') +const fs = require('fs-extra') +const COVERAGE_JEST_FOLDER = 'coverage' +const COVERAGE_CYPRESS_FOLDER = 'coverage/cypress' +const NYC_FOLDER = '.nyc_output' +const REPORTS_FOLDER = 'coverage/reports' +const FINAL_OUTPUT_FOLDER = 'coverage/merged' +const run = (commands) => { + commands.forEach((command) => execSync(command, { stdio: 'inherit' })) +} +// Create the reports folder and move the reports from cypress and jest inside it +fs.emptyDirSync(NYC_FOLDER) +fs.emptyDirSync(REPORTS_FOLDER) +fs.copyFileSync(`${COVERAGE_CYPRESS_FOLDER}/coverage-final.json`, `${REPORTS_FOLDER}/from-cypress.json`) +fs.copyFileSync(`${COVERAGE_JEST_FOLDER}/coverage-final.json`, `${REPORTS_FOLDER}/from-jest.json`) +fs.emptyDirSync('.nyc_output') +fs.emptyDirSync(FINAL_OUTPUT_FOLDER) +// Run "nyc merge" inside the reports folder, merging the two coverage files into one, +// then generate the final report on the coverage folder +run([ + // "nyc merge" will create a "coverage.json" file on the root, we move it to .nyc_output + `nyc merge ${REPORTS_FOLDER} && mv coverage.json .nyc_output/out.json`, + `nyc report --reporter lcov --report-dir ${FINAL_OUTPUT_FOLDER}` +]) diff --git a/template/src/App.test.tsx b/template/src/App.test.tsx index 352c4c6..7551bb7 100644 --- a/template/src/App.test.tsx +++ b/template/src/App.test.tsx @@ -1,11 +1,16 @@ -import React from 'react' +import React from 'react'; -import { render, screen } from '@testing-library/react' +import { render, screen } from '@testing-library/react'; -import App from './App' +import App from './App'; -test('renders learn react link', () => { - render() - const linkElement = screen.getByText(/learn react/i) - expect(linkElement).toBeInTheDocument() -}) +describe('App', () => { + it('renders learn react link', () => { + render(); + + const linkElement = screen.getByTestId('app-link'); + + expect(linkElement).toBeInTheDocument(); + expect(linkElement).toHaveTextContent('sample_page.learn_react'); + }); +}); diff --git a/template/src/App.tsx b/template/src/App.tsx index eaf1e9b..e83b49b 100644 --- a/template/src/App.tsx +++ b/template/src/App.tsx @@ -1,23 +1,24 @@ -import React from 'react' +import React from 'react'; +import { useTranslation } from 'react-i18next'; -import logo from './assets/images/logo.svg' -import './dummy.scss' -import './assets/stylesheets/application.scss' +import logo from './assets/images/logo.svg'; +import './dummy.scss'; +import './assets/stylesheets/application.scss'; + +const App = (): JSX.Element => { + const { t } = useTranslation(); -function App(): JSX.Element { return (
logo -

- Edit src/App.tsx and save to reload. -

- - Learn React +

{t('sample_page.message', { codeSample: 'src/App.tsx' })}

+
+ {t('sample_page.learn_react')}
- ) -} + ); +}; -export default App +export default App; diff --git a/template/src/__mocks__/i18n.ts b/template/src/__mocks__/i18n.ts new file mode 100644 index 0000000..61caa14 --- /dev/null +++ b/template/src/__mocks__/i18n.ts @@ -0,0 +1,3 @@ +const configureI18n = jest.fn(); + +export default configureI18n; diff --git a/template/src/__mocks__/react-i18next.ts b/template/src/__mocks__/react-i18next.ts new file mode 100644 index 0000000..3acf683 --- /dev/null +++ b/template/src/__mocks__/react-i18next.ts @@ -0,0 +1,8 @@ +const useMock = { + t: (k: string): string => { + return k; + }, + i18n: {}, +}; + +export const useTranslation = (): { t: (k: string) => string; i18n: Record } => useMock; diff --git a/template/src/i18n.ts b/template/src/i18n.ts new file mode 100644 index 0000000..f17d691 --- /dev/null +++ b/template/src/i18n.ts @@ -0,0 +1,35 @@ +import { initReactI18next } from 'react-i18next'; + +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from 'i18next-http-backend'; + +export const supportedLanguages = ['en']; + +const DEFAULT_FALLBACK_LANGUAGE = 'en'; + +const configureI18n = (): void => { + i18n + // load translation using http -> see /public/locales + // (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) + // learn more: https://github.com/i18next/i18next-http-backend + .use(Backend) + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: process.env.REACT_APP_DEFAULT_LANGUAGE ?? DEFAULT_FALLBACK_LANGUAGE, + debug: false, + supportedLngs: supportedLanguages, + + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + }); +}; + +export default configureI18n; diff --git a/template/src/index.tsx b/template/src/index.tsx index dc54ab9..5742c3d 100644 --- a/template/src/index.tsx +++ b/template/src/index.tsx @@ -1,17 +1,22 @@ -import React from 'react' -import ReactDOM from 'react-dom' +import React, { Suspense } from 'react'; +import ReactDOM from 'react-dom'; -import App from './App' -import reportWebVitals from './reportWebVitals' +import App from './App'; +import configureI18n from './i18n'; +import reportWebVitals from './reportWebVitals'; + +configureI18n(); ReactDOM.render( - + + + , document.getElementById('root') -) +); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals() +reportWebVitals(); diff --git a/template/src/lib/requestManager.test.ts b/template/src/lib/requestManager.test.ts new file mode 100644 index 0000000..93f3bec --- /dev/null +++ b/template/src/lib/requestManager.test.ts @@ -0,0 +1,46 @@ +import axios from 'axios'; + +import requestManager, { defaultOptions } from './requestManager'; + +jest.mock('axios'); + +describe('requestManager', () => { + const endPoint = 'https://sample-endpoint.com/api/'; + + it('fetches successfully data from an API', async () => { + const responseData = { + data: [ + { id: 1, value: 'first object' }, + { id: 2, value: 'second object' }, + ], + }; + + const requestSpy = jest.spyOn(axios, 'request').mockImplementation(() => Promise.resolve(responseData)); + + await expect(requestManager('POST', endPoint)).resolves.toEqual(responseData.data); + + requestSpy.mockRestore(); + }); + + it('fetches the provided endPoint', async () => { + const requestOptions = { ...defaultOptions, method: 'POST', url: endPoint }; + + const requestSpy = jest.spyOn(axios, 'request').mockImplementation(() => Promise.resolve({})); + + await requestManager('POST', endPoint); + + expect(axios.request).toHaveBeenCalledWith(requestOptions); + + requestSpy.mockRestore(); + }); + + it('fetches erroneously data from an API', async () => { + const errorMessage = 'Network Error'; + + const requestSpy = jest.spyOn(axios, 'request').mockImplementation(() => Promise.reject(new Error(errorMessage))); + + await expect(requestManager('POST', endPoint)).rejects.toThrow(errorMessage); + + requestSpy.mockRestore(); + }); +}); diff --git a/template/src/lib/requestManager.ts b/template/src/lib/requestManager.ts index 30614f7..154b0f0 100644 --- a/template/src/lib/requestManager.ts +++ b/template/src/lib/requestManager.ts @@ -1,8 +1,8 @@ -import axios, { Method as HTTPMethod, ResponseType, AxiosRequestConfig, AxiosResponse } from 'axios' +import axios, { Method as HTTPMethod, ResponseType, AxiosRequestConfig, AxiosResponse } from 'axios'; -const defaultOptions: { responseType: ResponseType } = { - responseType: 'json' -} +export const defaultOptions: { responseType: ResponseType } = { + responseType: 'json', +}; /** * The main API access function that comes preconfigured with useful defaults. @@ -14,17 +14,21 @@ const defaultOptions: { responseType: ResponseType } = { * an error object for its reason */ -function requestManager(method: HTTPMethod, endpoint: string, requestOptions: AxiosRequestConfig = {}) { +const requestManager = ( + method: HTTPMethod, + endpoint: string, + requestOptions: AxiosRequestConfig = {} +): Promise => { const requestParams: AxiosRequestConfig = { method, url: endpoint, ...defaultOptions, - ...requestOptions - } + ...requestOptions, + }; - return axios.request(requestParams).then((response: AxiosResponse) => { - return response.data - }) -} + return axios.request(requestParams).then((response: AxiosResponse) => { + return response.data; + }); +}; -export default requestManager +export default requestManager; diff --git a/template/src/reportWebVitals.ts b/template/src/reportWebVitals.ts index dfe2a4f..59de1b7 100644 --- a/template/src/reportWebVitals.ts +++ b/template/src/reportWebVitals.ts @@ -1,15 +1,15 @@ -import { ReportHandler } from 'web-vitals' +import type { ReportHandler } from 'web-vitals'; const reportWebVitals = (onPerfEntry?: ReportHandler): void => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry) - getFID(onPerfEntry) - getFCP(onPerfEntry) - getLCP(onPerfEntry) - getTTFB(onPerfEntry) - }) + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); } -} +}; -export default reportWebVitals +export default reportWebVitals; diff --git a/template/src/setupTests.ts b/template/src/setupTests.ts index 52aaef1..79e38eb 100644 --- a/template/src/setupTests.ts +++ b/template/src/setupTests.ts @@ -2,4 +2,9 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom' +import '@testing-library/jest-dom'; +import { configure } from '@testing-library/dom'; + +configure({ + testIdAttribute: 'data-test-id', +}); diff --git a/template/src/types/react-i18next.d.ts b/template/src/types/react-i18next.d.ts new file mode 100644 index 0000000..4eea285 --- /dev/null +++ b/template/src/types/react-i18next.d.ts @@ -0,0 +1,13 @@ +import 'react-i18next'; +import { TFuncKey } from 'react-i18next'; + +import defaultRes from '../../public/locales/en/translation.json'; + +export type TranslationKey = TFuncKey<'translation', undefined, { translation: typeof defaultRes }>; + +declare module 'react-i18next' { + interface CustomTypeOptions { + defaultNS: 'translation'; + resources: { translation: typeof defaultRes }; + } +}